Skip to content

squad consult writes .squad/ to the shared info/exclude, silently hiding state in main and all sibling worktrees #1826

Closed

Description

@bradygaster

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) shells git 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

  1. consult.ts guard — 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.
  2. squad doctor check — run git check-ignore -v on a representative .squad/ path and discriminate on match source: .gitignore is expected (report as info), $GIT_DIR/info/exclude on .squad/ is an anomaly (fail, with a repair command). This clears the .squad/decisions.md:604 bar 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

  1. bradygaster commented on Aug 23, 2026

    @bradygaster
    CollaboratorAuthor

    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.md at 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 running squad doctor and a build — no consult invoked 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/exclude resolves 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 via git 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 reach decisions.md on their own:

    $ git check-ignore -v .squad/decisions/inbox/eecom-1793-crlf-working-tree-repair.md
    C:/src/squad/.git/info/exclude:10:.squad/
    

    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 clean git status is not evidence the work was captured.

    Related, and probably the right framing: #1830 documents that squad-promote.yml has stripped all of .squad/ since d4058715 (2026-02-20) — one day before the first .gitignore entry. 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:

    1. Do not write to the shared info/exclude. If per-worktree scoping is genuinely needed, $GIT_DIR/info/exclude for a linked worktree is not it — that resolves to the common dir too.
    2. Prefer a mechanism that cannot outlive the consult session, or that is scoped to the invoking worktree only.
    3. Whatever replaces it, add a check that fails when .squad/ is unstageable — the current failure mode is invisible, which is why this survived this long.
  2. added
    bugSomething isn't working
    gh-awGitHub Agentic Workflows integration
    on Aug 23, 2026
  3. bradygaster commented on Aug 24, 2026

    @bradygaster
    CollaboratorAuthor

    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.ts built a fabricated .git/ directory as its fixture:

    mkdirSync(join(PROJECT_ROOT, '.git', 'info'), { recursive: true });

    That passes the "is there a .git here" check but is not a valid repository, so git rev-parse walked out of the fixture and answered for the enclosing squad checkout. Measured on a clean clone:

    BEFORE: 240        # pristine git template
    npx vitest run test/sdk/consult.test.ts -t "setupConsultMode"
    AFTER:  313        # +73 B consult block
    

    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 test has 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/exclude is never read. Verified by planting one and running git 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 hermetic git init fixture, and three regression tests. Exclude size now holds steady at 241 B across all five consult-adjacent suites (122 tests).

    Suggestion (2) — the squad doctor check — 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.

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

    bugSomething isn't workinggh-awGitHub Agentic Workflows integrationpriority:p1This sprinttriagedIssue has been triagedwave:1-nextWave 1 — immediate next sprint

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions