ctx.branch = getCachedBranch(getProjectDir()) lib/context.ts:23 └─ reads the ENV = PRIMARY checkout denial slug = repoSlugFromCwd(input.project_dir) git-validator.ts state read slug = repoSlugFromCwd(input.project_dir) state WRITE slug= repoSlugFromCwd(getProjectDir()) heartbeat-publisher:139 CC sends the WORKTREE as project_dir. So the hook pairs a worktree DIRECTORY with the TRUNK's branch, and reads state nobody wrote there.
Two payloads differing only in project_dir, driven through the built alpha.24 bundle:
DENY branch_in_message=main project_dir = PRIMARY (on main)
DENY branch_in_message=main project_dir = WORKTREE (on fix/3450-precompact-guard-auto)
^ nothing was on main
Byte-identical denials for two different directories is what isolated it. The worktree-awareness of #2363 and the shell_cwd path of #3411 both sit downstream of this, so both were compensating for a branch read from the wrong place.
Writer and reader disagreed on the state key, and repoSlugFromCwd is only basename(). Both files existed for the same session:
~/.claude/state/orchestkit/orchestkit/<sid>.json written, holds shell_cwd ~/.claude/state/orchestkit/precompact-guard/<sid>.json opened by the reader
So shell_cwd was always null for a worktree session: the #3411 mechanism was inert in precisely the scenario it was written for.
cd you ran earlier is invisible to it."
True before #3411, false after. Worse, it collapsed two situations an operator cannot otherwise tell apart. It now branches, and in the lost-track case names the cause: shell_cwd is cleared rather than guessed by any cd it cannot read statically (lib/shell-cwd.ts:75,91).
WT=$(...) && cd "$WT". Following the documented workflow is exactly what clears the record, and nothing said so.
git-validator-worktree.test.ts wrote its fixtures with a local writeShellCwdState() keyed by the test's own project dir. It validated the reader against state keyed the way the reader happened to read it, never the way the writer writes it. That is the same half-contract the #3411 changelog entry criticised in this very file, one layer down. The suite now mocks getProjectDir so writer and reader agree on one directory.
unknown branch at the session dir keeps ctx.branch, so a malformed payload cannot launder a protected-branch push