Repository navigation
Description
Problem
squad consult appends .squad/ and .github/agents/squad.agent.md to git's info/exclude. Because of how the exclude path is resolved, running it from a worktree writes to the shared common dir, silently hiding .squad/ in the main checkout and every sibling worktree.
This is the root cause established in #1817.
Evidence
packages/squad-sdk/src/sharing/consult.ts:463:
storage.appendSync(gitExclude, '\n# Squad consult mode (local only)\n' + excludeLines.join('\n') + '\n');with excludeLines built at L455-461 as ['.squad/', '.github/agents/squad.agent.md'].
Byte math confirms it is the sole writer: git's default info/exclude template is 240 B; the consult block is 73 B; the live poisoned file is exactly 313 B with no hand edits.
The defect
Two functions disagree about scope:
resolveGitExcludePath()(consult.ts:328) shellsgit rev-parse --git-path info/exclude, which from any worktree returns the shared common dir (verified).- The "already squadified → refuse" guard (
consult.ts:396) checks the worktree-local.squad/.
So running squad consult in a worktree that lacks .squad/ passes the guard while poisoning the shared exclude for every checkout.
This is a defect on the code reading alone, independent of whether that route is what happened in any particular case.
Why CI cannot catch this
CI runs on fresh clones, which structurally never carry the poison. A CI check here is blind by construction. Confirmed during investigation; also confirmed the release pipeline is not involved (squad-promote.yml already strips .squad/ before preview; squad-npm-publish.yml has zero .squad references).
Impact
Silent loss of new .squad/ state files on a poisoned clone. Confirmed live: .squad/e2e/ is not in .gitignore but is hidden by the exclude, which is why the git add -f + git cat-file -e protocol was necessary for PRs #1819-#1821.
Because info/exclude is per-clone and untracked, no repo-side change can repair an already-poisoned clone — and the same file can commit cleanly from one machine and vanish from another.
Suggested direction
consult.tsguard — make the guard's scope match the write's scope. Either refuse when the resolved exclude path is the shared common dir while the.squad/check was worktree-local, or write to a worktree-local exclude.squad doctorcheck — rungit check-ignore -von a representative.squad/path and discriminate on match source:.gitignoreis expected (report as info),$GIT_DIR/info/excludeon.squad/is an anomaly (fail, with a repair command). This clears the.squad/decisions.md:604bar because it compares Squad's intent against git's actual enforcement engine rather than re-reading a file Squad wrote.
(1) stops new poisonings; (2) detects and repairs existing ones. Both are needed — (1) cannot help the clones already affected.
Confidence
High that squad consult wrote the line (byte-exact match, sole writer of that string). Medium that the worktree route is what occurred in the observed case — supported by elimination, and now unprovable: no surviving worktree predates the write, and no invocation record exists. Recorded as "we know the writer with high confidence; the route is unproven and now unprovable." The recommendation does not depend on the unproven half.
Owners
EECOM (consult.ts) with CAPCOM or FIDO (doctor check).
Activity
bradygaster commentedon Aug 23, 2026
Live reproduction — this re-poisons during ordinary usage, not just once historically
Caught this happening in real time today. Stronger evidence than a static code read, and it changes the severity.
Observed:
C:\src\squad\.git\info\exclude— mtime 2026-08-22 17:20:25, restored to 321 B with.squad/at line 10 and.github/agents/squad.agent.mdat line 11.I had manually removed those three lines earlier the same day (backup kept at
exclude.bak-20260822). They came back on their own. The only active worker in that window was a sub-session runningsquad doctorand a build — noconsultinvoked by hand, no manual edit.That matters because it reframes the bug: this is not a one-time historical mistake that can be cleaned up once. Ordinary CLI usage re-applies it. Any fix that only removes the lines is temporary; the write path itself has to change.
Blast radius, confirmed:
git rev-parse --git-path info/excluderesolves to the shared common dir from every worktree. One write poisons the main checkout and every sibling worktree at once. Verified from three separate worktrees today.Code path:
packages/squad-sdk/src/sharing/consult.ts— line ~330 resolves the path viagit rev-parse --git-path info/exclude, line ~447 performs the write.Real consequence, hit twice today. Agent-authored decision records under
.squad/decisions/inbox/are git-invisible, so they are silently unstageable and never reachdecisions.mdon their own:Two records — EECOM's #1793 decision (4,997 B) and FIDO's #1831 review (3,559 B) — were written into a sub-session worktree, were invisible to
git status, and would have been destroyed with that worktree. I recovered both by hand only because I knew to look. A cleangit statusis not evidence the work was captured.Related, and probably the right framing: #1830 documents that
squad-promote.ymlhas stripped all of.squad/sinced4058715(2026-02-20) — one day before the first.gitignoreentry. So the exclusion is redundant with the promote-time strip, which suggests the write may be unnecessary rather than merely mis-scoped. Worth confirming before designing a fix: if promote already strips it, why does consult need to exclude it locally at all?Suggested direction, not a mandate:
info/exclude. If per-worktree scoping is genuinely needed,$GIT_DIR/info/excludefor a linked worktree is not it — that resolves to the common dir too..squad/is unstageable — the current failure mode is invisible, which is why this survived this long.chore(squad): gh-aw wave-1 session records + repair 3 lost history ar…
bradygaster commentedon Aug 24, 2026
Two corrections from implementing this in #1850, both of which change the issue's conclusions.
1. The route is not unprovable — and it was not (only) the worktree.
This issue records "we know the writer with high confidence; the route is unproven and now unprovable." A second route was live in the repo and reproduces on demand: the SDK consult test suite itself.
test/sdk/consult.test.tsbuilt a fabricated.git/directory as its fixture:That passes the "is there a
.githere" check but is not a valid repository, sogit rev-parsewalked out of the fixture and answered for the enclosing squad checkout. Measured on a clean clone:240 + 73 = 313 — byte-identical to the forensic math in this issue. Your byte analysis was exactly right; it just had a second suspect. Anyone who has run
npm testhas a poisoned clone, and CI never showed it because CI clones are discarded — as you predicted.2. Suggested direction (1b) is not available.
The issue offers "refuse ... or write to a worktree-local exclude." Git keeps no per-worktree
info/exclude; a file placed at.git/worktrees/<id>/info/excludeis never read. Verified by planting one and runninggit check-ignore— no match.Taking that branch would have swapped a loud bug for a quiet one: consult mode silently failing to hide
.squad/, leaving it exposed to accidental commits. So refusing is the only safe behaviour.What #1850 does: a single containment check (
isExcludeOwnedBy) that covers normal repo / linked worktree / nested-in-ancestor, refusal before any mutation, a hermeticgit initfixture, and three regression tests. Exclude size now holds steady at 241 B across all five consult-adjacent suites (122 tests).Suggestion (2) — the
squad doctorcheck — is deliberately not in that PR and is filed separately. As you noted, both halves are needed: #1850 stops new poisonings but cannot repair the clones already affected.fix(sdk): refuse consult when info/exclude resolves outside the proje…