Repository navigation
fix(claude): squad_state starts from a repository subfolder - #2490
Conversation
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>
🟡 Impact Analysis — PR #2490Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (2 files)
squad-cli (3 files)
squad-sdk (3 files)
tests (3 files)
This report is generated automatically for every PR. See #733 for details. |
🏗️ Architectural Review
Automated architectural review — informational only. |
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| 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.
There was a problem hiding this comment.
🟡 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)
Review: changes neededReviewed head P2: Launcher discovery escapes the intended project when Git metadata is absentLocation: The project's Executable reproduction: With no 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: ValidationSDK 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, Completed CI was green at review time. Recommendation: bound launcher discovery before merging; ordinary subfolder startup is working. |
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>

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.jsonbut starts the MCP server inpackages/api.node .squad/squad.mjs state-mcpthen fails with "Cannot find module", so the coordinator has nosquad_state. I checked the obvious alternatives in real runs:${CLAUDE_PROJECT_DIR}is not expanded in.mcp.json.CLAUDE_PROJECT_DIRis the subfolder anyway.Fix
The committed command is now
node -e <bootstrap> .squad/squad.mjs state-mcp. The bootstrap:.git), so it never runs a launcher from outside the repository;Other changes:
repo-launcher.ts(SQUAD_REPO_LAUNCHER_BOOTSTRAP,squadRepoLauncherArgs,squadRepoLauncherScript).squad doctorandsquad upgraderead the launcher path from both the old and new forms..mcp.jsononly, since Copilot shares that file.Project hooks are a separate issue. From a subfolder, Claude Code doesn't load the root
.claude/settings.jsonat all. That's Claude Code's own behavior, and this PR doesn't change it.Tests
test/portable-launch.test.ts: the committed command runs the launcher from the root and frompackages/api, with arguments unchanged. A nested repository never reaches the launcher above it.claude -pstarted inpackages/apiof a freshsquad initcalledsquad_state_listsuccessfully.dev(symlinked/tmppaths and other local environment issues). They aren't caused by this change.🤖 Generated with Claude Code