Team recap · 24 July 2026 · investigation

git-agent's pre-commit lint gate and where it is inert

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.

At a glance

0Code changes shipped
0Files modified
7Files read
4Findings verified by test
3Decisions
3Open items

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.

What changed

No source files changed this session. What changed is what the team knows — each block below is a finding, not a code change.

The commit helper does not run lint itself

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

A hook does run lint, one layer down

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

The gate is confirmed working — tested, not just read

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

In this repository the gate is inert

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

The gate belongs to the plugin, not to this project

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

How it works now

gitlint-before-commit.pyHarness PreToolUsecommit-agent or shipgitlint-before-commit.pyHarness PreToolUsecommit-agent or shipcommit is never createdalt[lint fails][lint passes, or check skipped]run "git commit -m ..."command and working directorynpm run lint, then typecheckexit 2 plus lint outputBlocked - fix and retryexit 0git commitcommit created
The skill never calls lint. It asks to run 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.

no

yes

no

yes

yes

no

no

yes

no

yes

passed

could not run or timed out

failed

Any shell command

Is it git commit?

exit 0 - allow

Inside a git repo?

Opt-out file present?

package.json readable?

Dependencies installed?

Run lint, then typecheck

Result

exit 2 - BLOCK the commit

Every path but one ends in "allow". Only a check that actually ran and actually found something is permitted to block — the single branch reaching BLOCK is the only exit that stops a commit.

Before and after

What the team assumed at the start of the session, against what the test actually showed.

AssumedActually true
The commit helper runs lintIt does not; a hook shipped beside it does
Only commit-agent is coveredAny git commit is covered — including ship, background agents, and hand-typed commits
A lint failure gives you a commit to fixThe 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 configIt ships with the git-agent plugin and travels to every repo that installs it
Any command containing "commit" would trip itgit log --grep commit passes cleanly; the pattern anchors on the real subcommand
Disabling it means editing the pluginCreate .claude/no-lint-gate at the repo root

The four cases we actually ran:

CaseExitOutcome
Failing lint, git commit2Blocked, with the lint output returned
Failing lint, git log --grep commit0Allowed — not a commit
Passing lint, git commit0Allowed
Failing lint, opt-out file present0Allowed — opt-out honoured

Decisions

Verify with a purpose-built fixture instead of reading the script

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.

Report this repo's no-op as a headline, not a footnote

"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.

Leave the test fixture in place rather than deleting it

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.

Learnings

Open items

Files touched

No files were modified this session. All repository access was read-only. Files examined, grouped by area:

The lint gate itself

Skills checked for a lint step

Project configuration

Created outside the repository

Glossary

git-agent
The plugin in this marketplace that automates git work: creating branches, writing commits, opening pull requests, merging.
commit-agent
The part of git-agent that writes a commit message and commits. The subject of the original question.
ship
The part of git-agent that commits, pushes, and opens a pull request in one go.
merge
The part of git-agent that checks whether a pull request is ready and merges it. Unlike the others, it runs lint directly.
Hook
A script the harness runs automatically around tool calls. It runs whether or not the assistant knows it exists, which is why it can enforce a rule that a written instruction cannot.
PreToolUse
The hook event that fires before a tool call runs, so the hook can block the call. The lint gate uses this; the project's own hooks use PostToolUse, which fires after and cannot block.
Exit code
The number a script returns when it finishes. Here, 0 means allow and 2 means block.
Fail-open
A design where anything unrecognized is allowed through rather than blocked. Chosen here because the hook runs on every shell command in every repo that installs the plugin, so a false block would be far more disruptive than a missed check.
package.json
The file that defines a JavaScript project and its named scripts, including lint. The hook's only detection mechanism.
node_modules
The directory holding a JavaScript project's installed dependencies. Its absence tells the hook the project is not set up yet.
Lint / typecheck
Automated checks for, respectively, code style and likely mistakes, and type correctness.
Working tree
The files as they currently sit on disk, including edits not yet committed.
Scratchpad
A temporary directory scoped to one session, used for files that should not enter the repository.