Worktree sessions: gate-pass producers and commit hooks resolve different project roots (H-09b/H-10b unsatisfiable)
Version: ca 2.11.1, Claude Code host, macOS.
Summary
In a session running inside a linked git worktree (.claude/worktrees/<name> created by the host's EnterWorktree), the sanctioned security-gate producer (hooks/security-pass.py) and the commit guard (_bashguardlib._check_h09b_h10b_crypto_secret) resolve different project roots, so a legitimately-passed crypto/secret gate can never be recorded where the hook looks. The commit is permanently blocked; H-19 (correctly) prevents copying the marker by hand; the only ways out are /ca:override's security ceiling or committing outside the agent.
Mechanism
project_root() resolution order is CLAUDE_PROJECT_DIR → payload cwd → git toplevel → cwd.
- Hook processes (PreToolUse pre-bash.py) inherit
CLAUDE_PROJECT_DIR from the harness = the main checkout (the directory the session was launched from), even after the session's working directory moves into a linked worktree. H-09b therefore reads <main>/.codearbiter/.markers/security-gate-passed.
security-pass.py invoked via Bash (as the crypto-compliance/secret-handling skill prose instructs) runs in the user shell where CLAUDE_PROJECT_DIR is not set, falls through to git rev-parse --show-toplevel from cwd = the worktree checkout, and writes <worktree>/.codearbiter/.markers/security-gate-passed.
Result: freshness check reads a stale/absent marker at a root the producer never writes. Setting CLAUDE_PROJECT_DIR=<main> for the producer doesn't help: candidate_lines() scans root's diff, so it would bind digests from the main checkout's (unrelated, possibly dirty) tree rather than the worktree diff being committed — coverage then fails, and it binds lines nobody reviewed.
Same split presumably affects migration-pass.py / H-14.
Observed
- Hook block:
BLOCKED [H-09b] ... no security-gate pass is recorded immediately after a successful security-pass.py run reported 2 sensitive line(s) bound.
<main>/.codearbiter/.markers/security-gate-passed: mtime 16 h old (previous feature).
<worktree>/.codearbiter/.markers/security-gate-passed: fresh, digests exactly matching the staged sensitive lines.
- Side observation: after a session resume inside the worktree,
${CLAUDE_PROJECT_DIR} rendered in skill bodies as <repo>/.git/worktrees/<name> (the worktree git-dir, not its checkout) — a third root candidate; hook-vs-render disagreement may vary across resume boundaries.
Expected
One root, shared by producers and checks. Either:
- resolve both through the same seam with a worktree rule — e.g. always climb to
git rev-parse --show-toplevel of the invocation cwd for both producer and hook (marker lives with the tree being committed), or
- keep markers in the main checkout but have
security-pass.py accept/derive the scan root from the git toplevel of the diff being approved while writing the marker to the shared root, or
- store markers under
$GIT_COMMON_DIR/.codearbiter-markers/<worktree-id>/ so both sides derive the same location from git itself.
Option 1 preserves the digest-binding TOCTOU protection with the least change: both sides already receive an effective cwd (added_lines(cwd, "--cached") scans the right diff today — only the marker root disagrees).
Repro
- In an arbiter-enabled repo, create a linked worktree and switch the session into it.
- Stage a diff containing a CRYPTO_RE-matching line (a prose mention of "RSA-2048 private key" suffices).
- Run the secret-handling/crypto-compliance gate to a genuine PASS; run
python3 hooks/security-pass.py (writes the worktree marker).
git commit → BLOCKED [H-09b] (freshness), despite the fresh, correctly-bound marker one directory tree over.
Worktree sessions: gate-pass producers and commit hooks resolve different project roots (H-09b/H-10b unsatisfiable)
Version: ca 2.11.1, Claude Code host, macOS.
Summary
In a session running inside a linked git worktree (
.claude/worktrees/<name>created by the host's EnterWorktree), the sanctioned security-gate producer (hooks/security-pass.py) and the commit guard (_bashguardlib._check_h09b_h10b_crypto_secret) resolve different project roots, so a legitimately-passed crypto/secret gate can never be recorded where the hook looks. The commit is permanently blocked; H-19 (correctly) prevents copying the marker by hand; the only ways out are/ca:override's security ceiling or committing outside the agent.Mechanism
project_root()resolution order isCLAUDE_PROJECT_DIR→ payload cwd → git toplevel → cwd.CLAUDE_PROJECT_DIRfrom the harness = the main checkout (the directory the session was launched from), even after the session's working directory moves into a linked worktree. H-09b therefore reads<main>/.codearbiter/.markers/security-gate-passed.security-pass.pyinvoked via Bash (as the crypto-compliance/secret-handling skill prose instructs) runs in the user shell whereCLAUDE_PROJECT_DIRis not set, falls through togit rev-parse --show-toplevelfrom cwd = the worktree checkout, and writes<worktree>/.codearbiter/.markers/security-gate-passed.Result: freshness check reads a stale/absent marker at a root the producer never writes. Setting
CLAUDE_PROJECT_DIR=<main>for the producer doesn't help:candidate_lines()scans root's diff, so it would bind digests from the main checkout's (unrelated, possibly dirty) tree rather than the worktree diff being committed — coverage then fails, and it binds lines nobody reviewed.Same split presumably affects
migration-pass.py/ H-14.Observed
BLOCKED [H-09b] ... no security-gate pass is recordedimmediately after a successfulsecurity-pass.pyrun reported2 sensitive line(s) bound.<main>/.codearbiter/.markers/security-gate-passed: mtime 16 h old (previous feature).<worktree>/.codearbiter/.markers/security-gate-passed: fresh, digests exactly matching the staged sensitive lines.${CLAUDE_PROJECT_DIR}rendered in skill bodies as<repo>/.git/worktrees/<name>(the worktree git-dir, not its checkout) — a third root candidate; hook-vs-render disagreement may vary across resume boundaries.Expected
One root, shared by producers and checks. Either:
git rev-parse --show-toplevelof the invocation cwd for both producer and hook (marker lives with the tree being committed), orsecurity-pass.pyaccept/derive the scan root from the git toplevel of the diff being approved while writing the marker to the shared root, or$GIT_COMMON_DIR/.codearbiter-markers/<worktree-id>/so both sides derive the same location from git itself.Option 1 preserves the digest-binding TOCTOU protection with the least change: both sides already receive an effective cwd (
added_lines(cwd, "--cached")scans the right diff today — only the marker root disagrees).Repro
python3 hooks/security-pass.py(writes the worktree marker).git commit→BLOCKED [H-09b](freshness), despite the fresh, correctly-bound marker one directory tree over.