Skip to content

detectDefaultBranch() trusts a stale/nonexistent origin/HEAD, fails with opaque 'Not a valid object name' #58

Description

@axisrow

Bug

detectDefaultBranch() in scripts/lib/git.mjs resolves the base branch for --scope auto (used by /codex:review and /codex:adversarial-review when no --base is passed) via:

const symbolic = git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
if (symbolic.status === 0) {
  const remoteHead = symbolic.stdout.trim();
  if (remoteHead.startsWith("refs/remotes/origin/")) {
    return remoteHead.replace("refs/remotes/origin/", "");
  }
}

It trusts this value unconditionally — unlike the main/master/trunk fallback candidates a few lines below, which each go through git show-ref --verify --quiet before being accepted.

refs/remotes/origin/HEAD is a local cache written once at clone time (or by git remote set-head) and never auto-refreshed by git. If a repo's default branch changes on GitHub after clone — including a branch that was later deleted — this local ref keeps pointing at the old, now-nonexistent name.

Repro

  1. Clone a repo whose GitHub default branch was X at clone time.
  2. On GitHub, change the default branch to main and delete X.
  3. In the existing local clone (never re-fetched/re-configured), run:
    git symbolic-ref refs/remotes/origin/HEAD
    # => refs/remotes/origin/X   (stale — X no longer exists anywhere)
    
  4. Run /codex:adversarial-review (or /codex:review) with no --base and a clean working tree, so it falls into the --scope autodetectDefaultBranch() path.
  5. It returns X, and the subsequent git merge-base HEAD X fails:
    fatal: Not a valid object name X
    
    surfaced to the user as an opaque git error with no indication of the actual cause (a stale local ref) or the fix (git remote set-head origin -a).

Fix suggestion

Before accepting the symbolic-ref result, verify it actually resolves to something, the same way the main/master/trunk candidates already do:

const symbolic = git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
if (symbolic.status === 0) {
  const remoteHead = symbolic.stdout.trim();
  if (remoteHead.startsWith("refs/remotes/origin/")) {
    const candidate = remoteHead.replace("refs/remotes/origin/", "");
    const verify = git(cwd, ["show-ref", "--verify", "--quiet", remoteHead]);
    if (verify.status === 0) {
      return `origin/${candidate}`;
    }
    // stale — fall through to the main/master/trunk candidates below
  }
}

And/or, when nothing resolves, make the final thrown error actionable:

throw new Error(
  "Unable to detect the repository default branch (origin/HEAD may be stale — try `git remote set-head origin -a`). Pass --base <ref> or use --scope working-tree."
);

Impact

Low severity (there's a --base <ref> escape hatch), but the failure mode is confusing: the error names a branch the user has likely never heard of and gives no hint that the fix is a one-line git remote set-head. Encountered on a clone whose origin/HEAD was cached against a long-merged-and-deleted feature branch (edit-token-command), even though the actual GitHub default branch (main) had been correct for a long time.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions