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.
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.
-
Credential starvation (primary). No SSH keys, no
GITHUB_TOKEN, no credential helpers, no~/.gitconfigfrom the host. Only the single*_API_KEYfor the chosen tool is forwarded.git pushandghcannot 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. -
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 everygh/glab/hubsubcommand. 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 likegit -C /tmp pushorgit -c safe.directory=* pushare caught — not justgit pushwritten naively. The stable/usr/bin/git.realpath 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 bysandbox verify. -
Container isolation: non-root (
--user 1001),--cap-drop=ALL,--security-opt=no-new-privileges,--read-onlyrootfs, tmpfs scratch with size caps andnoexecon/tmpand/run,--memory/--cpus/--pids-limit/--ulimit. -
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. -
.git/configand.git/hooksbind-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 typegit commiton the host. Inside the containercore.hooksPath=/dev/nullin/etc/gitconfigneutralises the known bypass of pointingcore.hooksPathelsewhere in the workspace. -
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-slimwhoselast_updatedis 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 unlessSANDBOX_ALLOW_FLOATING_BASE=1is set explicitly. - APT packages:
/etc/apt/sources.listis rewritten to point atsnapshot.debian.org/archive/debian/<YYYYMMDDTHHMMSSZ>/, sogit,curl,jq,ripgrep, and the rest are resolved against Debian's date-stamped archive snapshot. - npm packages (build + runtime):
npm install --before=DATEat image build time covers@anthropic-ai/claude-code,@openai/codex, and all transitive deps. The same date is forwarded into running containers asSANDBOX_PKG_LAG_DATE/npm_config_before, and runtimenpm/npxwrappers reject user-supplied--beforeoverrides, while stale compatibility paths such as/usr/local/bin/npm.realand/usr/local/bin/npx.realare removed from the image. - opencode: not on npm — the build queries the GitHub releases API
for the newest release asset whose
published_atis on or before the cutoff, downloads the versioned Linuxtar.gzdirectly 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_timeis on or before the cutoff. The build queriespypi.org/pypi/uv/json, resolves the qualifying version from the release history, and installsuv==VERSIONwith 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 fromSANDBOX_PKG_LAG_DAYS: pnpm usesminimum-release-age(minutes) in~/.config/pnpm/rc; bun usesminimumReleaseAge(seconds) in~/.bunfig.toml; uv usesexclude-newer(RFC 3339 date) in~/.config/uv/uv.toml; pip usesuploaded-prior-to(ISO 8601 durationP<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 installis additionally denylisted at the agent layer.yarn,cargo install,go install, andgem installhave 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=0to disable all controls. The cutoff is recomputed every invocation, sosandbox buildalways picks up the newest versions inside the cooldown window. For repeatable builds across days, see Reproducibility. - Base image: the launcher queries Docker Hub's tags API for the
newest
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.
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/podmanagainst real accounts. - Exfiltration of host SSH keys,
~/.aws,~/.config/gh, env tokens. - Writing outside
/workspaceon the host. - Privilege escalation, nested docker/podman, host package installation.
- Fork bombs, memory balloons, disk-fill of the host.
- Broad executable scratch space:
/tmpstays non-exec. OpenCode gets a dedicated exec-only temp mount viaTMPDIRbecause 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_DAYSago and the community yanks/reverts it within that window — neither the build nor the agent's runtimenpm installwill see it. Claude Code, Codex, and OpenCode CLI autoupdate are disabled at runtime, so CLI upgrades only happen duringsandbox 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>-homeand 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
/workspacethat you later run outside the sandbox. Treat each session's diff like a PR from a stranger:sandbox doctor+git log/git diffafter each run. - Runtime cooldown bypass via package managers other than
npm/npx. See Known limitations.
.
├── 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
- 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 machineruns a small Linux VM (via Apple's Virtualization framework) that hosts the actual container engine; bind mounts of$PWDare surfaced into the VM over virtiofs automatically. Increase CPU/RAM if you plan to runSANDBOX_MEM>6g. - 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
- 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
- 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>-homeandsandbox-<tool>-config), survive across sessions, and are inspectable withpodman volume ls | grep sandbox-. Wipe a tool's auth withpodman volume rm sandbox-claude-home sandbox-claude-config(etc.).
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.
| 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-privilegesare all honored. The launcher passes--userns=keep-id:uid=1001,gid=1001so the in-containersandboxuser (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=runscif you need stronger isolation; compatible with these flags but not required.
cd ~/projects/my-thing # this becomes /workspace (the ONLY writable host path)
sandbox claude # or: codex / opencode / shellFirst invocation auto-builds the image (~1 minute). Rebuild at any time:
sandbox buildsandbox 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.
| 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.
- 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 shelluses an ephemeral home instead. - Local
gitoperations: commit, branch, merge, rebase, tag,git log, etc. The image sets the default commit identity fromSANDBOX_GIT_NAMEandSANDBOX_GIT_EMAIL(read from the host'sgit configat 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$HOMElive in the tool's persistent podman home volume;sandbox shelluses 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=1or add those hosts to the denylists.pip install/pip3 installandpython -m pip installforms are denied at the agent layer, butuvis the recommended tool for Python package management —uv syncanduv pip installare 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/.venvinside the container. Agents can use the conventional.venvpath, but writes go to.venv_linux/and never mutate the host's macOS virtualenv.
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-onlygit remote/git remote -vandgit config --localare allowed.- Read your SSH keys, host
~/.gitconfig,~/.aws,~/.config/gh— none are mounted. - Write outside
/workspaceon 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>-homevolume for its$HOME(so files like Claude's~/.claude.jsonand Codex's~/.codexsibling 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.
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 |
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.
sandbox verifyThis runs four phases:
- Generator freshness check. Re-runs
generate_configs.pyand diffs its output against the on-diskclaude-config/settings.json,codex-config/config.toml,opencode-config/opencode.json, and shell wrapper rule tables. Any mismatch fails immediately. - 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.
- Escape-attempt suite. Each case in
verify/escape_attempts.yamlis 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--beforeoverrides, and nested.git/config/hookswrites). - Threat-model assertions. Each case in
verify/threat_model_assertions.yamldocuments 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 dedicatedTMPDIRis 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.
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 buildBuilds 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.
- Supply-chain cooldown coverage. Build-time: base image, apt
packages, npm packages (incl. transitives), opencode release, uv
release — all five lagged. Runtime:
npm/npxare enforced at the OS-wrapper level (setsnpm_config_beforefromSANDBOX_PKG_LAG_DATEand rejects--beforeoverrides; stalenpm.real/npx.realnames are removed).pnpm,bun,uv, andpipare covered at the config-layer via per-user config files written0444at 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, andgem installhave no cooldown enforcement at either layer.pip installandpython -m pip installare additionally denylisted at the agent layer;uv pip installanduv syncare not (uv is the intended Python package manager). For a tighter posture, runSANDBOX_NO_NET=1so 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.orgreliability. The Debian snapshot archive is community-hosted and occasionally slow or temporarily unavailable. A build that hits a snapshot outage will fail atapt-get update— retry, lowerSANDBOX_PKG_LAG_DAYSto 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/...persandbox build. If the call fails (rate limit, network), lagged builds fail closed. SetSANDBOX_ALLOW_FLOATING_BASE=1only 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/httpcalls to LLM and forge hosts, but Python, Node, and other runtime HTTP clients are not a network firewall. UseSANDBOX_NO_NET=1for 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
$HOMEplus 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 withpodman volume rmcommands from the setup section when you want a clean identity. - OpenCode executable temp directory.
/tmpand/runare mountednoexec, but OpenCode gets/tmp-opencodewithexecbecause its TUI runtime loads an extracted native library fromTMPDIR. Keep this mount small and tool-specific. - Worktrees, submodules, and
.gitsymlinks. The launcher refuses to start when any.gitunder the workspace is a symlink or gitfile. Normal.gitdirectories, including nested repositories, get validated read-only mounts forconfigandhooks. - 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
/workspacepointing outside. A bind mount follows symlinks only inside the container — if/workspace/xis a symlink to/workspace/../other, it resolves inside the container, which means/otheron 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$PWDis mounted. Don't runsandboxwith$PWDset 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
sandboxsession per repo. - macOS VM resources.
--memory/--cpusare enforced inside the Linux VM thatpodman machineprovisions; if the VM itself is starved, the limits are moot. Pass generous--cpus/--memorytopodman machine init, orpodman machine setan 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.
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.