agent-sandbox

command
v0.16.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 2 Imported by: 0

README

agent-sandbox

Routes an AI coding agent's shell commands to either the host machine or a sandboxed nono run invocation, based on operator-configured allow patterns.

Install

go install github.com/ynny-github/agent-sandbox/agent-sandbox@latest

Configuration

Copy config.example.toml to config.toml and edit:

[mcp]
command_output_dir = "/tmp/mcp-output"

[sandbox.shared]                       # host access for both sandboxes
capabilities = ["go", "python"]

[sandbox.agent]
allow_commands = [                     # run on the host; everything else is sandboxed
  "git *",
  "make *",
]

[sandbox.shell]
allow_domains = ["internal.example.com"] # extra domains for sandboxed commands, on top of
                                         # nono's "developer" profile and the capabilities' own

Usage

Start the MCP server:

agent-sandbox command-router --config agent-sandbox.toml

Check whether external dependencies are usable on this host:

agent-sandbox doctor

doctor verifies that nono is on PATH and that the command broker can actually bind a unix socket in its socket directory ($XDG_STATE_HOME/agent-sandbox, or ~/.local/state/agent-sandbox when XDG_STATE_HOME is unset) — the same way agent-sandbox claude does at launch. Exits 0 when all checks pass, 1 otherwise.

Run Claude inside the nono sandbox. Options after -- go to claude; the sandbox profile is generated from the [sandbox.shared] / [sandbox.agent] sections of agent-sandbox.toml, and agent-sandbox no longer forwards options to nono:

agent-sandbox claude -- --model opus

agent-sandbox claude starts a host-side command broker (a per-launch Unix socket server) before launching Claude, and tears it down when Claude exits. Sandboxed commands never run in a persistent container: each one is sent to the broker, which runs it under its own nono run invocation, scoped to the current working directory and the [sandbox.shell] network policy. There is nothing to start beforehand — no sandbox up step. If nono cannot be found or the broker socket cannot be created, Claude is not launched; run agent-sandbox doctor to diagnose.

agent-sandbox debug accepts the same form and prints the resulting nono command without running it, followed by the generated nono profile JSON and the GitHub MCP config JSON (with the token redacted) for inspection.

Register as an MCP tool in your Claude Code settings.

Route a single command through the router from the shell (streams output live):

agent-sandbox exec --config agent-sandbox.toml -- git status
Tool mode

tool_mode in agent-sandbox.toml selects how the agent's commands reach the router:

  • mcp (default): the claude launcher disables the Bash and Monitor tools, and the agent routes commands through the run_command MCP tool.
  • hook: the launcher leaves Bash and Monitor enabled and injects a PreToolUse hook via claude --settings at launch, so each command is rewritten to agent-sandbox exec -- <command> by agent-sandbox hook. No prior setup is needed and nothing is written to .claude/settings.json. agent-sandbox must be on PATH.
Environment variables (--env)

--env is a global flag that loads variables from an env file into the process before it launches Claude or runs a command. It is repeatable and uses a scheme-based reference; only the file: source exists today:

agent-sandbox claude --env file:.env -- --model opus
agent-sandbox exec --env file:.env -- go test ./...

The file is a minimal dotenv subset: KEY=VALUE per line, # comments and blank lines ignored, an optional export prefix stripped, and surrounding quotes removed. There is no variable interpolation. Values override any same-named host environment variable; with multiple --env files, later files win.

For agent-sandbox claude, the loaded keys are also added to the sandbox profile's allowed env vars, so the sandboxed Claude process — and, in hook mode, the commands it runs through agent-sandbox exec — can read them.

GitHub MCP

The built-in GitHub MCP server is enabled when the GITHUB_MCP_TOKEN environment variable is non-empty; otherwise it is not configured. Supply it via --env or the ambient environment. Its value is passed to the MCP server as GITHUB_PERSONAL_ACCESS_TOKEN.

agent-sandbox claude --env file:.secrets.env -- --model opus
# where .secrets.env contains: GITHUB_MCP_TOKEN=ghp_...
Host access: [sandbox.shared], [sandbox.agent], [sandbox.shell]

agent-sandbox generates two nono profiles — one for the launched agent, one for the shell sandbox each brokered command runs in — and host access is declared per profile:

section applies to
[sandbox.shared] both the launched agent and the shell sandbox
[sandbox.agent] the launched agent only
[sandbox.shell] the shell sandbox only

Nothing is inherited between the two sides. A grant reaches a sandbox only if it is written where that sandbox can see it, which is why there is no way to subtract one: "keep this away from sandboxed commands" is expressed by declaring it under [sandbox.agent] instead of the shared base. Forgetting to put a grant in the shared base leaves a command sandbox without it; it can never silently gain one.

All three sections take the same fields. capabilities are named bundles — go, python, node, rust, docker, ssh, mise, bashrc — each expanding to the directories, files, env vars, and network domains that capability needs. Raw grants (allow, read, allow_file, read_file, allow_env) cover anything not already covered by a capability. The common PATH/HOME/... env vars and /dev/null are always granted from a built-in baseline.

A capability's domains are added to the shell sandbox's network, so each toolchain brings its own registry — go the module proxy, python PyPI, node the npm registry, rust crates.io, docker Docker Hub, mise its update and version hosts — without a config having to restate them. sandbox.shell.allow_domains is for what no capability covers. Domains apply to whichever side has a network to widen — that is the shell sandbox only, since the launched agent keeps the network of its nono base profile — so a capability declared under [sandbox.agent] contributes no domains.

docker and ssh expose host credentials but are otherwise ordinary capabilities: they apply to whichever side declares them. Put them in [sandbox.agent], not the shared base, unless a sandboxed command really needs the keys. agent-sandbox debug prints both resolved profiles and warns when the command profile ends up granting a credential path, and agent-sandbox ai explain lists the paths a sandboxed command actually reaches outside the working directory, resolved from the active config.

--env KEY=VALUE grants the launched agent alone (it is appended to [sandbox.agent].allow_env), so exposing a variable to sandboxed commands stays an explicit config edit.

Safe wrappers

agent-sandbox safe <tool> ... runs a tool only after validating that its invocation is safe, then passes the command through unchanged.

safe docker-compose
agent-sandbox safe docker-compose up -d

This resolves the project with docker compose config and refuses the invocation (exit 1, running nothing) when any of the following hold:

  • a bind mount resolves to a path outside the current working directory;
  • a bind mount targets the Docker socket (docker.sock);
  • a service sets privileged: true, network_mode: host, pid: host, ipc: host, or userns_mode: host;
  • a service exposes host devices;
  • cap_add contains a dangerous capability (e.g. SYS_ADMIN, NET_ADMIN);
  • security_opt disables confinement (seccomp:unconfined, apparmor:unconfined, label:disable);
  • the subcommand is run or exec.

Named volumes and tmpfs mounts are allowed, and every other subcommand (up, build, down, ps, logs, ...) passes through. The danger rules are fixed and built-in.

How It Works

  • Commands matching an allow pattern are executed on the host (after shell-safety validation).
  • Commands matching a drop pattern are refused — neither the host nor the sandbox runs them; the response carries exit code 1 and a stderr line. Each drop entry is a { pattern, message } table; when message is set it is printed on refusal, otherwise the default dropped: command matches drop pattern "<pattern>" line is used.
  • All other commands are sent to the host-side command broker started by agent-sandbox claude, which runs each one under its own nono run invocation, scoped to the current working directory and nono's developer network profile plus the extra domains (sandbox.shell.allow_domains and the declared capabilities' own).
  • Allow wins over drop: a command matching both an allow and a drop pattern runs on the host.
  • Output is always written to separate stdout/stderr files; the MCP response returns file paths and exit code only.

sandbox.agent.drop_commands example:

[sandbox.agent]
drop_commands = [
  { pattern = "git *" },
  { pattern = "gh *", message = "gh is disabled in this sandbox. Use the GitHub MCP server's tools instead." },
]

Migrating from an older config

The configuration was reorganized; old keys are no longer accepted.

Old New
server.output_dir mcp.command_output_dir
sandbox.build_context removed — commands run under nono, not Docker
sandbox.dockerfile removed — commands run under nono, not Docker
sandbox.image removed — commands run under nono, not Docker
sandbox.external_network removed — commands run under nono, not Docker
sandbox.container (whole section) removed — commands run under nono, not Docker
sandbox.network.allow_external removed — use sandbox.shell.allow_domains
sandbox.allow_cidrs removed — network access is now sandbox.shell.allow_domains
sandbox.allow_hosts removed — network access is now sandbox.shell.allow_domains
sandbox.network.allow_cidrs removed — replaced by sandbox.shell.allow_domains
sandbox.network.allow_hosts removed — replaced by sandbox.shell.allow_domains
[allow_patterns] patterns sandbox.agent.allow_commands
[drop_patterns] patterns sandbox.agent.drop_commands
[deny_patterns] patterns removed — move destructive entries into sandbox.agent.drop_commands
[container] env_passthrough allow_env in [sandbox.shell]
sandbox.container.env_passthrough allow_env in [sandbox.shell]
[nono] profile removed — configure the sandbox profile in [sandbox.shared] / [sandbox.agent] (nono options are no longer forwarded)
sandbox.network (whole section) sandbox.shell.allow_domains — it only ever configured brokered commands
sandbox.network.allow_domains sandbox.shell.allow_domains
sandbox.command.env_passthrough allow_env in [sandbox.shell]
docker / ssh in [sandbox.host] move to [sandbox.agent] — the shared base reaches the shell sandbox too, and these capabilities carry no built-in exclusion
sandbox.host sandbox.shared
sandbox.agent.host sandbox.agent (the keys move up one level)
sandbox.command.host sandbox.shell
sandbox.command.network.allow_domains sandbox.shell.allow_domains
sandbox.command.allow / sandbox.command.drop sandbox.agent.allow_commands / sandbox.agent.drop_commands
taskgate capability removed — grant ~/.local/state/taskgate with a raw read if a project still needs it
agent-sandbox sandbox up/down/prune removed — agent-sandbox claude starts and stops the per-launch command broker automatically

The deny routing axis is gone. Patterns that previously forced a host-allowed command into the sandbox now have two options: leave them out of allow (so they default to the sandbox), or add them to drop if they should be refused entirely.

Documentation

Overview

agent-sandbox/main.go

Directories

Path Synopsis
agent-sandbox/cmd/claude.go
agent-sandbox/cmd/claude.go
internal
broker
Package broker carries command execution across the sandbox boundary.
Package broker carries command execution across the sandbox boundary.
claude
Package claude builds and runs the sandboxed `claude` command: it parses the launcher's arguments, constructs the `nono wrap … claude …` invocation (including the hook settings injected in hook mode), and executes it.
Package claude builds and runs the sandboxed `claude` command: it parses the launcher's arguments, constructs the `nono wrap … claude …` invocation (including the hook settings injected in hook mode), and executes it.
envflag
Package envflag loads environment variables from --env references and applies them to the current process.
Package envflag loads environment variables from --env references and applies them to the current process.
policysnapshot
Package policysnapshot persists the sandbox policy (a *config.Config) to a per-session JSON file so hook-mode `agent-sandbox exec` can route from a frozen copy the agent cannot edit, rather than re-reading the mutable agent-sandbox.toml on every command.
Package policysnapshot persists the sandbox policy (a *config.Config) to a per-session JSON file so hook-mode `agent-sandbox exec` can route from a frozen copy the agent cannot edit, rather than re-reading the mutable agent-sandbox.toml on every command.
router
Package router routes a command to drop/host/sandbox and executes it, independent of any transport (MCP, CLI).
Package router routes a command to drop/host/sandbox and executes it, independent of any transport (MCP, CLI).
safe
Package safe holds shared helpers for the "safe" command wrappers.
Package safe holds shared helpers for the "safe" command wrappers.
safe/dockercompose
Package dockercompose validates and runs docker compose invocations safely.
Package dockercompose validates and runs docker compose invocations safely.
safe/git
Package git implements the "safe git" wrapper: it parses a git argv and reports known-dangerous invocations so the command layer can refuse them.
Package git implements the "safe git" wrapper: it parses a git argv and reports known-dangerous invocations so the command layer can refuse them.
sandboxhost
Package sandboxhost is the single source of truth for host-access capabilities.
Package sandboxhost is the single source of truth for host-access capabilities.
shellquote
Package shellquote quotes strings as single shell tokens.
Package shellquote quotes strings as single shell tokens.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL