---
slug: "sandbox-agent-santiago-afonso"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/santiago-afonso/sandbox-agent@main/README.md"
repo: "https://github.com/santiago-afonso/sandbox-agent"
source_file: "README.md"
branch: "main"
---
# sandbox-agent

A Podman wrapper that runs agent CLIs (primarily `codex`, but also `copilot`, `opencode`, and `pi`) inside a container.

For `codex`, it always uses:

- `--search` (web search tool availability)
- `--config web_search=live|cached|disabled` (defaults to `cached`, or uses host `~/.codex/config.toml` if set)

Networking is enabled (full egress).

## Quick start

### 1) Build an image

You need an image that includes `codex`, `git`, `bash`, and `uv` (plus a Python runtime; this image uses **uv-managed Python** by default, and exposes it as `python3` in the container).
Use `Containerfile`:

```bash
podman build -t localhost/sandbox-agent:latest -f Containerfile .
```

Or use the Makefile (also installs the wrapper):

```bash
make install
```

If you're on a corporate network with an npm mirror, override the registry:

```bash
make install NPM_REGISTRY=https://your-registry.example.com/
```

If TLS is intercepted (transparent proxy / self-signed in chain), pass a corporate CA file:

```bash
make install EXTRA_CA_CERT_PATH=$HOME/.local/share/machine-setup/certs/wbg-corp-ca-bundle.pem
```

The build accepts a single PEM cert, a PEM bundle, or a single DER cert.

If `EXTRA_CA_CERT_PATH` is not set, the Makefile auto-detects WBG CA material
only on the WBG laptop hostname (`PCACL-G7MKN94`) by checking:

- `bin/wbg-ca-helper corp-bundle-path` (preferred; generated local bundle), then
- `~/.local/share/machine-setup/certs/wbg-corp-ca-bundle.pem`, then
- `~/wbg-cloud-root-ca.pem` or `../../wbg-cloud-root-ca.pem`, then
- `~/wbg_root_ca_g2.cer` or `../../wbg_root_ca_g2.cer` as a legacy fallback.

These cert files are **not** stored in the repo; they are local machine files and should not be committed.

You can also override bundled tool versions:

```bash
make install CODEX_NPM_PKG=@openai/codex@latest OPENCODE_VERSION=latest \
  MQ_VERSION=0.5.9 MQ_TARGET=x86_64-unknown-linux-gnu \
  TYPST_VERSION=0.14.2 TYPST_TARGET=x86_64-unknown-linux-musl
```

`mq` is installed from its GitHub release assets and verified against the
upstream `checksums.txt`. If you override `MQ_TARGET`, make sure the selected
`MQ_VERSION` publishes a matching release artifact.

Override the default uv-managed Python version:

```bash
make install UV_DEFAULT_PYTHON=3.14
```

You can also override the bundled uv binary version (rarely needed):

```bash
make install UV_VERSION=0.9.21
```

If you want a smaller build (skip Playwright’s bundled browser download), set:

```bash
make install INSTALL_PLAYWRIGHT_BROWSERS=0
```

### 2) Install the wrapper

```bash
install -m 0755 ./sandbox-agent ~/.local/bin/sandbox-agent
install -m 0755 ./sandbox-agent-codex ~/.local/bin/sandbox-agent-codex
install -m 0755 ./sandbox-agent-copilot ~/.local/bin/sandbox-agent-copilot
install -m 0755 ./sandbox-agent-opencode ~/.local/bin/sandbox-agent-opencode
install -m 0755 ./sandbox-agent-pi ~/.local/bin/sandbox-agent-pi
```

### 3) (Optional) Configure mounts

Create `~/.config/sandbox-agent/config.sh`:

```bash
CODEX_CONTAINER_SANDBOX_IMAGE="localhost/sandbox-agent:latest"

# Optional: force an OCI runtime (useful on some WSL/work setups).
# CODEX_CONTAINER_SANDBOX_PODMAN_RUNTIME="runc"

# Optional: override DNS servers used inside the container.
# Default behavior is to inherit host/Podman DNS.
# Enable override explicitly:
# CODEX_CONTAINER_SANDBOX_DISABLE_DNS_OVERRIDE=0
# Or override the server list (in order):
# CODEX_CONTAINER_SANDBOX_DNS_SERVERS=(45.90.28.212 45.90.30.212 1.1.1.1 8.8.8.8)
#
# Note: When DNS override is enabled, the wrapper bind-mounts a generated
# /etc/resolv.conf into the container to bypass Podman DNS forwarders (often a
# 169.254.x.x hop).

# Mount helper tools read-only (mapped under /home/codex/...)
CODEX_CONTAINER_SANDBOX_RO_MOUNTS=(
  "$HOME/.local/bin"
  "$HOME/bin"
)

# Persist caches if needed
CODEX_CONTAINER_SANDBOX_RW_MOUNTS=(
  "$HOME/.cache/uv"
  "$HOME/tmp"
)

# Optional: add host env vars to pass through into the container.
# Defaults already include common LLM providers + AWS Bedrock credentials.
# CODEX_CONTAINER_SANDBOX_ENV_PASSTHROUGH+=(MY_EXTRA_API_KEY)
#
# Optional: disable default passthrough list, then define an explicit allowlist.
# CODEX_CONTAINER_SANDBOX_DISABLE_DEFAULT_ENV_PASSTHROUGH=1
# CODEX_CONTAINER_SANDBOX_ENV_PASSTHROUGH=(
#   GEMINI_API_KEY
#   GOOGLE_API_KEY
# )
#
# Optional: override/disable host OpenCode agents mount.
# CODEX_CONTAINER_SANDBOX_OPENCODE_AGENTS_DIR="$HOME/.config/opencode/agents"
# CODEX_CONTAINER_SANDBOX_DISABLE_OPENCODE_AGENTS_MOUNT=1
```

## Usage

### Default: zsh shell in container

```bash
sandbox-agent
```

### Codex (full network)

```bash
sandbox-agent codex exec "Summarize the repo"
```

### OpenCode (full network)

```bash
sandbox-agent opencode agent list
```

### Instruction-source dry run (recommended)

Prints effective global/project AGENTS sources and mount targets without launching the container:

```bash
sandbox-agent --dry-run-instructions codex exec "noop"
```

### Project AGENTS modes for `codex exec`

For `sandbox-agent codex exec`, project AGENTS behavior is explicit and mode-driven:

- `suppress` (default): do not mount project `AGENTS.md`
- `repo`: mount repo-root `AGENTS.md`
- `replace`: mount a custom project AGENTS file
- `concat`: compose `custom + repo` (custom first), then mount combined file

Examples:

```bash
# Use repo AGENTS.md
sandbox-agent --project-agents-mode repo codex exec "..."

# Replace project AGENTS.md with a custom file
sandbox-agent --project-agents-mode replace --project-agents-file ~/my-project-agents.md codex exec "..."

# Concatenate custom + repo AGENTS
sandbox-agent --project-agents-mode concat --project-agents-file ~/.codex/AGENTS.md codex exec "..."
```

You can also pin an explicit global AGENTS file for codex exec:

```bash
sandbox-agent --global-agents-file ~/machine-setup/tools/sandbox-agent/SANDBOXED-AGENT-AGENTS.md codex exec "..."
```

Single preset to combine sandbox global AGENTS + host `~/.codex/AGENTS.md` (as project concat):

```bash
sandbox-agent --instruction-profile host-plus-sandbox codex exec "..."
```

### Pi (pi-mono coding agent)

```bash
sandbox-agent pi
```

`pi` is available in the image, but sandbox-agent does not auto-install pi
plugins/packages. Install only what you need in your `~/.pi/` state.

### Makefile helpers

From the repo root:

```bash
make image
make selftest
make selftest-instructions
make pii-scan
make validate-docs
```

### Self-test (network + mount isolation)

Runs seven checks:

1. Container has internet connectivity.
2. Playwright + Chromium are usable.
3. Codex + OpenCode CLIs are available in-container.
4. Host files outside the workspace are not visible by default.
5. An explicitly mounted host directory is readable and writable (RW mount).
6. OpenCode agents mount wiring is visible in dry-run, mounted read-only when a host source exists, and `--rw` overlaps with `~/.agents` are rejected.
7. Instruction-source flag matrix passes (`scripts/test_instruction_flags.sh`).

```bash
./selftest.sh
```

### Shell inside container

```bash
sandbox-agent --shell
```

### Debug the podman command

```bash
CODEX_CONTAINER_SANDBOX_DEBUG=1 sandbox-agent codex exec "hello"
```

### Hide the printed `codex ...` command

By default the wrapper prints the computed `codex ...` command (to stderr) before starting.
Disable with:

```bash
sandbox-agent --no-print-codex-cmd codex exec "hello"
```

### Git commit identity

To avoid `git commit` failing with “Author identity unknown”, the wrapper will (by default)
copy `user.name` / `user.email` from the host git config into a persistent container-global
gitconfig under `CODEX_HOME`, and set `GIT_CONFIG_GLOBAL` accordingly.

Override (or set explicitly if host identity is not set):

```bash
CODEX_CONTAINER_SANDBOX_GIT_NAME="Your Name" \
CODEX_CONTAINER_SANDBOX_GIT_EMAIL="you@domain" \
  sandbox-agent ...
```

Disable:

```bash
CODEX_CONTAINER_SANDBOX_DISABLE_GIT_IDENTITY_SYNC=1 sandbox-agent ...
```

## Mount behavior

- If you run inside a git repo, the **repo root** is mounted read-write.
- The container working directory is set to your original `$PWD` inside that mount.
- For `codex exec`, global AGENTS is mounted from canonical `SANDBOXED-AGENT-AGENTS.md` to container `~/.codex/AGENTS.md` (or from `--global-agents-file` when provided).
- Extra mounts under `$HOME` are mapped to the same relative path under `/home/codex`.
- `XDG_CACHE_HOME` is set to `$CODEX_HOME/cache` so tools like `uv` have a writable cache by default.
- If host OpenCode agents are discoverable, they are mounted read-only to `~/.config/opencode/agents`.
- Pi state in the container lives under `~/.pi/`, backed by a wrapper-managed host directory: `~/.local/state/sandbox-agent/pi` (disable with `CODEX_CONTAINER_SANDBOX_DISABLE_PI_MOUNT=1`).
- If host `~/.pi/agent` exists, it is mounted read-only into the container at `~/.pi-host/agent` so you can reuse host extensions/prompts without allowing in-container mutation (disable with `CODEX_CONTAINER_SANDBOX_DISABLE_PI_HOST_AGENT_MOUNT=1`).
- sandbox-agent does not seed pi harness plugins into `~/.pi/agent/settings.json`.
- Any mounts derived from `~/.agents` are read-only by design.
- If `~/.agents` resolves into the mounted workspace tree, sandbox-agent adds a read-only submount at that workspace path so the repo bind mount cannot make it writable.
- sandbox-agent refuses explicit `--rw` mounts that overlap `~/.agents`.

## Auth

Codex credentials live in `CODEX_HOME` (`~/.local/state/sandbox-agent` by default).
Login once inside the container:

```bash
sandbox-agent --shell
codex login
```

### Reuse host auth.json (optional)

If you already have a working host login (for example `~/.codex/auth.json`), you can mount it
into the container so `codex` doesn't prompt for login again:

- Auto-detects and mounts `~/.codex/auth.json` if it exists.
- Override the path with `CODEX_CONTAINER_SANDBOX_AUTH_FILE=/path/to/auth.json`.
- Disable mounting entirely with `CODEX_CONTAINER_SANDBOX_DISABLE_AUTH_MOUNT=1`.
- Control mount mode (default `ro`) with `CODEX_CONTAINER_SANDBOX_AUTH_MOUNT_MODE=ro|rw`.

### Reuse host skills and Pi customizations (optional)

To keep Codex skills consistent with your host setup, the wrapper can mount:

- `~/.agents/skills` (preferred) or `~/.codex/skills` (fallback) -> `$CODEX_HOME/skills` (read-only)

Controls:

- Override paths:
  - `CODEX_CONTAINER_SANDBOX_SKILLS_DIR=/path/to/skills`
- Disable:
  - `CODEX_CONTAINER_SANDBOX_DISABLE_SKILLS_MOUNT=1`

Pi prompt templates ride through the existing read-only `~/.pi/agent` mount. When `setup.sh`
wires `~/.pi/agent/prompts` to `.agents/generated/pi/prompts`, the container sees the same
templates at `~/.pi-host/agent/prompts` without any separate Codex prompt mount.

### Reuse host OpenCode agents (optional)

To keep OpenCode role definitions consistent with your host setup, the wrapper can mount:

- `~/.config/opencode/agents` (preferred)
- `~/.agents/subagents/generated/opencode/agents` (fallback)
- `<workspace>/.agents/subagents/generated/opencode/agents` (repo-local fallback)

All OpenCode agents mounts are read-only and target:

- `~/.config/opencode/agents` inside the container

Controls:

- Override path:
  - `CODEX_CONTAINER_SANDBOX_OPENCODE_AGENTS_DIR=/path/to/agents`
- Disable:
  - `CODEX_CONTAINER_SANDBOX_DISABLE_OPENCODE_AGENTS_MOUNT=1`

## LLM env passthrough

The wrapper forwards a default allowlist of provider credential environment
variables from host to container (only when each variable is present on host):

- `OPENAI_API_KEY`
- `ANTHROPIC_API_KEY`
- `GEMINI_API_KEY`
- `GOOGLE_API_KEY`
- `GOOGLE_GENAI_API_KEY`
- `AZURE_OPENAI_API_KEY`
- `AZURE_OPENAI_ENDPOINT`
- `ELEVENLABS_API_KEY`
- `MISTRAL_API_KEY`
- `COHERE_API_KEY`
- `GROQ_API_KEY`
- `PERPLEXITY_API_KEY`
- `TOGETHER_API_KEY`
- `FIREWORKS_API_KEY`
- `OPENROUTER_API_KEY`
- `XAI_API_KEY`
- `DEEPSEEK_API_KEY`
- `CEREBRAS_API_KEY`
- `AWS_ACCESS_KEY_ID`
- `AWS_SECRET_ACCESS_KEY`
- `AWS_SESSION_TOKEN`
- `AWS_REGION`
- `AWS_DEFAULT_REGION`
- `AWS_PROFILE`

Customization:

- Add extra vars in `~/.config/sandbox-agent/config.sh`:
  - `CODEX_CONTAINER_SANDBOX_ENV_PASSTHROUGH+=(MY_EXTRA_API_KEY)`
- Replace defaults with your own explicit list:
  - `CODEX_CONTAINER_SANDBOX_DISABLE_DEFAULT_ENV_PASSTHROUGH=1`
  - `CODEX_CONTAINER_SANDBOX_ENV_PASSTHROUGH=(VAR_A VAR_B)`

### Reuse host helper CLIs (read-pdf)

If you have `read-pdf` installed on the host at `~/.local/bin/read-pdf`, the wrapper will
mount that executable (and its helper scripts like `read_pdf_page_candidates.py`) read-only
into the container (without mounting the entire `~/.local/bin` directory) so it is available
on the container `$PATH`.

Disable with:

```bash
CODEX_CONTAINER_SANDBOX_DISABLE_LOCAL_BIN_MOUNT=1 sandbox-agent ...
```

### Built-in tools (image is self-sufficient)

The image ships with a few common “skills dependencies” so you don’t need host mounts:

- `codex`
- `opencode`
- `copilot`
- `pi`
- `imagemagick` (`convert`, `identify`) for `image-crop`
- `poppler-utils` (`pdfinfo`, `pdftoppm`) for `read-pdf --as-images`
- `markitdown` for `read-webpage-content-as-markdown` and `read-pdf --as-text-fast`
- `pandoc`
- `mq`
- `typst`
- `chromium` + `playwright` (JS/client-rendered pages)

### Reuse host CLIs (optional; for extra tools/versions)

If you install CLIs on the host via:

- `uv tool install ...` (often creates symlinks under `~/.local/bin` pointing at `~/.local/share/uv/tools/...`)

the wrapper can mount the needed host directories read-only so those tools work inside the container.

Defaults (best-effort, only when detected):

- Mount `~/.local/share/uv/tools` read-only when `ttok` is detected as a uv tool install.
- Also mount `~/.local/share/uv/python` read-only (needed for uv tool shebang interpreters) when present.
- Mount host `~/.opencode/bin/opencode` read-only when present (fallback path; in-image `/usr/local/bin/opencode` remains preferred).

Disable:

```bash
CODEX_CONTAINER_SANDBOX_DISABLE_UV_TOOLS_MOUNT=1 sandbox-agent ...
CODEX_CONTAINER_SANDBOX_DISABLE_UV_PYTHON_MOUNT=1 sandbox-agent ...
```

## Security note

This wrapper is about **filesystem isolation** (mount boundaries), not egress safety.
Because this runs full yolo with full networking, the agent can exfiltrate anything it can
read inside the container (including anything you mount, and `CODEX_HOME/auth.json`).
