Skip to content

.squad/ in .git/info/exclude silently hides new squad state files from git #1817

Closed as not planned

Description

@bradygaster

Problem

.git/info/exclude (line 9) contains:

.squad/

commented as "Squad consult mode (local only)".

This rule behaves asymmetrically, and the asymmetry is the hazard:

File state Behavior
Already tracked in git Shows as modified, commits normally ✅
New / untracked Invisible to git status -uall, silently never commits ❌

An agent can write a file, see no error, report success, and have that file quietly die with the worktree. There is no signal at any point in the workflow.

.git/info/exclude is per-clone and not version controlled, so this is also invisible in code review and won't reproduce for other contributors — making it harder to diagnose when it does bite.

Observed impact

During the agentic-workflows audit session (2026-08-21), Scribe wrote 8 files. 3 were already tracked and committed cleanly via #1816. The other 5 were silently dropped:

  • .squad/log/2026-08-21T09-30-07-07-00-agentic-workflows-audit.md
  • .squad/orchestration-log/2026-08-21T09-30-07-07-00-booster.md
  • .squad/orchestration-log/2026-08-21T09-30-07-07-00-flight.md
  • .squad/orchestration-log/2026-08-21T09-30-07-07-00-sims.md
  • .squad/agents/sims/history.md (newly created — Sims had no prior history file, so this agent now has no persisted memory at all)

The sims/history.md loss is the most consequential: it isn't a missing increment, it's a missing origin.

Why this looks like a behavior change, not long-standing policy

dev currently tracks files that this rule would now prevent from ever being added:

  • 4 files under .squad/log/
  • 11 files under .squad/orchestration-log/

So these paths were previously committed as a matter of course. The exclude rule appears to have been added later, and it silently froze those directories — existing entries keep updating, new ones can never appear.

Relevant context

  • .squad/ is tracked on dev by design and is stripped at release time by squad-promote.yml (git rm -rf --cached --ignore-unmatch .squad/), and squad-preview.yml fails if .squad/ is tracked on a preview branch. So committing .squad/ to dev does not leak into releases — the release path already handles it.
  • The Squad File Leakage CI check (scripts/check-squad-leakage.mjs) is informational only — its header states "Exit code: always 0 (informational only — does not block merge)". It would not have caught this.
  • Squad's own Scribe archival-safety rules explicitly warn about this exact failure shape: ".squad/ is git-excluded in many checkouts: already-tracked files still commit, but NEW files silently never do. Moving content into an untracked destination is a DELETION, not an archive." The rules anticipated the trap; the environment still contains it.

Options to discuss

  1. Remove the .squad/ line from .git/info/exclude. Restores the pre-existing behavior where session logs and orchestration logs commit normally. Simplest fix, but re-opens whatever concern motivated adding it.
  2. Narrow the rule to only the paths genuinely meant to stay local (e.g. .squad/memory/, scratch dirs) rather than the whole tree.
  3. Keep it and make the loss loud. Have Scribe verify with git check-ignore -v after each write and fail loudly when a file it just authored is unreachable to git — so silent loss becomes a visible error.
  4. Keep it and redirect — route session/orchestration logs to a path outside the exclude, or to the session artifacts dir, and accept that they are ephemeral by design.

Worth deciding once, in the open, rather than each session rediscovering it.

Acceptance criteria

  • Team decides which option (or combination) to adopt
  • Decision recorded in .squad/decisions.md
  • If the rule stays in any form, there is a loud failure mode — no silent drops
  • Consider regenerating .squad/agents/sims/history.md, which currently does not exist in any committed form

Raised by Flight during review of #1816, independently corroborated by the Coordinator during the same session.

Activity

  1. bradygaster commented on Aug 21, 2026

    @bradygaster
    CollaboratorAuthor

    Additional hazard surfaced during E2E readiness assessment (Sims): .git/info/exclude is per-clone and untracked.

    The consequence is worse than "new .squad/ files silently do not commit." Because this file lives in .git/ and is never version-controlled, each clone and each worktree host has its own copy. That means:

    • The same file can commit successfully from one machine and silently vanish from another.
    • Two operators can disagree about whether a doc "landed" and both be looking at correct local evidence.
    • Any fix has to be applied per-clone. Landing a fix in the repo cannot fix it for anyone else.

    This makes the failure mode a ratchet: git add -f <file> works, but only for the person who remembers, only that time. Every subsequent contributor silently reintroduces the bug.

    Two operational mitigations worth capturing regardless of how the underlying line is resolved:

    1. Any operator starting an E-series wave should run git check-ignore -v .squad/<newfile> before assuming a newly written doc is committable.
    2. Verify after committing, not just before — git cat-file -e <rev>:<path> is the only trustworthy confirmation the blob actually landed.

    Open question that should be answered before removing the line: why was .squad/ added to the exclude in the first place? If it was deliberate (e.g. keeping local agent history out of commits), blind removal trades this bug for a different one. dev still tracks 4 .squad/log/ and 11 .squad/orchestration-log/ files from before the rule existed, which suggests the exclusion is a later behavior change rather than a standing policy — but that is inference, not evidence.

  2. bradygaster commented on Aug 21, 2026

    @bradygaster
    CollaboratorAuthor

    1. Why the line exists — answered

    squad consult wrote it. It is a tool artifact, not policy. Confidence: high.

    packages/squad-sdk/src/sharing/consult.ts:463:

    storage.appendSync(gitExclude, '\n# Squad consult mode (local only)\n' + excludeLines.join('\n') + '\n');

    …where excludeLines is ['.squad/', '.github/agents/squad.agent.md'] (lines 455–461). That is byte-for-byte what is in the file, same comment, same order.

    Byte math closes it:

    bytes
    git's default info/exclude template 240
    the consult block 73
    live file 313

    The file contains the stock git template plus exactly that block and nothing else — no hand edits, ever. mtime 2026-06-24 10:43:26, ctime 2026-06-15 (clone time). Consult mode shipped 2026-03-01 (47a77aee), ~3.5 months before the write.

    Release pipeline: ruled out explicitly.

    • squad-npm-publish.yml — zero .squad references.
    • squad-promote.yml:52-57 — strips .squad/ (git rm -rf --cached) when merging dev→preview. So .squad/ tracked on dev never reaches a release.
    • squad-preview.yml:36-50 — the gate only guards the preview branch, and promote has already stripped by then. It is satisfied today with .squad/ tracked on dev.
    • .squad/decisions.md — no decision about .git/info/exclude. CONTRIBUTING.md, .gitattributes, .npmignore — nothing.
    • scripts/check-squad-leakage.mjs — confirmed exit 0 always, informational only.

    The release path was already correct without this line. It was never the reason.

    Mechanism — how a guarded function wrote it anyway. resolveGitExcludePath() (consult.ts:328) shells git rev-parse --git-path info/exclude, which from any worktree returns the shared common dir. Verified from this worktree:

    $ git rev-parse --git-path info/exclude
    C:/src/squad/.git/info/exclude
    

    But the "already squadified → refuse" guard (consult.ts:396) checks the worktree-local .squad/. Run squad consult in a worktree that lacks .squad/ and the guard passes while the write lands in the shared exclude — poisoning the main checkout and every sibling worktree in one shot. Confidence: high that consult wrote it (byte-exact); medium-high on the worktree route specifically (mechanism verified; the individual invocation isn't logged anywhere).

    2. Correction: 4 of the 5 lost files are not the exclude's doing

    I built three isolated repos — tracked-.gitignore-only, exclude-only, and both — and asked git which paths each hides:

    lost file .gitignore alone exclude alone
    .squad/log/…audit.md ignored — .squad/log/ ignored
    .squad/orchestration-log/…booster.md ignored — .squad/orchestration-log/ ignored
    .squad/orchestration-log/…flight.md ignored — .squad/orchestration-log/ ignored
    .squad/orchestration-log/…sims.md ignored — .squad/orchestration-log/ ignored
    .squad/agents/sims/history.md not ignored ignored

    .gitignore:13-18 has ignored .squad/log/ and .squad/orchestration-log/ since 2026-02-21 (c36e8ecc) — tracked, reviewed, repo-wide, and older than every one of the 15 tracked files in those directories (earliest add 2026-02-22, f4f86683). Those files were committed with -f against a standing rule.

    So the "the exclude was added later and silently froze those directories" hypothesis is refuted. The directories were never frozen — adds continued through 2026-08-02 (065d3021), six weeks after this machine was poisoned. That late add is itself independent proof of the per-clone split you described: a clean clone still committed those paths while this one could not.

    Net: removing the exclude line would have saved exactly one of the five files — sims/history.md. The other four are working as designed by policy that predates the exclude by four months.

    3. Live hazard, needs flagging now

    .squad/e2e/ is not in .gitignore but is hidden by the exclude. The in-flight .squad/e2e/ work is squarely in the blast radius — the git add -f + post-commit git cat-file -e protocol there is necessary, not belt-and-braces.

    4. Recommendation

    The per-clone constraint is the whole problem: anything landed in the repo cannot repair an already-poisoned clone. So the repo-side answer has to be detection, and the local edit is a separate, manual act.

    option verdict
    A. Remove the line locally Per-machine workaround, not a fix. Fixes 1 of 5 losses on 1 machine.
    B. .gitignore with scoped negation Reject. .gitignore can't express it — .squad/ must stay tracked on dev, release is already handled by promote-time stripping, and ignore rules don't affect tracked files anyway. Trades a per-clone hazard for a repo-wide one.
    C. CI gate Reject as primary. CI runs on fresh clones that never carry the poisoned exclude, so CI structurally cannot observe this failure. Decoration by the decisions.md:604 bar.
    D. squad doctor check Recommend.
    E. Root-cause guard in consult.ts Recommend, paired with D.

    D — squad doctor check. Run git check-ignore -v against the state paths squad intends to write, and discriminate on the matching source: a .gitignore match is expected policy → info; a $GIT_DIR/info/exclude match on .squad/ is the anomaly → fail, with the exact repair command. This clears decisions.md:604 because it compares two genuinely independent sources — squad's own intent (its state-path model) against git's real enforcement engine (check-ignore) — rather than reading the file it is guarding. Slots into the existing DoctorCheck shape in packages/squad-cli/src/cli/commands/doctor.ts.
    Cost: doctor is opt-in; it only helps whoever runs it. Worth wiring into the operator prelude / session-init so it fires unprompted.

    E — guard in consult.ts. Refuse to add .squad/ to the exclude when .squad/ is already tracked (git ls-files --error-unmatch .squad/), and resolve worktree-vs-common-dir correctly. Stops all future poisoning; does nothing for already-poisoned clones — which is exactly why it pairs with D.

    5. The larger bug underneath this one

    Four of the five losses were .squad/log/ and .squad/orchestration-log/ writes into directories that have been gitignored repo-wide since February. "Scribe writes session logs to .squad/log/" is a workflow that has never worked in a clean clone. Either those paths should be un-ignored or Scribe should stop writing there and the logs should go somewhere real. That is the bigger of the two problems and it has nothing to do with .git/info/exclude. Flagging for a team decision — not filing anything.

    6. Local remediation (per-machine, explicitly not a fix)

    Delete line 9 (.squad/) from C:\src\squad\.git\info\exclude, keeping .github/agents/squad.agent.md:

    # affects THIS clone + all its worktrees only
    $p = 'C:\src\squad\.git\info\exclude'
    Copy-Item $p "$p.bak"
    (Get-Content $p) | Where-Object { $_ -ne '.squad/' } | Set-Content $p
    git -C C:\src\squad check-ignore -v .squad/agents/sims/history.md; # expect: no output

    What it might break: nothing in the release path (promote strips .squad/, preview gate is downstream of that). The real risk is the reverse — .squad/ becomes visible to git status again, so any git add .-style staging can now sweep in local squad state. Given the standing "never git add ." rule that is acceptable, but it is a genuine behavior change on this machine.

    Do not run it while the .squad/e2e/ session is in flight — it changes ignore semantics underneath that work.

  3. bradygaster commented on Aug 23, 2026

    @bradygaster
    CollaboratorAuthor

    Closing as a duplicate of #1826 — not because the problem is solved, but because the investigation that started here landed the root cause there, and two issues on one defect will drift.

    Your analysis in this thread is what identified the writer:

    packages/squad-sdk/src/sharing/consult.ts:463 … byte math closes it: git's default template 240 B + consult block 73 B = the live file.

    #1826 carries that forward with the piece this issue could not have: the scope defect. resolveGitExcludePath() shells git rev-parse --git-path info/exclude, which from a worktree resolves to the shared common dir, while the "already squadified → refuse" guard checks the worktree-local .squad/. So squad consult run in a worktree passes the guard and poisons the exclude for the main checkout and every sibling worktree. #1826 also has a live re-poisoning observation from 2026-08-22, after the lines had been manually removed the same day — which upgrades this from a historical artifact to an active bug.

    Where the rest of this thread's content now lives:

    Concern from this issue Tracked in
    The squad consult writer + worktree scope defect #1826
    Should .squad/ paths be ignored at all — the policy audit #1830
    Squad state that silently evaporates because it is ignored (Scribe logs) #1823
    The data loss this actually caused #1836

    Reopen if there is a per-clone repair story that #1826 does not cover — noting your own point that .git/info/exclude is untracked, so no repo-side change can fix an already-poisoned clone.

    Triage: 2026-08-23.

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

    close:duplicateCandidate for closing as duplicate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions