Development record
Agent worktrees commit without the private name-guard: .abcd/.work.local/ is per-worktree, so every isolated-worktree agent commit runs with the banlist layer absent — loudly warned, per design, but the isolated-agent pattern now systematically bypasses a protection the main checkout has. Candidate remedies: the worktree-creation path seeds a pointer to the primary checkout's store, or the hook falls back to reading the primary worktree's local tier
Press Release
Seeded by promotion from iss-370. Expand into the full press-release narrative before planning.
Why This Matters
Graduated from iss-370: Agent worktrees commit without the private name-guard: .abcd/.work.local/ is per-worktree, so every isolated-worktree agent commit runs with the banlist layer absent — loudly warned, per design, but the isolated-agent pattern now systematically bypasses a protection the main checkout has. Candidate remedies: the worktree-creation path seeds a pointer to the primary checkout's store, or the hook falls back to reading the primary worktree's local tier. Read that issue record for the source observation.
Scope Conditions
None stated.
Acceptance Criteria
- Given a name recorded in the private banlist of the primary checkout's
.work.localstore but absent from any store inside a linked git worktree, when an agent stages a commit whose content carries that name from within the linked worktree, then the guard hook resolves the banlist from the primary checkout and blocks the commit on that name. - Given a freshly created linked worktree with no private banlist file of its own, when the guard hook runs there, then it enforces the same banlist as the main checkout without any per-worktree banlist setup having been performed.
- Given a name banned in the linked worktree's own local store, when a commit runs in that worktree, then the guard still blocks it, so the primary store is a fallback and never overrides a worktree-local entry.
- Given the guard runs in a standalone checkout that is not a linked worktree, when no primary-checkout store can be resolved, then it behaves as it does today and emits the existing loud warning rather than failing closed on a resolution error.
Open Questions
None recorded yet.
Audit Notes
Fidelity review — receipt rcp-4b7ee348ffdd (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:d11a1ac6e32a935bec2f2bde66eb8d8a999c960bef0a60ba5b3d4ed5fd164e3c Input attestations: diff:328a6755^1..328a6755 (PR #555), judged against the tree at bad1c73e@-;
Acceptance rollup: MET 3 · MET_WITH_CONCERNS 1 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET: the scaffolded hook resolves the primary checkout via --git-common-dir plus
git worktree list, loads its store as a fallback layer, and the linked-worktree test blocks on a name only the primary store bans evidence: internal/core/ahoy/defaults/pre-commit:198 — "common_dir=$(git rev-parse --path-format=absolute --git-common-dir" evidence: internal/core/ahoy/defaults/pre-commit:247 — "primary_banlist="$primary_root/$banlist"" evidence: internal/core/ahoy/defaults/pre-commit:656 — "load_store "$primary_banlist" "$primary_label" 1" evidence: internal/core/banlist/hook_test.go:885 — "func TestPreCommitHook_LinkedWorktreeInheritsThePrimaryStore" - ac-2 — MET: store_present is true when either store exists, and the test fixture asserts the linked worktree carries no local tier at all before the block is observed evidence: internal/core/ahoy/defaults/pre-commit:412 — "if [ -n "$primary_banlist" ] && [ -f "$primary_banlist" ]; then" evidence: internal/core/banlist/hook_test.go:888 — "the linked worktree already carries a local tier; the fixture proves nothing"
- ac-3 — MET_WITH_CONCERNS: the two stores are loaded as a union (primary first, local second) so a local entry blocks and the primary never displaces it; concern: spc-43 described a duplicate-key local-wins override, and the hook deliberately delivers a union instead, documented in place as the safer direction evidence: internal/core/ahoy/defaults/pre-commit:453 — "The two stores are a UNION, never an override." evidence: internal/core/ahoy/defaults/pre-commit:659 — "load_store "$banlist" "$banlist" 0" evidence: internal/core/banlist/hook_test.go:912 — "func TestPreCommitHook_LinkedWorktreeStoreWinsOverThePrimary"
- ac-4 — MET: resolution failure leaves primary_banlist empty and the hook takes the existing INACTIVE warning branch; the pre-existing warning tests and the no-store-anywhere worktree test all pass at BASE evidence: internal/core/ahoy/defaults/pre-commit:190 — "Resolution NEVER fails the commit." evidence: internal/core/ahoy/defaults/pre-commit:419 — "WARNING — the private name guard is INACTIVE on this machine." evidence: internal/core/banlist/hook_test.go:155 — "func TestPreCommitHook_AbsentBanlistWarnsLoudly" evidence: internal/core/banlist/hook_test.go:965 — "func TestPreCommitHook_LinkedWorktreeWithNoStoreAnywhereStillWarns"
Gap audit:
- honoured:
- the guard hook resolves the primary checkout's store from a linked worktree, asking git which tree is primary rather than path arithmetic evidence: internal/core/ahoy/defaults/pre-commit:223 — "worktree_list=$(git worktree list --porcelain" evidence: internal/core/banlist/hook_test.go:987 — "func TestPreCommitHook_BareRepoWorktreeDoesNotInheritASiblingStore"
- abcd's own committed hook carries the same resolution as the scaffolded default evidence: .githooks/pre-commit:198 — "common_dir=$(git rev-parse --path-format=absolute --git-common-dir" evidence: .githooks/pre-commit:247 — "primary_banlist="$primary_root/$banlist""
- CLI parity:
abcd banlistrenders the inherited layer the guard enforces evidence: internal/core/banlist/worktree.go:78 — "func PrimaryWorktreeRoot(repoRoot string) (string, bool)" evidence: internal/surface/cli/banlist.go:84 — "inherited, err := banlist.InheritedPrivate(root)" evidence: internal/core/banlist/worktree_test.go:137 — "func TestListCarriesTheInheritedLayer" - a standalone checkout keeps today's loud warning instead of failing closed evidence: internal/core/ahoy/defaults/pre-commit:416 — "if [ "$local_present" -eq 0 ] && [ "$primary_present" -eq 0 ]; then"
- diverged:
- spc-43 said a duplicate key from the local store overrides the primary entry; the delivered hook merges the two stores as a union with no override, so both patterns are enforced (strictly more refusals, never fewer) evidence: internal/core/ahoy/defaults/pre-commit:453 — "The two stores are a UNION, never an override." evidence: .abcd/development/specs/closed/spc-43-agent-worktrees-commit-without-the.md:60 — "a duplicate key from the local store overrides the primary entry"
- missing: (none)
Frontmatter
| id | itd-150 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | minor |
.abcd/development/intents/shipped/itd-150-agent-worktrees-commit-without-the.md
open on GitHub ↗ · commit history ↗
Referenced by2
- iss-370Agent worktrees commit without the private name-guard: .abcd/.work.local/ is per-worktree, so every isolated-worktree agent commit runs with the banlist layer absent — loudly warned, per design, but the isolated-agent pattern now systematically bypasses a protection the main checkout has. Candidate remedies: the worktree-creation path seeds a pointer to the primary checkout's store, or the hook falls back to reading the primary worktree's local tierrelated
- spc-43agent-worktrees-commit-without-the-private-name-guard-abcd-wimplemented by
mentions6
- dcn-26092300000000192026-09-23 — The one sanctioned mention of abcd in an adopted repository (product thinker, ruling F of the run A interview, 14:38Z, on iss-2609231103413459). The 2026-09-11 ruling that `ahoy install` must not write abcd by name into a repository it adopts (carried by iss-2609110944498549) covers the name-guard hooks (`.githooks/pre-commit`, `.githooks/pre-merge-commit`) and the `.gitignore` fence, and they are its sanctioned exception: both keep their markers and their naming, documented as the one allowed mention of abcd in an adopted repository, because the hooks run the binary and the fence tells people not to hand-edit it. No rename and no migration: detection classifies an adopted repository by exactly that hook marker and fence, so a rename would have to migrate every repository already adopted. The product thinker ruled the markers, not abcd's record ids: the hook templates' citations of abcd's own records (itd-74, spc-20, itd-150, iss-370, and a path into the design record) resolve to nothing in an adopter's repository, so they are dropped from the templates and a test keeps them out. iss-2609231103413459 closes as wontfix on this ruling.
- iss-2608290808193471Nothing ties a merged implementation to closing its spec, so twelve specs stayed open and twelve intents stayed planned after the code that realises them landed on main: abcd spec close is the verb that advances the intent planned to shipped, closes the spec open to closed, and emits the fidelity-audit receipt, but no gate, lint rule or CI step notices that a spec is still open while its acceptance criteria are demonstrably delivered. The consequence is not cosmetic: the changelog composer reads the shipped-intent and resolved-issue folders, so a release cut derived while the intents sit in planned announces the resolved issues and omits every intent the release actually delivers. Found after the itd-150..162 implementation merged with all fourteen issues resolved but all twelve specs left open. Wants a detector for the shape 'spec open, linked intent planned, and the spec's own scope files changed since the spec was written', which is the same reference-graph the dangling-supersedes ratchet already walks.
- iss-2608290810032799The attribution gate validates the Assisted-by trailer's SHAPE but cannot tell whether the model id names the model that actually did the work, so a wrong but well-formed id passes green. In the itd-150..162 run four implementation agents were briefed with an incorrect model id: two used it as instructed and their commits passed the gate, two recognised it as a false disclosure and stamped their real id instead. The convention exists precisely to prevent a false disclosure, and the gate is blind to the only failure that matters. A full mechanical fix may be impossible because the runner cannot attest which model produced a commit, but two partial rungs exist: refuse a vendor and model pair absent from a maintained known-models list, so a typo or a stale id is caught; and have the dispatching harness supply its own model id to the agent, so a hand-written briefing cannot introduce a wrong one. Distinct from iss-211 (making the attribution preference portable) and iss-220 (composing the trailer at creation time): this is about the truthfulness of a trailer that is present and well-formed.
- iss-2609181122202952The pre-commit name guard prints its inherited-store notice three times per commit from a linked worktree. The committed .githooks/pre-commit announces the itd-150 fallback once per run, at a single site ("pre-commit: abcd name-guard: inheriting the primary checkout's <store>" on stderr), so three copies on one commit mean the hook body ran three times for that commit, which points at the dispatch (the global hooks dispatcher plus the repo hook, or a pre-merge-commit run) rather than at the notice itself. Relayed from the Gropius managed-repo session gropiusllm-56 on 2026-09-18 at v0.9.0, which called it cosmetic. Recorded because the repetition is also a measurement: if the guard runs three times, the scan costs three times what it should, and one run is the one whose refusal counts. Wanted: establish which invocations produce the three runs and either de-duplicate the dispatch or print the notice once per commit.
- itd-148Every change starts in its own worktree in abcd's store: the primary checkout is a read-only surface, and abcd blocks mutations there for every session but a declared coordinator
- itd-2609151516525843A Repository Can Ban A Name On Its First Commit, And A Machine Can Ban One Everywhere