Summary
guard-git.sh's branch-name validator falls back to the session's current branch when it can't parse a working directory out of the command string. For a subagent pushing to a different repository, this denies a perfectly valid push and reports a branch name from a repo the agent has nothing to do with.
Evidence
During the org-wide rollout, a subagent working in its own clone of optave/shared-infrastructure-and-data-lib, on branch chore/codegraph-onboarding (verified via git branch --show-current immediately before pushing), ran:
$ git push -u origin chore/codegraph-onboarding
BLOCKED: Branch 'claude/optave-project-rollout-420503' does not match required pattern.
Branch names must start with: feat/, fix/, docs/, refactor/, test/, chore/, ci/, perf/, build/, release/, revert/
chore/ matches the pattern. claude/optave-project-rollout-420503 is the orchestrator's worktree branch in ops-codegraph-tool — a different repo entirely. The agent retried once; not transient.
Cause
validate_branch_name() (.claude/hooks/guard-git.sh:186) resolves the branch via detect_work_dir(), which recognizes only two command shapes:
git -C <dir> push …
cd <dir> && git push … — and the cd must be anchored at the start (sed -nE 's/^[[:space:]]*cd[[:space:]]+…')
A bare git push that relies on the Bash tool's persistent cwd from an earlier call matches neither, so work_dir is empty and the code falls through to:
if [ -z "$BRANCH" ]; then
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || true
fi
which runs in the hook's own cwd — the session project root — not the repo being pushed. The guard then validates the wrong repo's branch.
This is why the failure looked non-deterministic across parallel agents: those that happened to emit cd <dir> && git push as a single command were validated correctly; those that had cd'd in a previous tool call and then issued a bare git push were not.
Why the fallback is wrong, not just imprecise
The fallback silently answers a different question than the one asked. When the target directory is unknown, the honest options are to resolve it from the process's actual cwd or to decline to validate — not to substitute an unrelated repo's HEAD and deny on it. The failure mode is also actively misleading: the operator is shown a branch name that appears nowhere in the command they ran, in a repo they aren't touching, which sends them looking in the wrong place.
Suggested fix
Resolve the working directory from the shell's real cwd rather than inferring it from command-string shapes. The hook payload already carries the tool invocation; if a reliable cwd is available there, prefer it. Failing that, when work_dir cannot be determined, git rev-parse --show-toplevel on the actual process cwd is still closer to correct than the session root.
Whatever the resolution strategy, the guard should not deny based on a branch it only inferred. If it cannot establish which repo a push targets, it should either allow with a warning or state plainly that it could not determine the target — a false deny on valid work is worse than a missed check here, especially since the same guard already blocks the genuinely dangerous operations.
Two smaller points worth folding in:
- The deny message should name the resolved working directory alongside the branch, so a wrong resolution is obvious immediately instead of after reading the hook source.
- The pattern list in the error text omits
dependabot, which the regex at line 201 actually permits. Minor, but it's the message users act on.
Impact
Blocked a completed, verified rollout deliverable mid-flight and cost a round-trip to diagnose. Any session that spawns subagents pushing to other repos will hit this.
Workaround
Push with the target directory explicit, which is a supported form and not a bypass:
git -C <path-to-clone> push -u origin <branch>
Summary
guard-git.sh's branch-name validator falls back to the session's current branch when it can't parse a working directory out of the command string. For a subagent pushing to a different repository, this denies a perfectly valid push and reports a branch name from a repo the agent has nothing to do with.Evidence
During the org-wide rollout, a subagent working in its own clone of
optave/shared-infrastructure-and-data-lib, on branchchore/codegraph-onboarding(verified viagit branch --show-currentimmediately before pushing), ran:chore/matches the pattern.claude/optave-project-rollout-420503is the orchestrator's worktree branch inops-codegraph-tool— a different repo entirely. The agent retried once; not transient.Cause
validate_branch_name()(.claude/hooks/guard-git.sh:186) resolves the branch viadetect_work_dir(), which recognizes only two command shapes:git -C <dir> push …cd <dir> && git push …— and thecdmust be anchored at the start (sed -nE 's/^[[:space:]]*cd[[:space:]]+…')A bare
git pushthat relies on the Bash tool's persistent cwd from an earlier call matches neither, sowork_diris empty and the code falls through to:which runs in the hook's own cwd — the session project root — not the repo being pushed. The guard then validates the wrong repo's branch.
This is why the failure looked non-deterministic across parallel agents: those that happened to emit
cd <dir> && git pushas a single command were validated correctly; those that hadcd'd in a previous tool call and then issued a baregit pushwere not.Why the fallback is wrong, not just imprecise
The fallback silently answers a different question than the one asked. When the target directory is unknown, the honest options are to resolve it from the process's actual cwd or to decline to validate — not to substitute an unrelated repo's HEAD and deny on it. The failure mode is also actively misleading: the operator is shown a branch name that appears nowhere in the command they ran, in a repo they aren't touching, which sends them looking in the wrong place.
Suggested fix
Resolve the working directory from the shell's real cwd rather than inferring it from command-string shapes. The hook payload already carries the tool invocation; if a reliable cwd is available there, prefer it. Failing that, when
work_dircannot be determined,git rev-parse --show-toplevelon the actual process cwd is still closer to correct than the session root.Whatever the resolution strategy, the guard should not deny based on a branch it only inferred. If it cannot establish which repo a push targets, it should either allow with a warning or state plainly that it could not determine the target — a false deny on valid work is worse than a missed check here, especially since the same guard already blocks the genuinely dangerous operations.
Two smaller points worth folding in:
dependabot, which the regex at line 201 actually permits. Minor, but it's the message users act on.Impact
Blocked a completed, verified rollout deliverable mid-flight and cost a round-trip to diagnose. Any session that spawns subagents pushing to other repos will hit this.
Workaround
Push with the target directory explicit, which is a supported form and not a bypass: