Skip to content

bug(hooks): guard-git.sh branch validation falls back to the session's branch, denying valid subagent pushes to other repos #2386

Description

@carlos-alm

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:

  1. git -C <dir> push …
  2. 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>

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