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
- Clone a repo whose GitHub default branch was
X at clone time.
- On GitHub, change the default branch to
main and delete X.
- 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)
- Run
/codex:adversarial-review (or /codex:review) with no --base and a clean working tree, so it falls into the --scope auto → detectDefaultBranch() path.
- 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.
Bug
detectDefaultBranch()inscripts/lib/git.mjsresolves the base branch for--scope auto(used by/codex:reviewand/codex:adversarial-reviewwhen no--baseis passed) via:It trusts this value unconditionally — unlike the
main/master/trunkfallback candidates a few lines below, which each go throughgit show-ref --verify --quietbefore being accepted.refs/remotes/origin/HEADis a local cache written once at clone time (or bygit 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
Xat clone time.mainand deleteX./codex:adversarial-review(or/codex:review) with no--baseand a clean working tree, so it falls into the--scope auto→detectDefaultBranch()path.X, and the subsequentgit merge-base HEAD Xfails:git remote set-head origin -a).Fix suggestion
Before accepting the
symbolic-refresult, verify it actually resolves to something, the same way themain/master/trunkcandidates already do:And/or, when nothing resolves, make the final thrown error actionable:
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-linegit remote set-head. Encountered on a clone whoseorigin/HEADwas 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.