Skip to content

fix: sandbox agent bash and restrict ca --agent (VULN-1413) - #28

Open
mitch-fultz wants to merge 2 commits into
mainfrom
cursor/sandbox-agent-bash-6740
Open

mitch-fultz wants to merge 2 commits into
mainfrom
cursor/sandbox-agent-bash-6740

Conversation

@mitch-fultz

Copy link
Copy Markdown
Contributor

Fixes VULN-1413. Agent roles ran Pi's bash with the host's credentials, environment tokens and network. A prompt injection in issue text, repo content or PR comments could therefore reach RCE and credential theft.

What changed

  • Sandboxed bash for every role (scout, implementer, verifier, reviewer, closer, retrospective, interviewer). It uses @anthropic-ai/sandbox-runtime (bubblewrap on Linux, sandbox-exec on macOS) through Pi's bash operations hook. No Pi upgrade was needed.
    • Credential files are hidden: ~/.ssh, ~/.gnupg, ~/.aws, ~/.azure, ~/.kube, ~/.docker, ~/.config/{gh,git,gcloud}, ~/.git-credentials, ~/.gitconfig, ~/.netrc, ~/.npmrc, ~/.yarnrc.yml, ~/.pypirc, ~/.pi, ~/.claude, keyrings, shell rc files.
    • The child environment is an allowlist (PATH, HOME, locale, CASE_*, etc.), so tokens and API keys don't reach it. Git gets an empty global config, and only the commit name and email are passed through.
    • Writes are limited to the repo, /tmp, and the repo's git dirs. Git hooks and config stay read-only, as do Pi's .pi/ and agent dirs.
    • Network egress is allowlisted (see below).
    • The sandbox fails closed: no sandbox dependencies means no agent bash. On Linux this includes seccomp; the compiled binary now ships the seccomp helper.
  • Read/write/edit tools get the same path rules. These tools run outside the sandbox, so without this they could still read ~/.ssh or /proc/*/environ, or plant .git/hooks.
  • Credentialed steps are now fixed host-side tools. The model only supplies text or a validated file path, never a command line.
    • create_pull_request pushes the current branch and runs gh pr create. It refuses main/master.
    • comment_on_pull_request runs gh pr review --comment.
    • upload_evidence runs ca upload on a validated copy of the file.
    • The closer and verifier prompts are updated to use these tools.
  • Host-side git push and git checkout no longer run repo hooks. Agents can write hook paths like husky's .husky/.
  • ca --agent is read-only. The session now gets a guarded read plus run_pipeline, fetch_issue, create_task and run_baseline, with no bash, edit or write. The interviewer session also lost Pi's built-in edit/write, which it was getting by accident.
  • ca upload now writes its webm→mp4 output next to the input instead of at a predictable /tmp path.

Please confirm (guessed)

  • Network allowlist: registry.npmjs.org, api.workos.com, *.authkit.app, plus localhost binding. GitHub is deliberately excluded. If verification of some repo needs more domains, add them to ALLOWED_DOMAINS in src/agent/sandbox.ts.
  • Verifier credentials: the test-account file from Task Context (e.g. ~/.config/case/credentials) stays readable inside the sandbox, as before. These are test-app credentials, not host credentials.

Testing

  • New test/sandbox/ suite. It runs as its own process, unmocked, as a third step of bun run test. It runs real sandboxed bash for every role and checks that:

    • planted secret files and secret env vars never show up, including via /proc/*/environ;
    • writes to $HOME and .git/hooks fail, while repo and /tmp writes succeed;
    • non-allowlisted network access is blocked;
    • git commit works;
    • the read and write tools refuse credential paths.

    It also checks, with a real Pi AgentSession, that ca --agent has no bash, edit or write.

  • Existing tests: the unit step has the same 46 failures as on main and none new. Those failures are mock leakage between test files and gitignored asset files. The standalone and sandbox steps pass. typecheck is clean. Lint and format issues are unchanged from main.

  • Manual checks:

    • The privileged tools were run against a local bare remote with stub gh and ca. The push landed, the husky pre-push hook did not run, shell syntax in the PR body stayed inert text, and symlink, non-media and fake-webm (HLS playlist) uploads were rejected.
    • With the sandbox active, registry.npmjs.org was reachable and github.com was blocked.
    • Unix-socket creation is blocked, including inside a compiled binary.
    • dist/ca builds, and ca --agent from source starts.

Known limits / follow-ups

  • Not verified on macOS. Also not verified: a live model run, the playwright UI flow, and a real GitHub push/PR or upload.
  • On Linux, each bash call is its own sandbox, so background servers (pnpm dev &) don't survive between calls. The UI verification flow likely needs a single-call approach there.
  • Global package caches outside the repo and /tmp are read-only. Installs that need to write to them (e.g. adding a new dependency) can fail. ca snapshot in the retrospective fails for the same reason.
  • Commits made inside the sandbox are unsigned.
  • run_baseline (and ca bootstrap) still run the repo's install and test commands host-side, unsandboxed. Repo-shipped .pi/ config is still trusted by Pi in ca --agent and ca onboard --interview.
  • Allowlisted domains could still be used to send out data the sandbox can read (repo contents), but not credentials.
  • The compiled dist/ca --agent already crashed on main (missing Pi theme asset). That bug is unrelated to this change.
Open in Web Open in Cursor 

cursoragent and others added 2 commits November 10, 2026 15:48
…ols (VULN-1413)

Pipeline and interviewer agents ran Pi's bash with the host's credentials,
env tokens, and network, so a prompt injection could reach RCE and
credential theft.

- Every role's bash now runs through @anthropic-ai/sandbox-runtime
  (bubblewrap on Linux, sandbox-exec on macOS): credential paths hidden,
  env reduced to an allowlist, writes limited to the repo, /tmp and the
  repo's git dirs (hooks/config read-only), egress limited to
  registry.npmjs.org, api.workos.com and *.authkit.app. The sandbox fails
  closed when its dependencies (or Linux seccomp) are missing.
- Read/write/edit tools get the same path rules host-side.
- The closer's push + `gh pr create` + review comment and the verifier's
  `ca upload` become fixed host-side tools (create_pull_request,
  comment_on_pull_request, upload_evidence); prompts updated.
- Host-side git push/checkout no longer run repo hooks, which agents can
  write (e.g. husky's .husky/).
- The interviewer session no longer gets Pi's built-in edit/write tools.

Co-authored-by: mitch-fultz <mitch-fultz@users.noreply.github.com>
`ca --agent` started its Pi session without a tool allowlist, so Pi's
default bash/edit/write were active alongside Case's tools. The session
now gets only a path-guarded read plus run_pipeline, fetch_issue,
create_task and run_baseline. Tests cover the allowlist with mocked Pi
and with a real AgentSession.

Co-authored-by: mitch-fultz <mitch-fultz@users.noreply.github.com>
@linear-code

linear-code Bot commented Nov 10, 2026

Copy link
Copy Markdown

VULN-1413

@greptile-apps

greptile-apps Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 2/5

[High impact] The PR changes command and file boundaries across the agent workflow, including authenticated PR actions.

Fix the custom Pi credential exposure and video overwrite before merging.

Findings

  1. P1 Security Custom Pi credentials remain readable ▶
  2. P1 Uploads overwrite existing videos ▶
Fix with agent prompt
### Issue 1
src/agent/sandbox.ts:35-38
If the user stores Pi credentials in a custom `PI_CODING_AGENT_DIR`, `hiddenPaths()` does not hide that directory. The guarded `read` tool can read its `auth.json`. Isolated sessions also link that file into their temporary directories; resolving the link still leaves it outside the denied paths.

Add the configured auth location to the read restrictions before replacing `PI_CODING_AGENT_DIR` with the isolated directory.

**How this was verified:** The sessions link `realAgentDir/auth.json`, while `readablePath()` rejects only the fixed home paths and `/proc`, allowing a custom auth path.

### Issue 2
src/commands/upload.ts:102
Uploading `video.webm` now writes to the adjacent `video.mp4`. If that file already exists, `ffmpeg -y` silently overwrites it. An upload command can therefore destroy a user's existing video.

Create a unique temporary directory beside the input and put the converted file there instead.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR puts agent shell commands behind a sandbox, adds guarded file tools, and moves credentialed GitHub and upload steps into fixed host-side tools.

  • Agent roles run commands and handle files with bounded access.
  • The closer and verifier use fixed tools for PRs and evidence.
  • The steering session can read and run Case actions, but cannot edit files.

Acknowledged limits from mitch-fultz, not reported as findings:

  • Background servers do not survive separate Linux shell calls.
  • Package-cache writes and retrospective ca snapshot can fail.
  • Sandbox commits are unsigned.
  • Baseline commands still run on the host, and repository .pi configuration remains trusted.
  • Readable test credentials and allowed-domain data transfers are intentional.
  • The compiled CLI theme crash predates this PR.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Agent --> FileTools["Guarded read/write/edit"]
  Agent --> Bash["Sandboxed bash"]
  Bash --> Restricted["Restricted files, environment, and network"]
  Agent --> HostTools["Fixed host-side tools"]
  HostTools --> GitHub["Push, PRs, comments, and evidence uploads"]
Loading

Reviews (1) · Last reviewed commit: "fix(orchestrator): restrict ca --agent t..." · Reviewed by Greptile

Comment thread src/agent/sandbox.ts
Comment on lines +35 to +38
function hiddenPaths(): string[] {
// Bun's homedir() ignores $HOME, which is what `~` means to the sandboxed shell; cover both.
const homes = [...new Set([process.env.HOME, homedir()].filter((home): home is string => !!home))];
const configHome = process.env.XDG_CONFIG_HOME;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Custom Pi credentials remain readable

If the user stores Pi credentials in a custom PI_CODING_AGENT_DIR, hiddenPaths() does not hide that directory. The guarded read tool can read its auth.json. Isolated sessions also link that file into their temporary directories; resolving the link still leaves it outside the denied paths.

Add the configured auth location to the read restrictions before replacing PI_CODING_AGENT_DIR with the isolated directory.

How this was verified: The sessions link realAgentDir/auth.json, while readablePath() rejects only the fixed home paths and /proc, allowing a custom auth path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent/sandbox.ts
Line: 35-38

Comment:
**Custom Pi credentials remain readable**

If the user stores Pi credentials in a custom `PI_CODING_AGENT_DIR`, `hiddenPaths()` does not hide that directory. The guarded `read` tool can read its `auth.json`. Isolated sessions also link that file into their temporary directories; resolving the link still leaves it outside the denied paths.

Add the configured auth location to the read restrictions before replacing `PI_CODING_AGENT_DIR` with the isolated directory.

**How this was verified:** The sessions link `realAgentDir/auth.json`, while `readablePath()` rejects only the fixed home paths and `/proc`, allowing a custom auth path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/commands/upload.ts
if ((await ffmpegCheck.exited) === 0) {
const stem = basename(filePath, `.${ext}`);
mp4Path = `/tmp/${stem}.mp4`;
mp4Path = join(dirname(filePath), `${stem}.mp4`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Uploads overwrite existing videos

Uploading video.webm now writes to the adjacent video.mp4. If that file already exists, ffmpeg -y silently overwrites it. An upload command can therefore destroy a user's existing video.

Create a unique temporary directory beside the input and put the converted file there instead.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/upload.ts
Line: 102

Comment:
**Uploads overwrite existing videos**

Uploading `video.webm` now writes to the adjacent `video.mp4`. If that file already exists, `ffmpeg -y` silently overwrites it. An upload command can therefore destroy a user's existing video.

Create a unique temporary directory beside the input and put the converted file there instead.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants