docs: add detached-HEAD refspec recovery and the zsh status gotcha to audit-git-checkouts #49
Loading…
Reference in a new issue
No description provided.
Delete branch "align/audit-git-checkouts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Relocates the two verified facts from setup-atlas's
sync-reposskill ahead of that skill's deletion: recoveringorigin/<default>when a narrow fetch refspec (e.g. tag-only) hides it while moving a detached checkout onto its default branch, and zsh's read-onlystatusparameter breaking ad hoc sweep loops. The setup-atlas deletion PR depends on this landing first.Approach review: The overall approach is sound. Relocating two verified facts into
audit-git-checkoutsahead of deleting thesync-reposskill is the right sequencing, the change adds no new machinery, and each fact lands next to the material it qualifies (checkoutComparison/detached-reference handling; the jq reporting recipes).One material alternative on the recovery procedure: the prescribed fix persists a widened
remote.<name>.fetchin the owner's repository, when a one-shot refspec on the fetch command achieves the same recovery without mutating owner-configured state.scripts/audit-checkouts.sh:801already takes exactly that route — it fetches'+refs/heads/*:refs/remotes/origin/*'explicitly rather than trusting or editing the configured refspec — so the skill would be advising a config edit for a problem its own driver routes around. A narrow refspec is often deliberate (a tag-only mirror kept cheap on purpose), and widening it changes every future fetch in that checkout, including ones unrelated to this skill. Details inline.Minor, non-blocking: the new paragraph is the one place in the skill that authorizes switching branches in an audited checkout, and it sits in the read-only "Audit workflow" section while "Safety boundary" states the automatic update "never switches branches" and "Cleanup when explicitly authorized" owns owner-authorized mutations. Worth a pointer from that section if the mutation guidance is meant to be discoverable from there.
Approach review by Claude Code Opus (opus)
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then aFor every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.When the owner explicitly asks to move a detached checkout onto its default branch, switch only after confirming the working tree is clean, and verify the remote-tracking ref exists first with `git show-ref --verify refs/remotes/<remote>/<default>`: a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it. If the ref is missing, inspect `remote.<name>.fetch`, add `+refs/heads/*:refs/remotes/<remote>/*`, fetch again, then create the tracking branch and update with `git pull --ff-only`.Consider recovering with a one-shot refspec instead of persisting a config change.
git fetch <remote> '+refs/heads/*:refs/remotes/<remote>/*'(optionally narrowed to+refs/heads/<default>:refs/remotes/<remote>/<default>) populates the missing ref without touchingremote.<name>.fetch, which is the same technique the bundled driver already uses atscripts/audit-checkouts.sh:801— it passes the heads refspec explicitly rather than relying on the configured one. Two reasons to prefer it here: a narrow refspec is frequently intentional (a tag-only mirror), so widening it silently changes the cost and result of every later fetch in that checkout; and because the driver already fetches heads explicitly fororigin, a missingrefs/remotes/origin/<default>after a successful audit run points at a failed fetch (defaultComparison.fresh != true, per the--no-fetchparagraph above) rather than at the refspec — so config editing would be treating the wrong cause for theorigincase, and only a non-originremote reaches this path with a narrow refspec as the real explanation.Adopting the one-shot fetch also simplifies the tail of the procedure: with the ref freshly fetched,
git switch <default>(DWIM off the remote-tracking ref) already lands on the fetched tip, so the trailinggit pull --ff-onlyis only needed to work around the still-narrow configured refspec — the step that motivated the config edit in the first place.Summary: Docs-only change adding two paragraphs to
skills/audit-git-checkouts/SKILL.md. Found 2 medium issues, both in the new detached-HEAD recovery paragraph (line 61); one carries an additional low-severity note.I reproduced the paragraph's premise in a scratch repo: with
remote.origin.fetchnarrowed to+refs/tags/*:refs/tags/*,git fetch originleavesrefs/remotes/origin/mainabsent whilegit ls-remote --heads originstill reports the branch, andgit show-ref --verify refs/remotes/origin/mainexits 128. The documented failure mode is real. The problems are with the two remedies prescribed around it: the clean-tree precondition does not cover commits made on the detached HEAD, and the refspec fix mutates owner config where a one-shot fetch (already whatscripts/audit-checkouts.sh:801does) suffices.The zsh
statusnote (line 135) could not be verified empirically — no zsh in this environment and no Perplexity credential foraskpplx— so I am not asserting anything about it either way. It matches the documented zsh special-parameter behavior and I found no reason to doubt it.Code review by Claude Code Opus (opus)
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then aFor every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.When the owner explicitly asks to move a detached checkout onto its default branch, switch only after confirming the working tree is clean, and verify the remote-tracking ref exists first with `git show-ref --verify refs/remotes/<remote>/<default>`: a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it. If the ref is missing, inspect `remote.<name>.fetch`, add `+refs/heads/*:refs/remotes/<remote>/*`, fetch again, then create the tracking branch and update with `git pull --ff-only`.🟡 Medium: The precondition — "switch only after confirming the working tree is clean" — does not cover the case this skill treats as most precious. Line 18 states "Treat every dirty file, local commit, and stash as user work," but a clean
git status --porcelainsays nothing about commits made on the detached HEAD, which are reachable from no ref at all.Reproduced: committed on a detached HEAD, confirmed
git status --porcelainempty andgit for-each-ref --contains <sha>empty, thengit switch mainsucceeded and left the commit reflog-only (subject togc.reflogExpireUnreachable, 30 days by default). Git's "you are leaving 1 commit behind" advice prints only after the switch, so an agent following the documented gate has already moved HEAD before seeing it.Add a reachability gate alongside the cleanliness one: refuse the switch unless the detached tip is named by something, e.g.
git for-each-ref --contains HEADis non-empty, orgit merge-base --is-ancestor HEAD refs/remotes/<remote>/<default>succeeds. Otherwise report the unique commits to the owner rather than switching — the same standard the containment ladder applies everywhere else in this skill.🟡 Medium: The recovery step — "add
+refs/heads/*:refs/remotes/<remote>/*" toremote.<name>.fetch— makes a permanent change to the owner's repository configuration to fix a one-time visibility problem, and a narrow refspec is usually deliberate (a--single-branchclone, a tag-only mirror). Widening it silently changes what every futuregit fetchin that repository pulls down and prunes.A one-shot fetch with an explicit refspec recovers the ref without touching config. Verified in a scratch repo with
remote.origin.fetchset to+refs/tags/*:refs/tags/*only:This is exactly what the driver already does at
scripts/audit-checkouts.sh:801, so the persistent edit is not needed for the audit path either. Suggest recommending the explicit-refspec fetch, and mentioning the config edit only as an owner-authorized option when they want the widening to persist.🟢 Low: In the same paragraph, prefer
git show-ref --verify --quiet refs/remotes/<remote>/<default>for the existence check. Without--quieta missing ref printsfatal: 'refs/remotes/origin/main' - not a valid refand exits 128; with it, exit 1 and no output. In a skill that trains the agent to triageoperational/outcomes from stderr, an expected-negative check emittingfatal:invites misreading it as a repository-level failure.All three findings are incorporated in
3f9690f; the paragraph at line 61 is rewritten:git for-each-ref --contains HEADprinting at least one line, with the explicit warning that a clean porcelain says nothing about detached-HEAD commits and that git's "leaving commits behind" advice only prints after HEAD has moved. An unreachable tip is reported to the owner instead of switched away from.git fetch --prune <remote> '+refs/heads/*:refs/remotes/<remote>/*', with the rationale that a narrow refspec is often deliberate and widening it changes every future fetch;git config --get-all remote.<name>.fetchstays as the diagnostic, and persisting a wider refspec is framed as an owner-choice only. The trailinggit pull --ff-onlyis gone — with the ref fetched,git switch <default>lands on the fetched tip, as the approach review noted.--quieton the existence check (44033 low): adopted, with the stderr-triage rationale inline.On the approach review's non-blocking placement note: leaving the paragraph in the audit-workflow section is deliberate — it qualifies the detached-checkout reporting guidance directly above it, and its opening clause ("When the owner explicitly asks…") is the same authorization language the cleanup section uses, so the mutation gate travels with the text rather than through a cross-reference. The cleanup section governs removals, which this is not.
On the unverified zsh note (review body): confirmed live on this workstation before the original commit —
zsh -c 'status=5'exits 1 withzsh:1: read-only variable: status.Summary: Found 1 high and 1 medium issue, both in the new detached-HEAD recovery paragraph in
skills/audit-git-checkouts/SKILL.md.I reproduced the documented scenario (tag-only
remote.origin.fetch, no local default branch, detached at a tag) against git 2.39.5. The reachability gate (git for-each-ref --contains HEADprints nothing for commits made on a detached HEAD, exit 0 either way), thegit show-ref --verifyquiet/non-quiet split (fatal:+ exit 128 without--quiet, silent exit 1 with it), and the ordering of git's "leaving N commits behind" advice all check out exactly as written. The two problems are in the recovery steps that follow:git switch <default>does not work under a narrow refspec, and--pruneon the one-shot fetch deletes other remote-tracking refs the narrow refspec maintained.The zsh
statusnote was not machine-verified (no zsh in this environment), but matches zsh's documented read-only special parameter.Code review by Claude Code Opus (opus)
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then aFor every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. Recover with a one-shot explicit refspec, `git fetch --prune <remote> '+refs/heads/*:refs/remotes/<remote>/*'`, rather than editing the configured refspec: a narrow refspec is often deliberate, and widening it changes every future fetch in that checkout. With the ref fetched, `git switch <default>` creates the tracking branch at the fetched tip. Persist a wider `remote.<name>.fetch` only when the owner wants the widening to stay.🟠 High:
git switch <default>does not work in the situation this paragraph describes, so an agent following the recipe hits a fatal error at the last step.The DWIM that creates a local branch from
<remote>/<default>resolves the remote-tracking ref through the remote's configured fetch refspec, not by looking for the ref on disk. With the refspec left narrow, the mapping fails even though the one-shot fetch just created the ref. Reproduced on git 2.39.5 (tag-onlyremote.origin.fetch, no localmain,refs/remotes/origin/mainpresent):Only after
git config --add remote.<name>.fetch '+refs/heads/*:refs/remotes/<name>/*'doesgit switch mainsucceed and printbranch 'main' set up to track 'origin/main'.This also inverts the closing sentence. Leaving the refspec narrow does not just skip a nicety: the branch gets no upstream, and a later plain
git fetch --prune <remote>never refreshesrefs/remotes/<remote>/<default>again — I advanced the upstream and confirmedorigin/mainstayed at the old SHA — so this skill's owndefaultComparisonfor that checkout is stale from the next audit onward.Suggested correction: say that the switch itself needs
git switch -c <default> <remote>/<default>(no upstream, and--track/--set-upstream-toboth refuse), and that a checkout that should keep tracking the default branch needs the heads refspec persisted — the one-shot fetch recovers the ref for a single inspection, not a self-maintaining tracking branch.🟡 Medium:
--pruneon the one-shot fetch is not as side-effect-free as "rather than editing the configured refspec" implies. Prune scope comes from the command-line refspec, so+refs/heads/*:refs/remotes/<remote>/*prunes everything underrefs/remotes/<remote>/that has no counterpart underrefs/heads/upstream — including refs the narrow refspec deliberately maintains. Reproduced on git 2.39.5 withremote.origin.fetch = +refs/tags/*:refs/remotes/origin/tags/*:Since this paragraph exists precisely for checkouts with non-default refspecs, that is the population most likely to be hit. There is also nothing to prune on a first fetch of a ref that was missing. Dropping
--prune, or narrowing to the one ref actually needed (git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'), keeps the recovery genuinely read-mostly.Both findings reproduced here on git 2.55.0, matching your 2.39.5 results, and both are incorporated in
2b0764a:git switch maindies withfatal: invalid reference: maineven withrefs/remotes/origin/mainfreshly on disk,--trackrefuses, and a branch created bare gets no upstream. The paragraph now states the mapping requirement outright and prescribesgit remote set-branches --add <remote> <default>, which appends exactly the single-branch refspec beside the narrow one (verified: after it,git switch maincreates the branch and printsbranch 'main' set up to track 'origin/main'). That also un-inverts the closing sentence: the persistent mapping is required for the ask this paragraph covers — a checkout living on its default branch that stays fetchable — and the all-heads widening stays owner-choice. The staleness consequence (a bare branch never refreshed,defaultComparisonstale from the next run) is stated inline.--prunedeletedorigin/tags/v1.0, the ref the narrow refspec maintained. The one-shot form survives only as the inspect-without-adopting option, narrowed to the single ref and with the prune-scope warning attached.The one-shot-recovers-everything shape from the previous round is gone; thanks for running it to ground.
Summary: Docs-only change to
skills/audit-git-checkouts/SKILL.md. I reproduced every git claim in the new detached-HEAD paragraph on git 2.39.5 against a purpose-built narrow-refspec fixture, and they hold exactly as written:git for-each-ref --contains HEADprints zero lines for an unreachable detached tip and the “leaving commits behind” advice only prints after the switch;git show-ref --verifywithout--quietemitsfatal: 'refs/remotes/origin/main' - not a valid ref;git switch maindies withfatal: invalid reference: maineven after the ref is on disk;--trackrefuses withstarting point 'origin/main' is not a branch;git remote set-branches --addappends precisely+refs/heads/main:refs/remotes/origin/mainand the subsequentgit switchcreates the branch with its upstream set; and the--pruneglob form does delete a narrow-refspec-maintainedrefs/remotes/origin/pinned. The zshstatusnote matches documented zsh behaviour (zsh was not installed here, so that one is unverified locally).Found 2 medium issues, both where the new text contradicts this skill’s own
scripts/audit-checkouts.sh: the driver's command-line fetch refspec keepsorigin/<default>fresh regardless of the configured narrow refspec (so the stateddefaultComparisonstaleness does not occur), and the driver itself runs the very--pruneglob form the paragraph says to never use.Code review by Claude Code Opus (opus)
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then aFor every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. The switch cannot ride a one-shot fetch: `git switch <default>` maps the branch through the remote's configured refspec, not through refs on disk, so under the still-narrow refspec it dies with `fatal: invalid reference` even after the ref is fetched, `--track` refuses (`starting point '<remote>/<default>' is not a branch`), and a branch created without an upstream is never refreshed by later fetches — this skill's own `defaultComparison` for that checkout goes stale from the next run. The owner asked for a checkout living on its default branch, so persist the minimal mapping: `git remote set-branches --add <remote> <default>` appends `+refs/heads/<default>:refs/remotes/<remote>/<default>` beside the narrow refspec it deliberately leaves alone; then `git fetch <remote>`, and `git switch <default>` creates the branch with its upstream set and future fetches covering it. Widen to all heads only when the owner wants that. To merely inspect the branch without adopting it, a one-shot `git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'` recovers just that ref — never the `--prune` glob form, whose prune scope is the command-line refspec, deleting every ref under `refs/remotes/<remote>/` that the narrow refspec maintains elsewhere.🟡 Medium: The clause “this skill’s own
defaultComparisonfor that checkout goes stale from the next run” does not hold for this driver, so it is a false justification for a persistent config mutation.scripts/audit-checkouts.sh:801fetches with an explicit command-line refspec —git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'— which overrides the configured narrow refspec, sorefs/remotes/origin/<default>is refreshed on every fetching run no matter whatremote.origin.fetchsays. And both consumers read that ref directly rather than@{upstream}:defaultComparisonisgit rev-list --left-right --count "HEAD...refs/remotes/origin/$default_branch"(line 615) and the fast-forward isgit merge --ff-only "refs/remotes/origin/$default_branch"(line 691). A local branch created without an upstream therefore still gets a freshdefaultComparisonand is still eligible for the fast-forward.Verified on git 2.39.5: in a checkout whose only configured refspec was
+refs/tags/v1:refs/remotes/origin/pinned, the driver’s exact fetch command advancedorigin/mainfrom6f0a8aato the new remote tip.The
git remote set-branches --addrecommendation still stands on its other grounds (the owner’s owngit fetch/git pullneed the mapping); drop or correct just this consequence clause.🟡 Medium: “never the
--pruneglob form” contradicts this skill’s own driver, which runs exactly that form unconditionally:scripts/audit-checkouts.sh:801isgit fetch --prune origin '+refs/heads/*:refs/remotes/origin/*', executed against every repository on every run that is not--no-fetch.The hazard described is real — verified on git 2.39.5 with
remote.origin.fetch = +refs/tags/v1:refs/remotes/origin/pinned, that command printed- [deleted] (none) -> origin/pinned— but it means the deletion has already happened during the audit fetch, before an agent ever reaches this paragraph, and an agent following the instruction cannot reconcile it with the tool it was told to run. (The loss is recoverable: a later plaingit fetch originrestores the ref from the configured refspec.)Either scope the warning to the ad hoc case and say the driver’s own fetch already prunes this way, or make the driver’s prune scope match the configured refspec.
Both findings adopted in
1b1e6eb— thank you for checking the prose against the driver.git fetch/git pullstay blind to the branch) and credits the audit fetch's command-line glob refspec with keepingorigin/<default>fresh regardless.--prunewarning is scoped to ad hoc glob fetches, and the paragraph now discloses that the driver's own fetch is exactly that pruning glob — so on a narrow-refspec repository those refs are already gone by report time, recoverable with a plaingit fetch.The driver's glob+prune fetch stays as is: the audit deliberately wants complete, prune-fresh
origin/*state, and the prose now matches it instead of contradicting it.Summary: No actionable issues found; 1 informational note.
Docs-only change adding two paragraphs to
skills/audit-git-checkouts/SKILL.md. I reproduced every runtime claim in scratch repositories with a tag-only fetch refspec on git 2.39.5, and all of them hold:git for-each-ref --contains HEADprints nothing for an unreachable detached tip while still exiting 0 — the paragraph correctly gates on "prints at least one line" rather than exit status, andHEADitself is not listed by default, so the check cannot satisfy itself.git show-ref --verify refs/remotes/origin/mainwithout--quietwritesfatal: 'refs/remotes/origin/main' - not a valid refand exits 128; with--quietit exits 1 silently.git switch maindiesfatal: invalid reference: maineven after a one-shot fetch createdrefs/remotes/origin/main, and--trackrefuses withcannot set up tracking information; starting point 'origin/main' is not a branch.git remote set-branches --add origin mainappends exactly+refs/heads/main:refs/remotes/origin/mainbeside the untouched tag refspec; a plaingit fetch originthen makesgit switch maincreate the branch with its upstream set.origin/mainhas no upstream:git pullfails withfatal: no upstream configured.git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'deletedrefs/remotes/origin/pr/7that the configured narrow refspec maintained, and a plaingit fetch originrestored it. That glob is exactly the driver's fetch (skills/audit-git-checkouts/scripts/audit-checkouts.sh:801), so the "already gone by report time" claim is accurate.zsh is not installable in this sandbox, so the read-only
statusparameter claim could not be executed, but it matches zsh's documented special parameters and the quoted error text.Code review by Claude Code Opus (opus)
@ -58,6 +58,8 @@ Use `.repositories[].worktrees[].classification` as the initial grouping, then aFor every checkout, read both `defaultComparison` and `checkoutComparison`. The former always measures against the server's default branch; the latter measures against the checkout's intended line, prioritizing an explicit `.gitmodules` branch, then an exact tag, then the checked-out branch's configured upstream, then a matching `origin/<current-branch>`. `checkoutTargetResolution` also lists exact tags and every containing remote branch. Always report both views for non-default branches: whether the checkout is current with its own branch, and separately how far it is ahead of or behind the default branch. A detached reference pinned to a v1 branch can therefore be current or slightly behind v1 while intentionally divergent from a v2 default branch; report both facts and never call the default-branch divergence stale work.When the owner explicitly asks to move a detached checkout onto its default branch, gate the switch on two checks, not one: the working tree is clean, and the detached tip is reachable from some ref — `git for-each-ref --contains HEAD` prints at least one line. A clean tree says nothing about commits made on the detached HEAD; switching away leaves them reflog-only, and git's "leaving commits behind" advice prints only after HEAD has moved. If the tip is unreachable, report its unique commits to the owner instead of switching. Verify the remote-tracking ref exists with `git show-ref --verify --quiet refs/remotes/<remote>/<default>` (without `--quiet` the expected-negative case prints `fatal:` to stderr, inviting misreading as a repository failure): a narrow fetch refspec, such as a tag-only fetch, hides the branch locally even though the remote reports it — confirm the cause with `git config --get-all remote.<name>.fetch`. The switch cannot ride a one-shot fetch: `git switch <default>` maps the branch through the remote's configured refspec, not through refs on disk, so under the still-narrow refspec it dies with `fatal: invalid reference` even after the ref is fetched, `--track` refuses (`starting point '<remote>/<default>' is not a branch`), and a branch created bare, without an upstream, leaves the owner's own `git fetch` and `git pull` blind to the branch — only this skill's audit fetch, whose command-line glob refspec overrides the configured one, keeps `origin/<default>` fresh there. The owner asked for a checkout living on its default branch, so persist the minimal mapping: `git remote set-branches --add <remote> <default>` appends `+refs/heads/<default>:refs/remotes/<remote>/<default>` beside the narrow refspec it deliberately leaves alone; then `git fetch <remote>`, and `git switch <default>` creates the branch with its upstream set and future fetches covering it. Widen to all heads only when the owner wants that. To merely inspect the branch without adopting it, a one-shot `git fetch <remote> '+refs/heads/<default>:refs/remotes/<remote>/<default>'` recovers just that ref. Don't add `--prune` to an ad hoc glob fetch: prune scope follows the command-line refspec, deleting every ref under `refs/remotes/<remote>/` that the narrow refspec maintains elsewhere. The audit driver's own fetch is exactly that pruning glob, so on such a repository those refs are already gone by report time — a plain `git fetch <remote>` restores whatever the configured refspec maintains.ℹ️ Info: The rationale "git's "leaving commits behind" advice prints only after HEAD has moved" is slightly narrower than observed behavior. On git 2.39.5,
git switch --track origin/<default>under a narrow refspec printed the fullWarning: you are leaving 1 commit behindblock and then died withfatal: cannot set up tracking information, leaving HEAD still detached at the original tip. So the advice can also appear on a switch that never moves HEAD.The operational guidance is unaffected — the advice still arrives too late to serve as a pre-switch gate, which is exactly why the paragraph requires the
for-each-ref --contains HEADcheck first. Worth noting only so a reader doesn't treat the appearance of that warning as proof HEAD already moved.