Skip to content

fix(claude): squad_state starts from a repository subfolder - #2490

Merged
bradygaster merged 5 commits into
devfrom
squad/mcp-launcher-subfolder
Nov 13, 2026
Merged

bradygaster merged 5 commits into
devfrom
squad/mcp-launcher-subfolder

Conversation

@bradygaster

Copy link
Copy Markdown
Collaborator

Found in the live scenario pass for #2487 (results). This is a regression from #2470, which hasn't been released.

Problem

Started in a monorepo package (packages/api), Claude Code finds the root .mcp.json but starts the MCP server in packages/api. node .squad/squad.mjs state-mcp then fails with "Cannot find module", so the coordinator has no squad_state. I checked the obvious alternatives in real runs:

  • ${CLAUDE_PROJECT_DIR} is not expanded in .mcp.json.
  • Inside the server, CLAUDE_PROJECT_DIR is the subfolder anyway.

Fix

The committed command is now node -e <bootstrap> .squad/squad.mjs state-mcp. The bootstrap:

  • looks for the launcher in the working directory, then in each parent folder;
  • stops at the repository root (the first folder with .git), so it never runs a launcher from outside the repository;
  • runs the launcher it finds with the remaining arguments unchanged;
  • uses only single quotes, so Windows argument quoting never has to escape it.

Other changes:

  • The new helpers live in repo-launcher.ts (SQUAD_REPO_LAUNCHER_BOOTSTRAP, squadRepoLauncherArgs, squadRepoLauncherScript).
  • Hooks, squad doctor and squad upgrade read the launcher path from both the old and new forms.
  • The Copilot snapshot changes in .mcp.json only, since Copilot shares that file.

Project hooks are a separate issue. From a subfolder, Claude Code doesn't load the root .claude/settings.json at all. That's Claude Code's own behavior, and this PR doesn't change it.

Tests

  • New test in test/portable-launch.test.ts: the committed command runs the launcher from the root and from packages/api, with arguments unchanged. A nested repository never reaches the launcher above it.
  • The plain script form still maps to the same hook command.
  • Live check: claude -p started in packages/api of a fresh squad init called squad_state_list successfully.
  • On macOS, 26 failures in 8 test files happen identically on dev (symlinked /tmp paths and other local environment issues). They aren't caused by this change.

🤖 Generated with Claude Code

Claude Code finds the root .mcp.json from a subfolder (a monorepo package)
but starts MCP servers in that subfolder, and ${CLAUDE_PROJECT_DIR} is not
expanded in .mcp.json, so `node .squad/squad.mjs state-mcp` failed with
"Cannot find module" (found in the #2487 scenario pass; regression from
#2470). The committed command is now `node -e <bootstrap> .squad/squad.mjs
state-mcp`: the bootstrap walks up from the working directory, stops at the
repository root, and runs the launcher with the remaining arguments.

Hooks, doctor and upgrade read the launcher path from both forms. The
Copilot snapshot changes only in .mcp.json, which Copilot shares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI dynamic review requested due to automatic review settings November 13, 2026 06:24
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #2490

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 11
Files added 2
Files modified 9
Files deleted 0
Modules touched 4

🎯 Risk Factors

  • 11 files changed (6-20 → MEDIUM)
  • 4 modules touched (2-4 → MEDIUM)

📦 Modules Affected

root (2 files)
  • .changeset/mcp-launcher-subfolder.md
  • CHANGELOG.md
squad-cli (3 files)
  • packages/squad-cli/src/cli/commands/doctor.ts
  • packages/squad-cli/src/cli/core/mcp-spec.ts
  • packages/squad-cli/src/cli/core/upgrade.ts
squad-sdk (3 files)
  • packages/squad-sdk/src/client/adapters/claude-hooks.ts
  • packages/squad-sdk/src/client/adapters/claude-plugin.ts
  • packages/squad-sdk/src/client/adapters/repo-launcher.ts
tests (3 files)
  • test/__snapshots__/host-projection-regression-guards.test.ts.snap
  • test/cli/upgrade-portability.test.ts
  • test/portable-launch.test.ts

This report is generated automatically for every PR. See #733 for details.

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

Authoritative evidence: scanners from the PR base commit, posted by the default-branch workflow_run reporter.
Results for head commit 41a43963d2e923a1fc112a3e396a02b80be1c9b5.

⚠️ Architectural review: 1 warning(s).

Severity Category Finding Files
🟡 warning bootstrap-area 2 file(s) in the bootstrap area (packages/squad-cli/src/cli/core/) were modified. These files must maintain zero external dependencies. Review carefully. packages/squad-cli/src/cli/core/mcp-spec.ts, packages/squad-cli/src/cli/core/upgrade.ts

Automated architectural review — informational only.

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 41a4396

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 3 item(s) to address before review

Status Check Details
❌ Single commit 5 commits — consider squashing before review
✅ Not in draft Ready for review
❌ Branch up to date dev is 1 commit(s) ahead — rebase recommended
❌ Copilot review No Copilot review yet — it may still be processing
✅ Changeset present Changeset file found
✅ Scope clean No .squad/ or docs/proposals/ files
✅ No merge conflicts Merge status unknown — GitHub is still computing
✅ Copilot threads resolved 0 active Copilot thread(s) resolved (1 outdated skipped)
✅ CI passing All checks passing

Files Changed (11 files, +128 −16)

File +/−
.changeset/mcp-launcher-subfolder.md +6 −0
CHANGELOG.md +1 −0
packages/squad-cli/src/cli/commands/doctor.ts +2 −1
packages/squad-cli/src/cli/core/mcp-spec.ts +5 −3
packages/squad-cli/src/cli/core/upgrade.ts +2 −1
packages/squad-sdk/src/client/adapters/claude-hooks.ts +3 −2
packages/squad-sdk/src/client/adapters/claude-plugin.ts +1 −5
packages/squad-sdk/src/client/adapters/repo-launcher.ts +44 −0
test/__snapshots__/host-projection-regression-guards.test.ts.snap +1 −1
test/cli/upgrade-portability.test.ts +2 −1
test/portable-launch.test.ts +61 −2

Total: +128 −16


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The launcher search can select an attacker-controlled module from a nested or ancestor directory, enabling code execution with the host user’s permissions when squad_state starts.

1 open issue
What changed in this PR

Fixes an unreleased regression that prevents squad_state from starting when Claude Code opens a repository subfolder, supporting the monorepo scenarios identified in #2487.

Changes:

  • Adds a portable bootstrap that searches parent directories for the repository launcher, stopping at .git.
  • Updates hooks, doctor checks, and upgrade descriptions to recognize both launcher formats.
  • Adds subfolder and repository-boundary tests, updates the shared MCP snapshot, and records the patch release notes.
File Description
test/​portable-launch.test.ts Tests subfolder launches, argument preservation, boundaries, and compatibility.
test/​cli/​upgrade-portability.test.ts Updates expected installed-CLI launch arguments.
test/​__snapshots__/​host-projection-regression-guards.test.ts.snap Updates only the shared .mcp.json hash.
packages/​squad-sdk/​src/​client/​adapters/​repo-launcher.ts Adds bootstrap and launcher argument helpers.
packages/​squad-sdk/​src/​client/​adapters/​claude-plugin.ts Re-exports the shared launcher helpers.
packages/​squad-sdk/​src/​client/​adapters/​claude-hooks.ts Recognizes both launcher formats for hooks.
packages/​squad-cli/​src/​cli/​core/​upgrade.ts Displays the launcher path from either format.
packages/​squad-cli/​src/​cli/​core/​mcp-spec.ts Generates bootstrap-based committed launch commands.
packages/​squad-cli/​src/​cli/​commands/​doctor.ts Checks launcher existence with the new format.
CHANGELOG.md Documents the subfolder fix and upgrade guidance.
.changeset/​mcp-launcher-subfolder.md Records patch releases for the SDK and CLI.

Review effort: Balanced (dynamic)

@bradygaster

Copy link
Copy Markdown
Collaborator Author

Review: changes needed

Reviewed head 1cf4e6aae4ee92909fc41c8e265c29cfd90bdad2 against dev base f229547fbcf4ca60c4524c5a69ef26d1924b7023.

P2: Launcher discovery escapes the intended project when Git metadata is absent

Location: packages/squad-sdk/src/client/adapters/repo-launcher.ts:23–25.

The project's .mcp.json does not bound the upward search. If the project launcher is missing and .git is absent, such as in an exported checkout, the bootstrap can execute an unrelated ancestor's launcher instead of failing.

Executable reproduction:

parent/.squad/squad.mjs                 # prints PARENT LAUNCHER
parent/exported-project/.mcp.json       # generated bootstrap configuration
parent/exported-project/packages/api/   # server cwd

With no .git or launcher in exported-project, the base/plain argv exits 1 with a missing-module error. This PR's bootstrap exits 0 and prints PARENT LAUNCHER. Adding a .git file at the project boundary correctly prevents escape.

This establishes unintended executable selection and a confinement regression, not a demonstrated privilege-escalation exploit. Child-folder launcher shadowing was also checked: both base and head exhibit it, so that concern alone is not newly introduced here.

Required correction: establish the intended configuration/project boundary and fail closed when its launcher is missing, including without Git metadata. Add an executable regression test. Suggested correction owner: plugin-platform-dev, with CLI compatibility rereview by cli-dev.

Validation

SDK and CLI builds and repository lint passed. Nine relevant Vitest suites passed: 297 tests passed, 3 Windows-only tests skipped. Exact-base comparison suites passed 170 tests. Executable probes covered root/subfolder startup, spaces, .git files, argv and exit-code preservation, SIGTERM forwarding, import/spawn failures, and the actual built CLI's squad_state_health from a subfolder. Upgrade normalization, legacy hooks, custom-launcher protection, and plugin generation checks passed. Native Windows execution remains unverified.

Completed CI was green at review time. Recommendation: bound launcher discovery before merging; ordinary subfolder startup is working.

bradygaster and others added 2 commits November 12, 2026 23:59
Copilot review: the first-match search let a package's own .squad/squad.mjs
shadow the root launcher, and without .git it could search above the
checkout. The bootstrap now finds the repository root first and runs only
<root>/<squad dir>/squad.mjs; with no .git it uses the working directory
alone, and it fails closed on a missing launcher or an escaping path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bradygaster
bradygaster enabled auto-merge (squash) November 13, 2026 08:13
@bradygaster
bradygaster merged commit 1f20ec1 into dev Nov 13, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants