Wrap agent terminal commands in macOS Seatbelt sandbox - #57431
Merged
Conversation
Introduces a new crate that wraps shell invocations under macOS's sandbox-exec(1) with a Seatbelt policy built from per-command permissions. Reads are unrestricted; writes are restricted to a caller-provided list of directories; network access and unrestricted filesystem writes must be opted into per command. No callers yet — the agent terminal tool wires up in a follow-up.
Adds a 'sandboxing' feature flag (off for staff by default) and, when it's on, renders a 'Terminal sandbox' section in the agent's system prompt that describes which paths are writable and which per-command flags the model can request to relax the sandbox. No behavior change to terminal execution yet — the next commit wires the actual sandbox-exec invocation into the terminal tool.
When the 'sandboxing' feature flag is on (macOS only), agent-run terminal commands are launched under /usr/bin/sandbox-exec with a per-command Seatbelt policy: - Reads: any path on the filesystem. - Writes: each project worktree plus a per-command $TMPDIR. - Network: blocked. The model can request relaxations on individual calls via three new input flags \u2014 allow_network, allow_fs_write, unsandboxed \u2014 which trigger a separate always-prompt user approval before the command runs. The per-command tempdir is provisioned regardless of sandbox state so the model can't infer whether the sandbox is in effect. The Seatbelt config file's lifetime is tied to the Terminal entity via an opaque RAII handle, so cancellation paths drop it automatically.
rtfeldman
force-pushed
the
sandboxing/3-tool
branch
from
May 21, 2026 18:51
0b41bbd to
ff16a04
Compare
rtfeldman
force-pushed
the
sandboxing/2-prompt
branch
from
May 21, 2026 18:51
a016ec2 to
461142f
Compare
rtfeldman
marked this pull request as ready for review
May 27, 2026 20:30
| // user's shared `/tmp`. Decoupling it from sandbox state also | ||
| // means the model can't infer the sandbox state by looking at | ||
| // `$TMPDIR`. | ||
| let temp_dir = tempfile::Builder::new() |
Contributor
There was a problem hiding this comment.
Temp dir life cycle should be tied to the thread lifecycle.
Contributor
There was a problem hiding this comment.
Non Trivial change will be done in a follow up PR
MartinYe1234
approved these changes
May 27, 2026
TomPlanche
pushed a commit
to TomPlanche/zed
that referenced
this pull request
Jun 2, 2026
…s#57431) Stacked on top of zed-industries#57430. When the `sandboxing` feature flag is on (macOS only), agent-run terminal commands are launched under `/usr/bin/sandbox-exec` with a per-command Seatbelt policy: - **Reads:** any path on the filesystem. - **Writes:** each project worktree plus a per-command `$TMPDIR`. - **Network:** blocked. The model can request relaxations on individual calls via three new input flags on the `terminal` tool: - `allow_network: true` - `allow_fs_write: true` - `unsandboxed: true` Each one triggers a separate always-prompt user approval (bypassing any `always_allow` rules, since escalation is a stronger trust boundary than the baseline command approval). The flags are silently ignored when sandboxing is off, so the model can't surreptitiously change runtime behavior by setting them in the no-op case. The per-command tempdir is provisioned regardless of sandbox state so the model can't infer whether the sandbox is in effect by looking at `$TMPDIR`. The Seatbelt config file's lifetime is tied to the `Terminal` entity via an opaque RAII handle (`SandboxConfigHandle = Box<dyn Any + Send>`), so cancellation paths drop it automatically. ### Notes for review - The writable scope passed to the sandbox is the project's worktree paths plus the per-command tempdir, **not** the resolved `cd` working directory — `cd` is model-controlled, and using it as the writable scope would let the model widen its own write permissions outside the project. - The new tool flags are always present in the JSON schema (with `#[serde(default)]`), even when the sandbox prompt section isn't rendered. The system prompt only documents them when the section is present, so the model shouldn't try to use them when it's not — but the schema doesn't actively reject them, just ignores them. - I opted not to add an end-to-end test for the escalation prompt path: the existing `FakeThreadEnvironment` ignores the new params and toggling the feature flag in tests is more wiring than felt worth it for a first cut. The pure-function pieces (`sandbox_approval_title`, schema, deserialization) are covered, and the sandbox crate itself has end-to-end tests that actually invoke `sandbox-exec`. Release Notes: - N/A --------- Co-authored-by: MartinYe1234 <52641447+MartinYe1234@users.noreply.github.com>
This was referenced Jun 10, 2026
This was referenced Jul 1, 2026
This was referenced Jul 10, 2026
jonx
pushed a commit
to jonx/zed-aros
that referenced
this pull request
Jul 17, 2026
…s#57431) Stacked on top of zed-industries#57430. When the `sandboxing` feature flag is on (macOS only), agent-run terminal commands are launched under `/usr/bin/sandbox-exec` with a per-command Seatbelt policy: - **Reads:** any path on the filesystem. - **Writes:** each project worktree plus a per-command `$TMPDIR`. - **Network:** blocked. The model can request relaxations on individual calls via three new input flags on the `terminal` tool: - `allow_network: true` - `allow_fs_write: true` - `unsandboxed: true` Each one triggers a separate always-prompt user approval (bypassing any `always_allow` rules, since escalation is a stronger trust boundary than the baseline command approval). The flags are silently ignored when sandboxing is off, so the model can't surreptitiously change runtime behavior by setting them in the no-op case. The per-command tempdir is provisioned regardless of sandbox state so the model can't infer whether the sandbox is in effect by looking at `$TMPDIR`. The Seatbelt config file's lifetime is tied to the `Terminal` entity via an opaque RAII handle (`SandboxConfigHandle = Box<dyn Any + Send>`), so cancellation paths drop it automatically. ### Notes for review - The writable scope passed to the sandbox is the project's worktree paths plus the per-command tempdir, **not** the resolved `cd` working directory — `cd` is model-controlled, and using it as the writable scope would let the model widen its own write permissions outside the project. - The new tool flags are always present in the JSON schema (with `#[serde(default)]`), even when the sandbox prompt section isn't rendered. The system prompt only documents them when the section is present, so the model shouldn't try to use them when it's not — but the schema doesn't actively reject them, just ignores them. - I opted not to add an end-to-end test for the escalation prompt path: the existing `FakeThreadEnvironment` ignores the new params and toggling the feature flag in tests is more wiring than felt worth it for a first cut. The pure-function pieces (`sandbox_approval_title`, schema, deserialization) are covered, and the sandbox crate itself has end-to-end tests that actually invoke `sandbox-exec`. Release Notes: - N/A --------- Co-authored-by: MartinYe1234 <52641447+MartinYe1234@users.noreply.github.com>
jolutz
pushed a commit
to jolutz/zed
that referenced
this pull request
Aug 8, 2026
…s#57431) Stacked on top of zed-industries#57430. When the `sandboxing` feature flag is on (macOS only), agent-run terminal commands are launched under `/usr/bin/sandbox-exec` with a per-command Seatbelt policy: - **Reads:** any path on the filesystem. - **Writes:** each project worktree plus a per-command `$TMPDIR`. - **Network:** blocked. The model can request relaxations on individual calls via three new input flags on the `terminal` tool: - `allow_network: true` - `allow_fs_write: true` - `unsandboxed: true` Each one triggers a separate always-prompt user approval (bypassing any `always_allow` rules, since escalation is a stronger trust boundary than the baseline command approval). The flags are silently ignored when sandboxing is off, so the model can't surreptitiously change runtime behavior by setting them in the no-op case. The per-command tempdir is provisioned regardless of sandbox state so the model can't infer whether the sandbox is in effect by looking at `$TMPDIR`. The Seatbelt config file's lifetime is tied to the `Terminal` entity via an opaque RAII handle (`SandboxConfigHandle = Box<dyn Any + Send>`), so cancellation paths drop it automatically. ### Notes for review - The writable scope passed to the sandbox is the project's worktree paths plus the per-command tempdir, **not** the resolved `cd` working directory — `cd` is model-controlled, and using it as the writable scope would let the model widen its own write permissions outside the project. - The new tool flags are always present in the JSON schema (with `#[serde(default)]`), even when the sandbox prompt section isn't rendered. The system prompt only documents them when the section is present, so the model shouldn't try to use them when it's not — but the schema doesn't actively reject them, just ignores them. - I opted not to add an end-to-end test for the escalation prompt path: the existing `FakeThreadEnvironment` ignores the new params and toggling the feature flag in tests is more wiring than felt worth it for a first cut. The pure-function pieces (`sandbox_approval_title`, schema, deserialization) are covered, and the sandbox crate itself has end-to-end tests that actually invoke `sandbox-exec`. Release Notes: - N/A --------- Co-authored-by: MartinYe1234 <52641447+MartinYe1234@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on top of #57430.
When the
sandboxingfeature flag is on (macOS only), agent-run terminal commands are launched under/usr/bin/sandbox-execwith a per-command Seatbelt policy:$TMPDIR.The model can request relaxations on individual calls via three new input flags on the
terminaltool:allow_network: trueallow_fs_write: trueunsandboxed: trueEach one triggers a separate always-prompt user approval (bypassing any
always_allowrules, since escalation is a stronger trust boundary than the baseline command approval). The flags are silently ignored when sandboxing is off, so the model can't surreptitiously change runtime behavior by setting them in the no-op case.The per-command tempdir is provisioned regardless of sandbox state so the model can't infer whether the sandbox is in effect by looking at
$TMPDIR.The Seatbelt config file's lifetime is tied to the
Terminalentity via an opaque RAII handle (SandboxConfigHandle = Box<dyn Any + Send>), so cancellation paths drop it automatically.Notes for review
cdworking directory —cdis model-controlled, and using it as the writable scope would let the model widen its own write permissions outside the project.#[serde(default)]), even when the sandbox prompt section isn't rendered. The system prompt only documents them when the section is present, so the model shouldn't try to use them when it's not — but the schema doesn't actively reject them, just ignores them.FakeThreadEnvironmentignores the new params and toggling the feature flag in tests is more wiring than felt worth it for a first cut. The pure-function pieces (sandbox_approval_title, schema, deserialization) are covered, and the sandbox crate itself has end-to-end tests that actually invokesandbox-exec.Release Notes: