Skip to content

squad externalize: project-key collision silently overwrites the external state of another project #1483

Closed

Description

@yeshurin

Summary

squad externalize names each project's external-state directory using only the basename of the project path (deriveProjectKey). Two different projects whose folders share a name therefore resolve to the same external directory, and externalize moves state in with no ownership/occupancy check — so the second project silently overwrites the first project's externalized state. There is no warning, no error, and no record of which project owns the directory, so the loss is invisible and unrecoverable after the fact.

Steps to reproduce

  1. ~/work/api → squad init (build up some team state) → squad externalize.
  2. ~/clients/acme/api (a different project, same folder name api) → squad init → squad externalize.
  3. The second externalize writes into the same …/squad/projects/api/ and overwrites the first project's decisions.md, agent histories, and memory. No warning is shown.

Root cause (v0.11.0 / dev)

  • packages/squad-sdk/src/resolution.ts — deriveProjectKey() uses only path.basename(projectDir) (lowercased + sanitized), with an unknown-project fallback. No parent path, no uniqueness.
  • packages/squad-sdk/src/resolution.ts — resolveExternalStateDir(key) builds {globalDir}/projects/{key} and validates only emptiness + ..; no ownership/collision guard.
  • packages/squad-cli/src/cli/commands/externalize.ts (runExternalize) — derives the key and moves state into the resolved directory with only the KEEP_LOCAL filter; no occupancy/ownership check before overwriting.

Impact

  • Silent, unrecoverable loss of a project's externalized team state whenever two project folders share a name (common: multiple repos named api, web, service, …).
  • Wider than exact matches: names are lowercased and non-[a-z0-9._-] characters are collapsed, so different names can merge; and any folder name made entirely of non-Latin characters (e.g. Chinese / Arabic / emoji) derives the single unknown-project key — so all such projects share one bucket and overwrite each other.
  • Today's only mitigation is remembering to pass the optional --key.

Proposed fix

The collision is created at a single site (runExternalize), so a guard there is sufficient; resolution.ts — and therefore every existing external directory — can stay byte-for-byte unchanged.

I've prepared a fix (draft PR to follow) that adds a small .owner.json ownership marker recording the owning project's path, and refuses when a different project targets an already-owned directory (pointing to the existing --key option). Pre-existing external dirs are adopted in place via the project's own config.json, so nothing is re-keyed or lost, and it catches every collision family (including the unknown-project bucket). If you'd rather not introduce a new on-disk file, a simpler variant that just refuses when the target dir is already non-empty and unproven is equally easy — happy to go that route instead.

Either way the behavior change is confined to the previously-broken path (silent overwrite → a clear refusal with a --key remedy), it is backward compatible, and it is a @bradygaster/squad-cli patch only (SDK untouched).


Reported with the help of Elad, a human-supervised autonomous coding assistant.

Activity

  1. added a commit that references this issue on Oct 24, 2026
    56173f1
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions