Skip to content

evdutt/ai-sandbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ai-sandbox

Hardened podman sandbox for running Claude Code, OpenAI Codex CLI, and OpenCode in fully autonomous mode (--dangerously-skip-permissions / --dangerously-bypass-approvals-and-sandbox) with the hard guarantee that the only thing the agent can damage is the directory you launch it from, plus that tool's own podman-managed named volumes; no other host path is mounted writable.

podman runs rootless and daemonless by default: the container engine is just a binary the launching user invokes, with no privileged background service, and in-container root maps to a non-privileged subuid on the host.

Primary target: macOS with podman machine. Linux hosts (including WSL2) are supported with one documented behavioral difference (see Host platforms).

For the buildable, file-by-file specification of how this works internally, see SPEC.md. This README is the operator's guide.


Why it's safe to be autonomous

Autonomy is enforced by six independent layers. Any one of them blocks the worst attacks; they are independent so a flaw in one does not cascade.

  1. Credential starvation (primary). No SSH keys, no GITHUB_TOKEN, no credential helpers, no ~/.gitconfig from the host. Only the single *_API_KEY for the chosen tool is forwarded. git push and gh cannot authenticate even if invoked. If the agent generates its own SSH key, that key is not registered with any remote account — push still fails. The chosen provider key or SSO token is still available to the agent process and same-uid subprocesses; use offline mode (SANDBOX_NO_NET=1) when provider-token exposure is unacceptable.

  2. OS-level wrappers at /usr/bin/git (with the real binary moved to /usr/bin/git.real), /usr/local/bin/gh, and /usr/local/bin/npm / /usr/local/bin/npx. They hard-exit on remote-mutating git subcommands (push, fetch-pack, send-pack, send-email, svn, p4, request-pull, remote add/set-url/rename/remove/rm, config --global/--system) and on every gh/glab/hub subcommand. The git wrapper resolves the actual subcommand by walking past global options that take values (-C, -c key=val, --git-dir=…, --work-tree=…, -C=, --exec-path=…), so bypasses like git -C /tmp push or git -c safe.directory=* push are caught — not just git push written naively. The stable /usr/bin/git.real path is also denied at the agent layer; credential starvation remains the backstop if same-uid code invokes the real binary directly.

    Every wrapper rejection writes one line to stderr: ai-sandbox: blocked: <category>: <command summary> and exits 126. This format is grep-friendly and consumed by sandbox verify.

  3. Container isolation: non-root (--user 1001), --cap-drop=ALL, --security-opt=no-new-privileges, --read-only rootfs, tmpfs scratch with size caps and noexec on /tmp and /run, --memory / --cpus / --pids-limit / --ulimit.

  4. Parity denylists across the three agents block the same set of dangerous commands at the agent layer (git-remote, forge CLIs, cloud CLIs, remote shells, package publishing, privilege escalation, direct API calls from a shell, dangerous FS outside /workspace). The three per-agent configs are generated from a single canonical Python rule set; see Parity between the three agents.

  5. .git/config and .git/hooks bind-mounted read-only for the root repository and nested repositories under the workspace, so the agent cannot silently change your remote URL or plant a hook that would later run when you type git commit on the host. Inside the container core.hooksPath=/dev/null in /etc/gitconfig neutralises the known bypass of pointing core.hooksPath elsewhere in the workspace.

  6. Supply-chain cooldown. Everything the image installs is held to a single cutoff: today − SANDBOX_PKG_LAG_DAYS (default 14 days). "Newest available, but at least N days old." Five sources are covered:

    • Base image: the launcher queries Docker Hub's tags API for the newest node:22.x.y-bookworm-slim whose last_updated is on or before the cutoff and pins it by digest (@sha256:…), defeating tag-overwrite attacks. If the API is unreachable while lag is enabled, the build fails closed unless SANDBOX_ALLOW_FLOATING_BASE=1 is set explicitly.
    • APT packages: /etc/apt/sources.list is rewritten to point at snapshot.debian.org/archive/debian/<YYYYMMDDTHHMMSSZ>/, so git, curl, jq, ripgrep, and the rest are resolved against Debian's date-stamped archive snapshot.
    • npm packages (build + runtime): npm install --before=DATE at image build time covers @anthropic-ai/claude-code, @openai/codex, and all transitive deps. The same date is forwarded into running containers as SANDBOX_PKG_LAG_DATE/npm_config_before, and runtime npm/npx wrappers reject user-supplied --before overrides, while stale compatibility paths such as /usr/local/bin/npm.real and /usr/local/bin/npx.real are removed from the image.
    • opencode: not on npm — the build queries the GitHub releases API for the newest release asset whose published_at is on or before the cutoff, downloads the versioned Linux tar.gz directly from GitHub, and verifies the asset's SHA-256 digest as published in the release metadata before installing it. This protects against in-flight tampering between the GitHub API call and the asset download; it is not signature verification of the release itself, since opencode releases are not signed today.
    • uv: installed via PyPI with a version pinned to the newest release whose upload_time is on or before the cutoff. The build queries pypi.org/pypi/uv/json, resolves the qualifying version from the release history, and installs uv==VERSION with pip.
    • Runtime package managers (pnpm, bun, uv, pip): per-user config files are written at image build time (mode 0444) so that if the agent invokes these managers they also respect the cooldown window. The values are derived from SANDBOX_PKG_LAG_DAYS: pnpm uses minimum-release-age (minutes) in ~/.config/pnpm/rc; bun uses minimumReleaseAge (seconds) in ~/.bunfig.toml; uv uses exclude-newer (RFC 3339 date) in ~/.config/uv/uv.toml; pip uses uploaded-prior-to (ISO 8601 duration P<N>D, requires pip ≥ 26.1) in ~/.config/pip/pip.conf. These are config-layer controls — they can be bypassed per-invocation with flags — not OS-level enforcement. pip install is additionally denylisted at the agent layer. yarn, cargo install, go install, and gem install have no cooldown enforcement. Intentional per-invocation overrides when a package is known safe:
      npm install <pkg> --min-release-age 0
      pnpm add <pkg> --minimum-release-age 0
      bun add <pkg> --minimum-release-age 0
      UV_EXCLUDE_NEWER= uv add <pkg>
      

    Set SANDBOX_PKG_LAG_DAYS=0 to disable all controls. The cutoff is recomputed every invocation, so sandbox build always picks up the newest versions inside the cooldown window. For repeatable builds across days, see Reproducibility.

Mounting .git/config:ro does not by itself prevent git push — it just stops URL rewriting. That's why credential starvation (layer 1) and the OS wrappers (layer 2) are the real defense for remote-repo protection.


Threat model

Prevented

  • Dangerous or unauthorised changes to remote git state (push, force-push, branch delete, repo delete) — via layers 1–4.
  • Use of gh / glab / hub / aws / gcloud / az / kubectl / docker / podman against real accounts.
  • Exfiltration of host SSH keys, ~/.aws, ~/.config/gh, env tokens.
  • Writing outside /workspace on the host.
  • Privilege escalation, nested docker/podman, host package installation.
  • Fork bombs, memory balloons, disk-fill of the host.
  • Broad executable scratch space: /tmp stays non-exec. OpenCode gets a dedicated exec-only temp mount via TMPDIR because its TUI runtime extracts a render library before loading it.
  • Supply-chain attacks where a malicious release (npm package, Debian package, base-image tag overwrite, opencode release, or uv release) is published less than SANDBOX_PKG_LAG_DAYS ago and the community yanks/reverts it within that window — neither the build nor the agent's runtime npm install will see it. Claude Code, Codex, and OpenCode CLI autoupdate are disabled at runtime, so CLI upgrades only happen during sandbox build. (Layer 6.)

Not prevented (inherent)

  • Same-uid code inside an agent session reading or modifying that tool's persistent podman auth/config volumes (sandbox-<tool>-home and nested config/state volumes). These volumes are trusted state for the selected agent, not an isolation boundary from the agent.
  • Same-uid subprocesses reading the selected provider credential from the process environment or from the agent's own persisted auth files.
  • Arbitrary network egress while agent networking is enabled. Shell denylists are command-pattern controls, not a firewall; Python, Node, and other HTTP clients can still make outbound requests.
  • Prompt-injection that manipulates the agent's intended LLM API actions (the agent is, by design, talking to its provider).
  • Malicious code the agent writes into /workspace that you later run outside the sandbox. Treat each session's diff like a PR from a stranger: sandbox doctor + git log/git diff after each run.
  • Runtime cooldown bypass via package managers other than npm/npx. See Known limitations.

File layout

.
├── Containerfile
├── sandbox                            # launcher (chmod +x)
├── README.md                          # this file
├── SPEC.md                            # buildable specification
├── config/
│   ├── deny_dangerous.py              # canonical denylist (source of truth)
│   ├── generate_configs.py            # produces generated agent configs + wrapper data
│   ├── extra_rules.yaml               # optional local additions
│   ├── wrappers_data.py               # GENERATED — consumed by wrappers
│   ├── claude-config/
│   │   ├── settings.json              # GENERATED — do not edit by hand
│   │   └── statusline.sh              # Claude Code status line command
│   ├── codex-config/
│   │   ├── config.toml                # GENERATED — do not edit by hand
│   │   └── codex-hooks/
│   │       └── deny_dangerous.py      # symlink → ../../deny_dangerous.py
│   └── opencode-config/
│       └── opencode.json              # GENERATED — do not edit by hand
├── wrappers/
│   ├── git                            # Python; replaces /usr/bin/git
│   ├── gh                             # POSIX sh; also installed as glab, hub
│   ├── npm                            # POSIX sh; rejects --before overrides
│   └── npx                            # POSIX sh; rejects --before overrides
└── verify/
    ├── escape_attempts.yaml           # cases that MUST fail
    ├── threat_model_assertions.yaml   # documented residual-risk checks
    ├── freshness_check.py             # static check that generated configs match the generator
    └── run_verify.py                  # verification entry point

Setup

macOS

  1. Install podman (brew install podman) and initialise the embedded Linux VM:
    podman machine init --cpus 4 --memory 6144 --disk-size 60
    podman machine start
    podman machine runs a small Linux VM (via Apple's Virtualization framework) that hosts the actual container engine; bind mounts of $PWD are surfaced into the VM over virtiofs automatically. Increase CPU/RAM if you plan to run SANDBOX_MEM>6g.
  2. Clone this repo, then:
    chmod +x sandbox
    ln -s "$PWD/sandbox" ~/.local/bin/sandbox    # user-wide symlink so you can run sandbox easily in any directory
  3. In your shell rc, export only the API key(s) you need:
    export ANTHROPIC_API_KEY=...      # for: sandbox claude
    export OPENAI_API_KEY=...         # for: sandbox codex
    export OPENROUTER_API_KEY=...     # for: sandbox opencode
  4. Optional: if you use Claude/Codex SSO or device auth instead of API keys, the tokens persist in per-tool podman named volumes (sandbox-<tool>-home and sandbox-<tool>-config), survive across sessions, and are inspectable with podman volume ls | grep sandbox-. Wipe a tool's auth with podman volume rm sandbox-claude-home sandbox-claude-config (etc.).

Linux & WSL2

Same as macOS apart from the VM setup (not needed — podman runs natively in rootless mode) and the .venv_linux/ mirror (not created — the host's .venv is already a Linux virtualenv). gVisor (runsc) is compatible with these flags but not required; rootless mode itself is the default and is the main isolation win over rootful docker.

Host platforms

Platform Status Notes
macOS + podman machine Primary target .venv_linux/ mirror created on start
Linux + rootless podman Supported Default mode; no .venv mirror needed
Linux + rootful podman Supported Works the same; loses the rootless win
WSL2 + podman Supported Treated as Linux
Windows (native) Not supported Use WSL2

The hardening flags above work with default rootless podman. --read-only, --cap-drop=ALL, and --security-opt=no-new-privileges are all honored. The launcher passes --userns=keep-id:uid=1001,gid=1001 so the in-container sandbox user (UID 1001) maps to the host launching user; this is what makes the workspace bind mount writable under rootless mode. gVisor can be layered on top via --runtime=runsc if you need stronger isolation; compatible with these flags but not required.


Usage

cd ~/projects/my-thing          # this becomes /workspace (the ONLY writable host path)
sandbox claude                  # or: codex / opencode / shell

First invocation auto-builds the image (~1 minute). Rebuild at any time:

sandbox build

Subcommands

sandbox claude       # run Claude Code in /workspace, network on
sandbox codex        # run Codex CLI in /workspace, network on
sandbox opencode     # run OpenCode TUI in /workspace, network on
sandbox shell        # raw bash, no agent, --network=none, ephemeral home
sandbox build        # (re)build the image
sandbox doctor       # audit host: leaky env vars, workspace remotes, image freshness
sandbox verify       # run escape-attempt suite + freshness check + threat-model assertions
sandbox lock         # write sandbox.lock.json pinning current resolved versions
sandbox --help       # show this list
sandbox --version    # show launcher + image versions

Exit codes: 0 success, 2 user error (bad flags, no API key when one is required), 3 build failure, 4 verify failure, 126 blocked command, 130 interrupted. Other non-zero codes propagate from the agent.

Tunables

Variable Default Effect
SANDBOX_MEM 4g --memory for the container
SANDBOX_CPUS 2 --cpus
SANDBOX_PIDS 512 --pids-limit
SANDBOX_NOFILE 4096 --ulimit nofile=…
SANDBOX_TMP_SIZE 512m Size of the /tmp tmpfs inside the container
SANDBOX_NO_NET unset If 1, runs --network=none even for agent modes
SANDBOX_IMAGE ai-sandbox:latest Image tag to run / build
SANDBOX_PKG_LAG_DAYS 14 Supply-chain cooldown window in days; 0 disables
SANDBOX_ALLOW_FLOATING_BASE unset If 1, allows unlagged base image when Hub API fails
SANDBOX_GIT_NAME host git config user.name Sets user.name in /etc/gitconfig
SANDBOX_GIT_EMAIL host git config user.email Sets user.email in /etc/gitconfig
SANDBOX_LOCK unset Path to lockfile; if set, build resolves from it

shell/bash mode runs --network=none automatically.

Bumping or lowering SANDBOX_PKG_LAG_DAYS only takes effect after a rebuild; the value is also forwarded into the running container as SANDBOX_PKG_LAG_DATE and npm_config_before, so npm/npx performed by the agent at runtime honors the same cutoff and cannot override --before.


What the agent CAN do

  • Anything inside /workspace: write, rewrite, delete, restructure. Back up anything irreplaceable.
  • Write to the selected tool's persistent podman volumes in agent modes. This is how SSO/device-auth state survives, and it also means compromised code can corrupt or delete that agent state. sandbox shell uses an ephemeral home instead.
  • Local git operations: commit, branch, merge, rebase, tag, git log, etc. The image sets the default commit identity from SANDBOX_GIT_NAME and SANDBOX_GIT_EMAIL (read from the host's git config at build time) in /etc/gitconfig, so all agents use that identity unless a workspace overrides it locally.
  • Install packages into /workspace, /tmp, or tool cache directories. In agent modes, cache directories under $HOME live in the tool's persistent podman home volume; sandbox shell uses an ephemeral home.
  • Talk to its LLM provider via the agent's own HTTP client (not via shell curl, which is denylisted for LLM/forge hosts).
  • Reach package registries (npm/crates.io/PyPI/etc.) for language toolchains — this is intentionally allowed so agents can install project dependencies. If you want a tighter posture, run SANDBOX_NO_NET=1 or add those hosts to the denylists. pip install/pip3 install and python -m pip install forms are denied at the agent layer, but uv is the recommended tool for Python package management — uv sync and uv pip install are not blocked. When a workspace has a .venv/ directory, the sandbox automatically creates a Linux-native .venv_linux/ mirror on startup (macOS only) and mounts it over /workspace/.venv inside the container. Agents can use the conventional .venv path, but writes go to .venv_linux/ and never mutate the host's macOS virtualenv.

What the agent CANNOT do

  • git push / git remote add|set-url|rename|remove|rm / git send-email / git config --global|--system / gh/glab/hub — blocked at the OS level (including bypass attempts via -C, -c, --git-dir=…, --work-tree=…), and unable to authenticate anyway. Read-only git remote / git remote -v and git config --local are allowed.
  • Read your SSH keys, host ~/.gitconfig, ~/.aws, ~/.config/gh — none are mounted.
  • Write outside /workspace on the host. Root FS is --read-only; the only writable paths are the workspace bind mount, the selected agent's podman auth/config volumes, and a few small tmpfs.
  • sudo / su / install host software / docker / podman / kubectl.
  • Survive its own session: on exit, the container is gone; only the workspace and the per-agent named volumes persist. Each agent gets a sandbox-<tool>-home volume for its $HOME (so files like Claude's ~/.claude.json and Codex's ~/.codex sibling state are kept) plus dedicated config volumes (e.g. sandbox-claude-config) for the tool's primary config dir. SSO / device-code logins land in those volumes and survive across runs without touching the workspace.

Parity between the three agents

All three configs enforce the same logical denylist. The canonical source of truth is config/deny_dangerous.py. The other three agent configs and the shell wrappers' rule data are generated from it by config/generate_configs.py, which runs:

  • at image build time (so the configs in the image always match the canonical),
  • and as the first step of sandbox verify (which fails the run if the generated outputs differ from the on-disk versions, catching accidental hand edits to the generated files).

To change a rule, edit deny_dangerous.py and run python config/generate_configs.py. Do not edit claude-config/settings.json, codex-config/config.toml, or opencode-config/opencode.json by hand — those edits will be overwritten and will fail the freshness check.

The generated configs also carry small status-line UX settings. Claude Code's generated settings point statusLine at ~/.claude/statusline.sh, which is mounted from config/claude-config/statusline.sh and prints model, usage, and cost when Claude provides those fields. Codex's generated config sets [tui].status_line to show model with reasoning, context remaining, 5-hour usage, weekly usage, and the current directory, with Codex's theme-derived status-line colors disabled.

Categories enforced by the canonical denylist:

Category Example rule
Remote git mutation git push, git remote set-url, git send-email, git svn
Forge CLIs gh, glab, hub
Direct LLM / forge HTTP curl/wget/http/httpie → anthropic/openai/openrouter/github/gitlab/bitbucket
Remote shell & file transfer ssh, scp, sftp, rsync, nc, socat
Package publishing / Python installs npm/pnpm/yarn publish, pip install/upload, python -m pip install/upload, twine, cargo publish, gem push
Cloud CLIs aws, gcloud, az, doctl, fly, heroku
Container / orchestration docker, podman, kubectl, helm, terraform apply/destroy
Privilege escalation sudo, su, doas
Dangerous FS outside workspace rm -rf /…, mkfs, dd

Auditing a session

After a session, audit the workspace diff:

git diff HEAD            # what did the agent change?
git log HEAD --since=...
sandbox doctor           # leaky env vars on the host? unexpected remotes?

If you need a hard command-level audit trail, wrap the agent invocation in script(1) on the host (script -q ~/sandbox.log sandbox claude), or run sandbox shell with set -o xtrace in ~/.bashrc.


Verifying the sandbox holds

sandbox verify

This runs four phases:

  1. Generator freshness check. Re-runs generate_configs.py and diffs its output against the on-disk claude-config/settings.json, codex-config/config.toml, opencode-config/opencode.json, and shell wrapper rule tables. Any mismatch fails immediately.
  2. Image freshness check. If the running image's build hash (Containerfile, launcher, generated config files, wrappers, and verify suite, computed deterministically) does not match the on-disk source tree, rebuild before continuing.
  3. Escape-attempt suite. Each case in verify/escape_attempts.yaml is run inside the container; each must exit non-zero and emit a stderr line matching ^ai-sandbox: blocked: . Cases include plain forms (git push, git remote add, gh repo list, ssh localhost, writing to /etc, executing from /tmp//run, sudo) and wrapper-bypass forms (git -C /tmp push, git -c x=y push, git --git-dir=/tmp push, /usr/bin/git push, /usr/bin/git.real push, env git push, git -c x=y config --global, npm/npx --before overrides, and nested .git/config/hooks writes).
  4. Threat-model assertions. Each case in verify/threat_model_assertions.yaml documents an expected residual risk and confirms it still behaves as documented (persistent agent homes are writable, forwarded provider env vars are visible to same-uid subprocesses, OpenCode's dedicated TMPDIR is executable by design, normal agent mode has outbound HTTPS).

sandbox verify exits non-zero if any escape attempt succeeds, any documented assertion fails, or the freshness check finds drift. Run it after Containerfile, launcher, config, wrapper, or verify-suite changes; if the existing image predates those inputs (build hash mismatch), verify rebuilds before running the runtime suite.


Reproducibility

SANDBOX_PKG_LAG_DATE is recomputed every sandbox build, so two builds on different days produce different (but each independently valid) images. For audit-grade reproducibility:

sandbox lock        # writes sandbox.lock.json pinning:
                    #   - resolved base image digest
                    #   - snapshot.debian.org timestamp
                    #   - npm_config_before date
                    #   - opencode version + asset SHA-256
                    #   - uv version
SANDBOX_LOCK=sandbox.lock.json sandbox build

Builds against the same lockfile produce byte-identical layer digests for everything the cooldown controls, modulo timestamps in the npm cache (which is purged before image finalisation). sandbox lock is intended to be checked into the repo if you need a frozen, auditable build artifact.


Known limitations & open considerations

  • Supply-chain cooldown coverage. Build-time: base image, apt packages, npm packages (incl. transitives), opencode release, uv release — all five lagged. Runtime: npm/npx are enforced at the OS-wrapper level (sets npm_config_before from SANDBOX_PKG_LAG_DATE and rejects --before overrides; stale npm.real/npx.real names are removed). pnpm, bun, uv, and pip are covered at the config-layer via per-user config files written 0444 at image build time — agents can bypass them with per-invocation flags but cannot silently widen the window by editing the files. yarn, cargo install, go install, and gem install have no cooldown enforcement at either layer. pip install and python -m pip install are additionally denylisted at the agent layer; uv pip install and uv sync are not (uv is the intended Python package manager). For a tighter posture, run SANDBOX_NO_NET=1 so the agent cannot reach any registry, or pre-stage the deps you trust into the workspace before launching.
  • Cooldown bypass for very-new packages. A malicious release that survives more than SANDBOX_PKG_LAG_DAYS (default 14) without being yanked will be pulled. The lag is a probability play, not a guarantee. Increase the value if you're willing to trade currency for more soak time.
  • snapshot.debian.org reliability. The Debian snapshot archive is community-hosted and occasionally slow or temporarily unavailable. A build that hits a snapshot outage will fail at apt-get update — retry, lower SANDBOX_PKG_LAG_DAYS to a date Debian has cached, or set it to 0 to bypass the snapshot entirely.
  • Docker Hub API rate limits. Resolving the lagged base image makes one anonymous call to hub.docker.com/v2/... per sandbox build. If the call fails (rate limit, network), lagged builds fail closed. Set SANDBOX_ALLOW_FLOATING_BASE=1 only when you intentionally accept the unlagged Containerfile default.
  • Network egress and provider credentials. Agent sessions need network access to reach their model provider, and the chosen provider credential is available to that agent process and its subprocesses. Shell-pattern denylists block common curl/wget/http calls to LLM and forge hosts, but Python, Node, and other runtime HTTP clients are not a network firewall. Use SANDBOX_NO_NET=1 for offline work, or put podman behind an external filtering proxy/firewall when you need allowlisted egress.
  • Persistent agent state. Claude, Codex, and OpenCode use writable podman named volumes for $HOME plus nested config/state directories so account login and onboarding state survive across sessions. That state is in scope for code running as the agent user: it can be read, corrupted, deleted, or exfiltrated if networking is enabled. Wipe a tool's volumes with podman volume rm commands from the setup section when you want a clean identity.
  • OpenCode executable temp directory. /tmp and /run are mounted noexec, but OpenCode gets /tmp-opencode with exec because its TUI runtime loads an extracted native library from TMPDIR. Keep this mount small and tool-specific.
  • Worktrees, submodules, and .git symlinks. The launcher refuses to start when any .git under the workspace is a symlink or gitfile. Normal .git directories, including nested repositories, get validated read-only mounts for config and hooks.
  • Package-registry exfiltration. The agent can publish packages only if publishing CLIs are allowed and credentials are present. Publishing CLIs are denylisted and no tokens are forwarded, so this is blocked in practice.
  • Self-supply-chain risk. Code the agent writes is only dangerous when you run it outside the sandbox. Review diffs before running new entry points (makefiles, scripts, CI configs).
  • Symlinks in /workspace pointing outside. A bind mount follows symlinks only inside the container — if /workspace/x is a symlink to /workspace/../other, it resolves inside the container, which means /other on the container (not writable; root is read-only). If you keep host symlinks inside your project that point to sibling directories, the agent cannot follow them, because only $PWD is mounted. Don't run sandbox with $PWD set to a directory whose parent you need protected.
  • Multi-repo workflows. Only one directory is mounted per session. For multi-repo work, use a parent directory that contains all repos and accept that the agent can modify any of them. A more isolated pattern is one sandbox session per repo.
  • macOS VM resources. --memory / --cpus are enforced inside the Linux VM that podman machine provisions; if the VM itself is starved, the limits are moot. Pass generous --cpus / --memory to podman machine init, or podman machine set an existing VM.
  • gVisor. Optional stronger isolation layer (--runtime=runsc), compatible with this setup. Not required for the threat model above. podman's rootless default already provides one of the bigger isolation wins gVisor used to be reached for under rootful docker.

FAQ

The agent says it pushed my code. Did it? No. Without credentials, authentication fails; also, the OS-level git wrapper rejects push outright. Verify with git log origin/main..HEAD from your host shell.

Why not --network=none always? Agents need network to reach their LLM provider and package registries. shell/bash mode runs --network=none automatically, and SANDBOX_NO_NET=1 forces it for any tool.

Can the agent create an SSH key and push with it? It can create a key file, but the key isn't registered against any remote account, so authentication fails. The git wrapper also refuses push before that matters.

Performance on macOS? Fine for typical source trees — podman machine uses virtiofs to surface bind mounts into the VM. Very large monorepos benefit from raising the podman machine VM's CPU/RAM with podman machine set --cpus N --memory M.

What if I edit one denylist — do I have to edit the others? No. Edit config/deny_dangerous.py (the canonical source) and run python config/generate_configs.py. The other configs and the wrappers' rule data are generated, and sandbox verify will catch any drift.

Can I extend the denylist with my own rules? Yes — deny_dangerous.py exposes a load_extra_rules() hook that reads from config/extra_rules.yaml if present. See SPEC.md §6.4 for the schema.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors