Skip to content

Worktree sessions: security-pass.py and H-09b/H-10b resolve different project roots — gate pass unrecordable #604

Description

@jmccarthy92

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:

  1. 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
  2. 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
  3. 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

  1. In an arbiter-enabled repo, create a linked worktree and switch the session into it.
  2. Stage a diff containing a CRYPTO_RE-matching line (a prose mention of "RSA-2048 private key" suffices).
  3. Run the secret-handling/crypto-compliance gate to a genuine PASS; run python3 hooks/security-pass.py (writes the worktree marker).
  4. git commitBLOCKED [H-09b] (freshness), despite the fresh, correctly-bound marker one directory tree over.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions