Fix the four defects in git-agent's lint-before-commit.py gate so it blocks only on failures the current commit introduces, lints the package the commit actually touches, recognizes non-Node projects, and registers its hook in the Claude Code desktop app.
The commit lint gate currently blocks on lint errors you did not cause, lints the wrong package in a monorepo, and ignores every non-Node project — and in the desktop app it never runs at all. This plan makes the gate block only on newly-introduced failures, resolve the nearest package, understand Python/Go/Rust, and actually register its hook.
Read and implement all steps in the plan at docs/plans/fix-lint-gate-defects.md — Make the commit lint gate trustworthy in every repo it lands in. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/fix-lint-gate-defects.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Achieve this goal: Make the commit lint gate trustworthy in every repo it lands in. The plan at docs/plans/fix-lint-gate-defects.md describes one approach — use it as reference, but optimize for the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/fix-lint-gate-defects.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
fix-lint-gate-defects.html
docs/plans/fix-lint-gate-defects.html
docs/plans/fix-lint-gate-defects.md
Context
The story behind this plan — what prompted the work and why it matters now.
The gate ships as a PreToolUse hook on Bash and fires in every repo that
installs git-agent, so each defect is an every-project defect rather than an
agentics one. All four were measured this session, not inferred.
Pre-existing failures block unrelated commits. The hook runs the host
repo's whole scripts.lint. Walk into a repo with 40 errors you did not
write and every commit is blocked until you fix them or create.claude/no-lint-gate. Four approaches were weighed: passing staged paths as
script arguments (cheap, but eslint . ignores appended paths); parsing
output for staged filenames (no extra runtime, but needs per-linter format
guesses); documenting the limitation only; and comparing against a baseline.
Baseline comparison was chosen because it is the only option that is correct
regardless of which linter the host repo uses. It costs a second lint run on
the failing path — see the timeout arithmetic in Step 6, which is the real
constraint, not the wall-clock cost.
Monorepos lint the wrong package. repo_root() resolvesgit rev-parse --show-toplevel and then reads only the root package.json.
Confirmed by experiment: a commit issued from sub/pkg/ ran the root lint
script and the nested package's own script never executed. Where the root has
no scripts.lint, the gate is silently off no matter what sub-packages
define.
Non-Node projects get no gate. Detection is package.json →scripts.lint, then scripts.typecheck. A Python, Go, or Rust project is a
silent no-op regardless of the linters it has configured.
The hook does not register — anywhere, not just the desktop app.
git-agent, plan-agent, and skill-reviewer all keep hooks.json at the plugin
root with no hooks key in plugin.json. That location was assumed to be
auto-discovered; it is not. The plugins reference lists exactly one
auto-discovered path, hooks/hooks.json.
Probe reading (recorded 2026-08-10, no desktop restart required). Two
controlled A/Bs, each varying one thing:
1. Is the root path read at all? Identical deliberately-corrupt JSON was
placed at hooks/hooks.json in one scratch plugin and at hooks.json in
another. claude plugin validate rejects the first — "Invalid JSON syntax
… At runtime this breaks the entire plugin load" — and passes the second
without comment. The root file is never opened. Adding the manifest key
does not change the validator's behaviour, because the validator checks the
conventional path rather than resolving the manifest field.
2. Does the manifest key fix it? Run against the installed git-agent
4.13.0 in ~/.claude/plugins/cache/, using claude plugin details — the
runtime's own component inventory — as the readout. Control, exactly as
shipped: Hooks (0). Treatment, same files plus"hooks": "./hooks.json": Hooks (2) UserPromptSubmit, PreToolUse. The
installed manifest was restored byte-identically afterwards.
So the declaration style is the whole cause, and the consequence is larger
than "the desktop app": the lint gate has never run for any installed user
on any surface. ponytail, whose manifest names a non-standard hooks
filename, was the correct control all along. The desktop app additionally
drops plugin hooks for its own separate reason, so a desktop session is not a
valid test surface for this fix either way.
Risk: the baseline run inherits the fresh-clone problem. A temp worktree
checked out at HEAD has no node_modules, so lint there fails on a missing
binary and would look like "HEAD was already broken", silently passing every
real failure. Step 6 mitigates by symlinking the host repo's dependency
directory into both worktrees and by treating an unusable baseline as fall
back to today's whole-project blocking, never as skip the gate. The
symlink carries one accepted limitation: a dependency change staged in the
commit is not reflected in the HEAD side, so a failure caused purely by a
dependency bump reads as pre-existing.
The comparison is pinned to the index, not the working tree. Comparing a
live working tree against HEAD races against your own edits and against any
other agent writing during the hook's run. Both sides are therefore
materialized as detached worktrees — one at HEAD, one at the staged index —
which also changes what the gate checks: it now checks what is being
committed rather than what happens to be on disk. Unstaged edits stop being
linted. For the commit-agent path this is a no-op, since Step 2 of that
skill runs git add -A.
Landing shape. The rewrite touches every existing safety invariant, so it
lands as four commits behind one PR — nearest-package, ecosystems, config,
baseline — each with the full test file green before the next begins.
Files that change
Every file this plan touches, and what happens to each one.
kit/plugins/git-agent/hooks/lint-before-commit.pymodified nearest-package resolution, ecosystem detection, config override, baseline comparisonkit/plugins/git-agent/hooks.jsonmodified hook timeout raised to cover the baseline runkit/plugins/git-agent/.claude-plugin/plugin.jsonmodified explicithookskeykit/plugins/plan-agent/.claude-plugin/plugin.jsonmodified explicithookskeykit/plugins/skill-reviewer/.claude-plugin/plugin.jsonmodified explicithookskey.claude-plugin/marketplace.jsonmodified version bumps for the three surviving pluginskit/plugins/plan-agent/CHANGELOG.mdmodified entry for the hook-registration fixkit/plugins/skill-reviewer/CHANGELOG.mdmodified entry for the hook-registration fix- kit/plugins/git-agent/
CHANGELOG.mdmodified entry for the gate rewriteREADME.mdmodified document ecosystems, config file, baseline behavior
tests/plugins/test-lint-before-commit.shmodified new sections for all four fixes
Steps
The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.
scratchpad/hook-probe.py (git-agent as control with a SessionStart echo only, plan-agent as treatment with the echo plus "hooks": "./hooks.json"), restart the desktop app, and record which banner appears.
HOOKPROBE git-agent CONTROL fired, HOOKPROBE plan-agent TREATMENT fired, both, or neither — write the observed result into this plan's Context before continuing."hooks": "./hooks.json" to the .claude-plugin/plugin.json of git-agent, plan-agent, skill-reviewer, and plan-interview; if CONTROL also fired, skip the manifest edits and instead record the real cause in Context.
git-agent's merge-shorthand hook fires on the literal prompt merge? in a desktop session.package.json lookup with nearest-package resolution — walk up from the payload's cwd to the git root and use the first directory whose manifest declares a matching script, keeping the git root as the walk's hard ceiling.
sub/pkg/ must lint sub/pkg, and the walk must not escape the repository into a parent directory's unrelated manifest.sub/pkg fixture blocks with the nested script's marker in the output, and the root script's marker is absent.pyproject.toml (ruff, then flake8), go.mod (go vet), and Cargo.toml (cargo clippy), reusing the same nearest-manifest walk and the same could-not-run guards as the Node path.
.claude/lint-gate.json config override that names a repo's own check commands and, when present, replaces built-in detection entirely rather than adding to it.
package.json lint script passes, proving detection was skipped rather than merged.file:line:message records relative to their own roots, and block only on records present in the index run and absent at HEAD; fall back to today's whole-project block with a message saying the baseline was unavailable, and re-budget PER_CHECK_TIMEOUT against hooks.json so two checks plus their baselines stay inside the declared hook timeout.
tests/plugins/test-lint-before-commit.sh with sections covering nearest-package resolution, each new ecosystem, the config override, baseline pass and block, the baseline-unavailable fallback, and a check pinning that the commit regex bails before any filesystem probing — keeping the existing make_repo/fire/check_rc helpers and all 38 current checks passing.
bash tests/plugins/test-lint-before-commit.sh reports zero failures with a total check count above 38.git-agent to 4.14.0 and plan-agent, skill-reviewer, plan-interview by a patch level in .claude-plugin/marketplace.json, add the git-agent CHANGELOG entry, and document the ecosystems, the config file, and the baseline behavior in the git-agent README.
git fetch origin && BASE_REF=main node scripts/check-plugin-versions.mjs exits 0.Tests
The tests that prove the change does what it promises.
.claude/lint-gate.json parsing; Key cases: config command beats built-in detection, malformed config is a silent no-op, .claude/no-lint-gate still overrides the config-C retargeting, git log --grep commit near-miss, and the opt-out markerDefinition of done
The plan counts as done when every statement below is true — check each one off as you verify it.
Final check
One last pass to confirm the whole change works end to end.
Run bash tests/plugins/test-lint-before-commit.sh and confirm zero failures
with a check count above 38. Then rungit fetch origin && BASE_REF=main node scripts/check-plugin-versions.mjs
and confirm exit 0.
End-to-end, build a scratch monorepo outside this repository with a root
package that passes lint, a nested packages/api whose lint fails on a
pre-existing error, and dependencies installed. Fire the hook with agit commit payload whose cwd is packages/api and confirm exit 0 — the
failure pre-existed. Add a new lint error to a file in packages/api, fire
the same payload, and confirm exit 2 with only the new error quoted in the
block message and the root package's script never invoked.
Finally, confirm the gate registers at all — the precondition for every other
guarantee here. The planned check (type merge? in a desktop session) was
replaced by a stronger one that needs no restart and no desktop: runclaude plugin details git-agent and read the component inventory. Shipped
layout reports Hooks (0); the same files with the manifest key reportHooks (2) UserPromptSubmit, PreToolUse. This supersedes the desktop check
in both directions — it observes registration directly rather than inferring
it from a hook's side effect, and the desktop app drops plugin hooks for a
separate reason, so it could never have confirmed this fix.
Wrapping up
Three gates that must all pass before this plan is marked completed.
Completion Report
- Hook registration
- verified by a controlled A/B on the installed plugin (
Hooks (0)→Hooks (2)), not by the planned desktop-restart probe, which is neither necessary nor valid for this defect - Steps 1 and 2 merged in practice
- the probe and the manifest edits landed together once the first A/B showed the root path is never read; the plan's branch where CONTROL also fires did not occur
plan-interviewmanifest- not edited; the plugin was folded into
plan-agent4.0.0 and is not in this marketplace, so three plugins were bumped rather than four - Output normalization (Unresolved Question 1)
- resolved without per-tool JSON formats: records are digit-masked, path-stripped lines compared as a multiset, so a record is new only when its count rises
- Baseline cost ceiling (Unresolved Question 2)
- resolved as 120s primary / 60s baseline / 30s materialization, inside a hook timeout raised 200s → 480s; a baseline timeout degrades to whole-project blocking
- Two repo tests skipped
test-imperative-pruning.shandtest-skill-behavior-baselines.shdrive the liveclaudeCLI against recorded model-behavior baselines for five SKILL.md files untouched by this change; the other 45 suites pass- Post-merge check outstanding
- the gate cannot run for installed users until this version ships and is reinstalled; nothing in this branch can advance that