Team recap · 24 July 2026 · investigation
Someone asked whether our automated commit helper runs a lint check before it commits. It does not — but a separate piece of plumbing that ships alongside it does, and it stops the commit outright when lint fails.
This was an investigation, not a code change — nothing in the repository was modified. We confirmed the lint gate exists, found it lives one layer below the commit helper, and proved it works by running it against a purpose-built test repository rather than by reading the source.
The finding worth carrying out of the session is the last one: in this repository the gate currently does nothing. It is installed and wired correctly, but it only knows how to check JavaScript-style projects, and this repo has no such project file at its root. Every commit here passes it without any check running.
No source files changed this session. What changed is what the team knows — each block below is a finding, not a code change.
AFFECTS anyone who assumed commit-agent was checking their work
commit-agent and its background twin agent-commit
contain no lint step at all. Both say the opposite in plain text — "STOP
here. Do not run tests, analyze coverage, check for issues". The same is
true of ship, which states "this skill does not run tests" and
mentions lint only as text it writes into a pull request's Test Plan.
kit/plugins/git-agent/skills/commit-agent/SKILL.md
AFFECTS everyone who commits in a repo with git-agent installed
The check lives in a hook — a script the harness runs automatically
before a tool call, without the skill knowing about it. It watches every
shell command, recognizes git commit, and runs the project's
lint script before allowing the commit through. On failure it stops the
commit and hands the lint output back, so the assistant can fix and retry
with no user round-trip.
kit/plugins/git-agent/hooks/lint-before-commit.py
AFFECTS teammates who need to trust the gate
We fed the hook four real inputs and recorded what it did. A failing
lint plus a git commit returned EXIT 2
and blocked the commit; all three cases that should not block returned
EXIT 0. The full matrix is in
Before and after.
Test fixture described under Open items
AFFECTS everyone committing to agentics — though nothing is broken by it today
The hook detects projects by reading package.json at the
repository root. This repo has none, so the hook exits immediately without
running anything. Every commit here — through commit-agent,
through ship, or typed by hand — passes the gate with no check
performed. That is the intended fail-open behaviour for a
Markdown-and-JSON repository, but it means "the lint gate is installed" and
"commits here are lint-checked" are two different claims, and only the
first is true.
Confirmed by running the hook against this repo's root
AFFECTS anyone installing git-agent into another repository
Both halves ship inside the plugin directory, so the gate travels with the plugin to every repo that installs it. This project's own settings register a separate, non-overlapping set of hooks — merge-driver setup, default-branch sync, marketplace validation, an uncommitted-plans warning, and a version-bump guard — and contain no lint hook at all.
kit/plugins/git-agent/hooks.json vs .claude/settings.json
git commit,
and the harness intercepts that request first. Look at the upper branch:
the commit is never created at all, rather than created and then
reverted.What the team assumed at the start of the session, against what the test actually showed.
| Assumed | Actually true |
|---|---|
| The commit helper runs lint | It does not; a hook shipped beside it does |
| Only commit-agent is covered | Any git commit is covered — including ship, background agents, and hand-typed commits |
| A lint failure gives you a commit to fix | The commit is never created; the request is blocked before git runs |
| "Gate installed" means "commits are checked" | Only where a root package.json exists. Here it is a silent no-op |
| The gate is part of this project's config | It ships with the git-agent plugin and travels to every repo that installs it |
| Any command containing "commit" would trip it | git log --grep commit passes cleanly; the pattern anchors on the real subcommand |
| Disabling it means editing the plugin | Create .claude/no-lint-gate at the repo root |
The four cases we actually ran:
| Case | Exit | Outcome |
|---|---|---|
Failing lint, git commit | 2 | Blocked, with the lint output returned |
Failing lint, git log --grep commit | 0 | Allowed — not a commit |
Passing lint, git commit | 0 | Allowed |
| Failing lint, opt-out file present | 0 | Allowed — opt-out honoured |
The user asked for confirmation that lint runs before a commit. Reading the code establishes intent, not behaviour, and this hook has enough fall-through conditions that the two can differ.
Rejected — reason through the source and report.
Would have produced a confident answer that was wrong for this repository:
nothing in the script text announces that it no-ops without a
package.json.
Rejected — run a real commit here and observe. Proves nothing. It would have passed, and passed for the wrong reason — no check ran.
"The gate is installed and working" is true and misleading in the same breath. A teammate acting on it would believe their commits here are checked.
Rejected — lead with the passing test matrix, mention the caveat at the end. Technically complete, but buries the one fact that changes behaviour.
Removing it needs rm, which this project requires explicit
approval for. The fixture sits in a session-isolated scratchpad and harms
nothing.
Rejected — delete it automatically as cleanup. The first attempt did exactly this and was correctly blocked. See Learnings.
rm -rf on the target directory
— routine scripting habit — and was denied. Rebuilding with
mkdir -p against a fresh directory name worked identically.
Reflex rm -rf in a setup script is a habit worth dropping in
this repo.node_modules as "not
installed yet" and exits before running anything, so a fixture without it
silently tests the wrong path and looks like a pass.git log --grep commit contains the literal word and is
correctly ignored; the pattern also excludes commit-tree,
which a plain word-boundary match would have caught by mistake.merge skill re-runs lint explicitly rather
than trusting the commit-time hook ever ran — and reports the gate as
skipped when its preconditions do not hold, rather than as
passed.package.json, so the lint gate never runs. If lint
coverage for the Markdown, JSON, and Python files here is wanted, it needs
a different mechanism — the current hook only understands JavaScript-style
projects. Nothing is broken today; this is a question, not a defect..../scratchpad/lintfix1, including a
package.json with a deliberately failing lint script.
Session-isolated and harmless; removal needs rm, which was not
approved.No files were modified this session. All repository access was read-only. Files examined, grouped by area:
kit/plugins/git-agent/hooks.json — the hook registration,
its event and its matcherkit/plugins/git-agent/hooks/lint-before-commit.py — the
full decision path, detection logic, and exit-code contractskills/commit-agent/SKILL.md — confirmed no lint stepagents/agent-commit.md — confirmed no lint stepskills/ship/SKILL.md — confirmed lint appears only as PR
text, never as a commandkit/plugins/git-agent/README.md — the contrasting
behaviour of merge and ship-autonomous, which do
run lint directly.claude/settings.json — confirmed the project registers no
lint hook of its ownkit/plugins/git-agent/.claude-plugin/plugin.json —
confirmed the hook ships with the plugin<scratchpad>/lintfix1/ — throwaway test repository;
see Open items