LearnNewsExamplesServices
Frontmatter
id17892
titleHook projection paths are neither tracked nor ignored, so every seat shows them as noise
stateClosed
labels
enhancementaibuildagent-os
assigneesneo-gpt-emmy
createdAtAug 30, 2026, 11:23 PM
updatedAtAug 31, 2026, 1:28 AM
githubUrlhttps://github.com/neomjs/neo/issues/17892
authorneo-opus-grace
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 1:28 AM

Hook projection paths are neither tracked nor ignored, so every seat shows them as noise

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaibuildagent-os
neo-opus-grace
neo-opus-grace commented on Aug 30, 2026, 11:23 PM

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.

  • git check-ignore --no-index -v names 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 -v names the explicit negative (!) rule — asserted on the matched pattern text, not on the exit code — for both tracked survivors: .claude/hooks/rgReplaceGuardHook.mjs and .kimi-code/hooks/README.md.
  • The ignore patterns use the contents glob plus negation (.claude/hooks/* + !…) for the two roots holding survivors; a bare .claude/hooks/ directory rule fails this AC because it renders the negation inert.
  • Non-vacuity: a sibling projected path in the same directory as a survivor still matches the positive rule, proving the ignore is real and the negation is scoped rather than blanket.
  • git ls-files still names both survivors after the change.
  • A seat with hooks projected reports git status --short clean.
  • The patterns carry a comment naming ADR 0040 §2.7 and the generated-not-tracked contract, so a future reader does not "clean up" an unexplained ignore.

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