Context
The companion Engine leaf for neomjs/neo-agent-brain#250 (leaf 11 — seat hook projection). Filed so #250 stays Brain-only and mergeable on its own; this half necessarily lives here because it edits this repository's .gitignore.
ADR 0040 §2.7 rules that seat provisioning materializes harness hooks into target checkouts as generated-not-tracked artifacts, and that the Engine's ignore rules take the generated paths. The second clause is currently unimplemented.
Live latest-open sweep: latest 12 open issues checked at 2026-08-30T21:23:12Z; nearest search hits (#14473, #15364, #17500) are closed and unrelated. Structure-map gate: N/A — no ai/ surface, no new .mjs.
The Problem
Measured on dev:
git check-ignore -v .claude/hooks/x.mjs -> (no output)
git check-ignore -v .codex/hooks/x.mjs -> (no output)
git check-ignore -v .kimi-code/hooks/x.mjs -> (no output)
None of the three projection roots is ignored. .gitignore covers .claude/worktrees/, .claude/settings.json, .codex/config.toml and — the precedent that matters — .claude/skills/, which is exactly this shape: the path neo-agent-skills' materializer projects into, ignored so its untracked symlinks are invisible to git status.
Untracked is not ignored. Once leaf 11 projects hooks, every seat's git status lists them, permanently, in a repository where a clean status is how contributors and agents read their own working state. That is noise which trains people to ignore the signal — and it is the reason §2.7 names ignore rules rather than merely saying "untracked".
This is not hypothetical for one harness: the Kimi seat generator already emits hook paths today.
The Fix
Add tracked ignore patterns for the three harness hook projection roots, mirroring the .claude/skills/ entry exactly in placement and comment style — the projection is generated, never committed.
Scope is deliberately only the projection paths. Engine-owned contributor guards that live in the same directories and are genuinely tracked — .claude/hooks/rgReplaceGuardHook.mjs, the Engine-only carve-out §2.7 names — must remain tracked and visible. A blanket .claude/hooks/ ignore would hide it, so the pattern set has to distinguish them.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Behavior |
Fallback |
Docs |
Evidence |
.gitignore hook-projection patterns |
ADR 0040 §2.7 |
generated hook artifacts are ignored in this repository |
none — absence means visible projection noise |
inline comment beside the patterns, mirroring .claude/skills/ |
git check-ignore -v names the rule for a projected path |
| Tracked Engine-only guards in the same dirs |
ADR 0040 §2.7 census carve-out |
remain tracked and visible |
none — a blanket directory ignore is the defect to avoid |
same comment |
git check-ignore returns nothing for rgReplaceGuardHook.mjs |
Decision Record impact
depends-on ADR 0040 §2.7. Implements the Engine half of leaf 11; neither amends nor challenges the record.
Acceptance Criteria
Corrected 2026-08-31 after @neo-gpt-emmy's intake (IC_kwDODSospM8AAAABRiUViQ). My original carve-out control was vacuous: it asserted that ordinary git check-ignore "returns nothing" for the tracked guard. Git suppresses tracked paths from ordinary check-ignore output regardless of the rules, so that assertion greens under the exact blanket-directory defect this ticket exists to reject. Emmy's correction and census completion are folded in below; both are measured, not accepted on assertion.
Measured controls (throwaway repo, tracked file under .claude/hooks/):
| ignore shape |
instrument |
result |
blanket .claude/hooks/, no negation |
git check-ignore -v |
empty, exit 1 — reads as "not ignored" = false pass |
blanket .claude/hooks/, no negation |
git check-ignore --no-index -v |
.gitignore:1:.claude/hooks/ — correctly reveals it would be ignored |
blanket .claude/hooks/ + !… negation |
git check-ignore --no-index -v |
still .gitignore:1:.claude/hooks/ — the negation is inert: git cannot re-include a file whose parent directory is excluded |
contents-glob .claude/hooks/* + !… negation |
git check-ignore --no-index -v |
.gitignore:2:!.claude/hooks/rgReplaceGuardHook.mjs — the negation is the matched rule |
Two consequences the ACs must encode. The pattern must use the contents glob (.claude/hooks/*), never the bare directory, or the carve-out cannot work at all. And --no-index -v exits 0 in both the ignored and the re-included case, so an exit-code assertion is itself vacuous — the witness is the matched pattern text, specifically whether it begins with !.
Census (verified independently, git ls-files): exactly two tracked survivors — .claude/hooks/rgReplaceGuardHook.mjs and .kimi-code/hooks/README.md. A broader sweep over every ^\.[a-z-]+/hooks/ root returns the same two, so no third root is unaccounted for. .codex/hooks/ has no tracked survivor and therefore takes the bare-directory form correctly.
Out of Scope
- The projector itself, the source census, and the dependency rewrites — neomjs/neo-agent-brain#250.
- Ignore rules for non-Engine target repositories: those are tenant checkouts whose
.gitignore we do not own, and #250's projector handles them through .git/info/exclude.
Avoided Traps
- A blanket
.claude/hooks/ ignore. It would hide the tracked Engine-only guard that correctly survived the cut, converting a deliberate carve-out into an invisible one.
- Treating untracked as sufficient. It is the state today, and it is what makes the projection show up as permanent
git status noise.
Related
- neomjs/neo-agent-brain#250 — leaf 11, the Brain half.
- neomjs/neo#17791 — the leaf-6 removal that stranded the hooks.
Origin Session ID: 7fc710e4-f796-47bc-97b7-b61d1414b3a3
Context
The companion Engine leaf for neomjs/neo-agent-brain#250 (leaf 11 — seat hook projection). Filed so #250 stays Brain-only and mergeable on its own; this half necessarily lives here because it edits this repository's
.gitignore.ADR 0040 §2.7 rules that seat provisioning materializes harness hooks into target checkouts as generated-not-tracked artifacts, and that the Engine's ignore rules take the generated paths. The second clause is currently unimplemented.
Live latest-open sweep: latest 12 open issues checked at 2026-08-30T21:23:12Z; nearest search hits (#14473, #15364, #17500) are closed and unrelated. Structure-map gate: N/A — no
ai/surface, no new.mjs.The Problem
Measured on
dev:None of the three projection roots is ignored.
.gitignorecovers.claude/worktrees/,.claude/settings.json,.codex/config.tomland — the precedent that matters —.claude/skills/, which is exactly this shape: the pathneo-agent-skills' materializer projects into, ignored so its untracked symlinks are invisible togit status.Untracked is not ignored. Once leaf 11 projects hooks, every seat's
git statuslists them, permanently, in a repository where a clean status is how contributors and agents read their own working state. That is noise which trains people to ignore the signal — and it is the reason §2.7 names ignore rules rather than merely saying "untracked".This is not hypothetical for one harness: the Kimi seat generator already emits hook paths today.
The Fix
Add tracked ignore patterns for the three harness hook projection roots, mirroring the
.claude/skills/entry exactly in placement and comment style — the projection is generated, never committed.Scope is deliberately only the projection paths. Engine-owned contributor guards that live in the same directories and are genuinely tracked —
.claude/hooks/rgReplaceGuardHook.mjs, the Engine-only carve-out §2.7 names — must remain tracked and visible. A blanket.claude/hooks/ignore would hide it, so the pattern set has to distinguish them.Contract Ledger Matrix
.gitignorehook-projection patterns.claude/skills/git check-ignore -vnames the rule for a projected pathgit check-ignorereturns nothing forrgReplaceGuardHook.mjsDecision Record impact
depends-on ADR 0040 §2.7. Implements the Engine half of leaf 11; neither amends nor challenges the record.Acceptance Criteria
Measured controls (throwaway repo, tracked file under
.claude/hooks/):.claude/hooks/, no negationgit check-ignore -v.claude/hooks/, no negationgit check-ignore --no-index -v.gitignore:1:.claude/hooks/— correctly reveals it would be ignored.claude/hooks/+!…negationgit check-ignore --no-index -v.gitignore:1:.claude/hooks/— the negation is inert: git cannot re-include a file whose parent directory is excluded.claude/hooks/*+!…negationgit check-ignore --no-index -v.gitignore:2:!.claude/hooks/rgReplaceGuardHook.mjs— the negation is the matched ruleTwo consequences the ACs must encode. The pattern must use the contents glob (
.claude/hooks/*), never the bare directory, or the carve-out cannot work at all. And--no-index -vexits 0 in both the ignored and the re-included case, so an exit-code assertion is itself vacuous — the witness is the matched pattern text, specifically whether it begins with!.Census (verified independently,
git ls-files): exactly two tracked survivors —.claude/hooks/rgReplaceGuardHook.mjsand.kimi-code/hooks/README.md. A broader sweep over every^\.[a-z-]+/hooks/root returns the same two, so no third root is unaccounted for..codex/hooks/has no tracked survivor and therefore takes the bare-directory form correctly.git check-ignore --no-index -vnames a positive ignore rule for a representative projected path under each of.claude/hooks/,.codex/hooks/and.kimi-code/hooks/.git check-ignore --no-index -vnames the explicit negative (!) rule — asserted on the matched pattern text, not on the exit code — for both tracked survivors:.claude/hooks/rgReplaceGuardHook.mjsand.kimi-code/hooks/README.md..claude/hooks/*+!…) for the two roots holding survivors; a bare.claude/hooks/directory rule fails this AC because it renders the negation inert.git ls-filesstill names both survivors after the change.git status --shortclean.Out of Scope
.gitignorewe do not own, and #250's projector handles them through.git/info/exclude.Avoided Traps
.claude/hooks/ignore. It would hide the tracked Engine-only guard that correctly survived the cut, converting a deliberate carve-out into an invisible one.git statusnoise.Related
Origin Session ID: 7fc710e4-f796-47bc-97b7-b61d1414b3a3