Skip to content

Run trusted commands outside the sandbox, as Codex's allow rules do - #309

Merged
lydakis merged 5 commits into
mainfrom
claude/project-thread-imncn8
Oct 29, 2026
Merged

lydakis merged 5 commits into
mainfrom
claude/project-thread-imncn8

Conversation

@lydakis

@lydakis lydakis commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Requested by George · project thread

Why

mcpx, and any tool that keeps a daemon of its own, fails in a sandboxed bot. The socket isn't the blocker: on macOS a Sonny bot with the network on can already connect to AF_UNIX sockets. Every mcpx call first opens ~/.local/state/mcpx/daemon.lock for writing, which the sandbox refuses even when the daemon is already running. A daemon started from inside the sandbox would also keep the sandbox's limits (its MCP servers couldn't write their caches), and on Linux it would die with the command's pid namespace.

I reproduced this here with a real mcpx build and a scripted bot:

sandboxed:              mcpx: acquiring daemon lock: opening lock file: open …/.local/state/mcpx/daemon.lock: read-only file system
unsandboxed = ["mcpx"]: hello   (exit 0)

What Codex does

Codex in workspace-write blocks this too. On macOS it doesn't allow AF_UNIX sockets unless [permissions.network] unix_sockets lists them, and it doesn't let tools like this run inside the sandbox. It gets them out in one of two ways:

  • An approved escalation. With on-request and the auto-reviewer, there's no prompt the person sees.
  • An execpolicy rule. prefix_rule(pattern=[...], decision="allow") makes a matching command skip approval and bypass the sandbox (bypass_sandbox: true in core/src/exec_policy.rs), but only when every command in the parsed script matches.

Claude Code's equivalent of the rule is sandbox.excludedCommands. Codex's full access has no sandbox at all.

Change

Daemon

  • A new bot setting, unsandboxed: a list of command prefixes such as mcpx or docker compose. Nothing is tool-specific, and nothing is in the list by default.
  • A shell call that doesn't ask to escalate, and whose script runs nothing but matching commands, runs unconfined and is not gated.
  • Only a plain script matches, as Codex reads one: words (quoted or not) in commands joined by &&, ||, ;, | or newlines.
  • These stay sandboxed: $, backquotes, redirections, parentheses, a lone & and comments. So does a script containing any other command (mcpx … | jq), which can still escalate.
  • Both the single-call and parallel paths honor it.

API and CLI

  • create and configure take unsandboxed together with the sandbox. It is refused without a read-only or workspace-write sandbox, and capped at 64 entries of up to 1 KiB each.
  • agent run --unsandboxed CMD is repeatable.
  • A bot made by a sandboxed bot keeps only the entries its creator has. A sandboxed bot still can't configure itself through its own socket.

Sonny

  • [sonny] unsandboxed = [...] in ~/.agent/config.toml goes to every confined lead and task. A lead whose list differs gets the new one when Sonny next opens it.
  • In full mode, tasks now run unconfined, as their lead does and as Codex's full access does. In full an agent has no gate, so a confined task's request to leave the sandbox was always refused with escalation_refused, and agents worked around it through panes. start_task reads the lead's mode, and an /approvals pick sets the tasks' sandboxes along with their gates.

Docs: RUST_PROTOTYPE (settings table, configure, sandbox section), CLI.md, sonny/README.

Known edge

The command is resolved on the daemon's PATH, so an entry is only as trusted as the folders on it. The same holds for Codex's rules. This is noted in the docs, and per the "good enough" safety stance it isn't built further.

Daemon cost

When unsandboxed is empty, Bot::unsandboxed returns on a Vec::is_empty check before parsing anything. It adds no store work and no per-bot state.

Tests

  • sandbox::tests::only_a_plain_script_of_allowed_commands_leaves_the_sandbox covers the matcher: 6 scripts that match and 23 that don't.
  • test_approvals.test_a_command_its_client_trusts_runs_outside_the_sandbox_without_asking: under a manual gate, a trusted command writes outside its roots without an approval, both alone and in a parallel batch, while a mixed script stays confined. It also checks configure validation and inheritance.
  • test_served.test_a_lead_starts_a_task_in_a_worktree_of_its_own: a task of an auto lead is confined, and a task of a full lead is not.
  • Sonny approvals tests cover regate setting a task's sandbox with its mode, and client config tests cover the new key.
  • After merging main, these pass: cargo test --workspace, clippy with -D warnings, cargo fmt --check, and the full runtime suite (tests.run --jobs 4, AGENT_TEST_SANDBOX=1 with bubblewrap).
  • Seatbelt is unchanged, but this hasn't been run on a Mac yet.

🤖 Generated with Claude Code

https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV

@lydakis lydakis self-assigned this Oct 29, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T05:03:56.573543Z a0727fa New commits
🔒 Security Review ✅ Completed 2026-10-29T05:05:38.825676Z a0727fa New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f31142a7b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/turn.rs Resolved
@lydakis
lydakis force-pushed the claude/project-thread-imncn8 branch 2 times, most recently from 25ce6bc to 63c2ac5 Compare October 29, 2026 04:52

lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

rust and macos fail on sonny/src/trigger.rs:2377 (no method named plist on Places). The failure comes from main, not this PR: main hasn't compiled since #318 and #344 landed together, and this PR doesn't touch trigger.rs. #351 fixes it. I'll rebase onto main once #351 merges, and merge this when CI is green.


Generated by Claude Code

A tool with a daemon of its own, such as mcpx, can't run in a bot's
sandbox: every call opens its lock file under ~/.local/state, and a
daemon started there would keep the sandbox's limits (on Linux it dies
with the command). Codex answers this with execpolicy rules that allow a
command prefix and run it unsandboxed; Claude Code with excludedCommands.

Bots now take `unsandboxed`, command prefixes (`mcpx`, `docker compose`)
that run outside their sandbox without asking. Only a plain script
matches, as Codex parses one: words, quoted or not, joined by &&, ||, ;,
| or newlines, every command a match. Expansions, substitutions,
redirections, subshells, background jobs and comments stay sandboxed, as
does a script with any other command. `create` and `configure` take it
with the sandbox, `agent run --unsandboxed` sets it, and a bot a
sandboxed bot makes keeps only what its creator has.

Sonny reads `[sonny] unsandboxed = ["mcpx"]` from ~/.agent/config.toml
and gives it to every confined lead and task; a lead whose list differs
is given it again when Sonny opens it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV
In `full` an agent has no gate, so a confined task's call that asks to
leave its sandbox was always refused (`escalation_refused`), and agents
worked around it through panes. As under Codex's full access, a task now
runs unconfined in `full`: start_task reads the lead's mode, and an
/approvals pick sets the tasks' sandboxes along with their gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGHN4UHiWFxnggwKHoV
#318's restamp still read the definition through Places::plist, which
#344 renamed to Places::job, so main did not compile. The store id it
rewrites now has a unit form too, and restamp runs at start on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jkdGF2ShRt6WjfWgiPdZF
@lydakis
lydakis force-pushed the claude/project-thread-imncn8 branch from 63c2ac5 to a0727fa Compare October 29, 2026 05:02

lydakis commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Following up on the comment above: I rebased onto main cc10fb8 and ported #351's commit as a0727fa, so CI can go green without waiting for it. Once #351 merges, the ported commit becomes a no-op. Locally, before the port, cargo build -p sonny reproduced the E0599, and after it the workspace tests, clippy, fmt and the runtime suite all pass.


Generated by Claude Code

@lydakis
lydakis merged commit fc8560a into main Oct 29, 2026
5 checks passed
@lydakis lydakis mentioned this pull request Oct 29, 2026
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