git-validator · branch-switch TOCTOU fix

The hook denied the exact workflow its own error message prescribes.

The bug — checked at PreToolUse time, true at commit time

session HEAD: main
command:  git checkout -b feat/x -q && git commit -m "..." && git push
          └────────┬────────┘
                   │ by the time git commit runs, HEAD is feat/x
hook:     reads ctx.branch = "main" → BLOCKED
message:  "Required workflow: 1. git checkout -b ..."   ← the command DID that

Same class as #2363's worktree gap, on the branch axis instead of the directory axis. Two live false positives on 2026-08-02: the #3245 rebase retry and a branch+commit chain. The identical defect existed in the user-global block-direct-dev-main-push.sh and was fixed the same day with matching semantics.

The fix — judge the branch the mutation will run on

extractPreCommitSwitchTarget(cmd):
  head = cmd up to the FIRST `git commit` / `git push`
  return LAST `git checkout <b>` | `checkout -b/-B <b>` | `switch [-c] <b>` in head
         (flags and file restores excluded)

if target exists and NOT protected  → allow, with context
if target exists and protected      → still block  (checkout main && push)
if no switch in command             → judge ctx.branch as before

The decision matrix (all 9 pinned as tests)

command shape (session on main)verdict
checkout -b feat/x && commit && push (incident 1)ALLOW
checkout existing-branch && rebase && push (incident 2)ALLOW
switch -c feat/y && commitALLOW
plain commit / plain pushBLOCK
checkout main && push (switch target protected)BLOCK
checkout feat/x && checkout main && push (last switch wins)BLOCK
checkout -- paths && commit (file restore ≠ switch)BLOCK
checkout HEAD~1 -- file && commitBLOCK
commit && checkout feat/x (switch AFTER mutation)BLOCK