# Recipes Concrete invocations. `pi` below is whatever you aliased to `ai-sandbox`. ## A first session ```bash cd ~/Projects/myapp ai-sandbox doctor # is this machine ready? pi --allow aipi.reeseapps.com --env AI_API_TOKEN ``` With your pi config bridged, the token is found on its own, so the allowlist is the only thing you have to say: ```bash pi --allow aipi.reeseapps.com ``` Same thing, but remembered by the repo (so teammates review the policy, not just your shell history): ```bash mkdir -p .ai-sandbox printf 'aipi.reeseapps.com\n' > .ai-sandbox/allow pi ``` ## Pi's own configuration The bridge rewrites your host `~/.pi/agent/{models,settings}.json` instead of copying it: providers, models, thinking budgets and preferences come along, the credential becomes `"$AI_API_TOKEN"`, and anything that only works on the host (a `!command` header, a path into `~`) is dropped with a warning. `auth.json` is never bridged. ```bash # the everyday case: same provider as the host, token from the environment pi --allow aipi.reeseapps.com # a model your gateway serves, chosen per run pi --allow aipi.reeseapps.com --model juggernaut # keep the model's endpoint pinned by IP as well, on a LAN inference host pi --allow aipi.reeseapps.com --allow 10.20.0.7:11434 # a different variable name for the credential pi --allow aipi.reeseapps.com --pi-token-env GW_TOKEN # do not hand the token over automatically: the run then fails closed unless you # pass it yourself, which is what you want in shared scripts pi --allow aipi.reeseapps.com --env AI_API_TOKEN --no-pi-token-env # no bridge: pi starts with its own defaults inside the volume (and no thinking policy) pi --no-pi-config --allow aipi.reeseapps.com ``` `models.json` is kept in step with the host unless you edit it in the sandbox, where your edit then wins; deleting it inside the sandbox re-seeds it. `settings.json` is merged per key, so a preference you changed in the sandbox stays changed and every one you did not touch follows the host again. ### How hard it thinks `defaultThinkingLevel` is policy, not preference: `xhigh` unless you say otherwise, and re-applied every run rather than remembered from whatever the last sandbox set. ```bash # the everyday case: xhigh pi --allow aipi.reeseapps.com # one-off: the top of pi's range, or no reasoning at all for a mechanical job pi --thinking max --allow aipi.reeseapps.com pi --thinking off --net allow --allow registry.npmjs.org -- npm ci # let settings.json decide, the way it did before this was policy pi --thinking host --allow aipi.reeseapps.com # per repo, chosen by .ai-sandbox/config # thinking = high ``` Nothing is written for `codex`, `claude` or a command after `--`: those read their own configuration and ai-sandbox does not touch it. ## Your skills and standing instructions `~/.agents` is shared read-only at `/home/sandbox/.agents` by default, so the sandboxed harness finds the same skills, prompt templates and `AGENTS.md` you use on the host. A project's own `.agents/` needs nothing: it arrives with the project mount, and harnesses that read both see the project's alongside yours. ```bash # the everyday case: your skills are simply there pi --allow aipi.reeseapps.com # a run where the harness should not have them: reviewing untrusted code, or a # skills set that mentions internal hosts you would rather it not repeat back pi --net none --no-agents -- bash -lc 'make lint' # a curated set for sandboxes -- e.g. host skills stay yours, this dir is what you share pi --agents-dir ~/.config/ai-sandbox/agents --allow aipi.reeseapps.com # two sets, per trust level, chosen by config file in a repo # .ai-sandbox/config: agents_dir = ./.agents-shared ``` It is read-only and there is no flag to change that: a skill is an instruction you follow on the host later, so letting a sandbox write one would let it persist past the run. If a workflow really needs to *author* skills, do it in the project directory and move the result into `~/.agents` yourself, where you can read it first. ## Per harness ```bash # pi (baked into the image, config bridged) pi --allow aipi.reeseapps.com # pi with a model chosen on the command line pi --harness 'pi --model turbo' --allow aipi.reeseapps.com # a gateway that speaks a different protocol still needs only its host pi --allow gateway.internal:8443 --env AI_API_TOKEN # OpenAI Codex CLI / Claude Code: they read their own config files, so mount or # generate what they need and allow their endpoints; the pi bridge does not apply pi --allow chatgpt.com --env CODEX_API_KEY pi --allow api.anthropic.com --env ANTHROPIC_API_KEY # just a shell in the sandbox pi --shell # any other build/test command, no network needed pi --net none -- make test ``` ## Per ecosystem Only the endpoints the tool actually needs. Everything else stays blocked, so a typosquatted package from an unexpected host fails loudly instead of quietly working. ```bash # npm / node pi --allow registry.npmjs.org --allow nodejs.org --allow github.com # python: pypi needs both of these (the CDN that serves the files is separate) pi --allow pypi.org --allow files.pythonhosted.org # rust pi --allow crates.io --allow static.crates.io --allow index.crates.io # go pi --allow proxy.golang.org --allow sum.golang.org # docker/podman images from within a build pi --allow registry-1.docker.io --allow auth.docker.io --allow production.cloudflare.docker.com # a project that also needs its dev database (non-HTTP: pinned IP + port) pi --allow aipi.reeseapps.com --allow 10.20.0.5:5432 ``` ## Git ```bash # fetch/push over https with a token in the environment pi --allow github.com --env GH_TOKEN -- git push origin main # ssh: the key has to be mounted deliberately (an explicit, reviewable grant) pi --allow github.com --mount ~/.ssh:ro -- git push origin main # read-only view of a sibling repo for context pi --allow aipi.reeseapps.com --mount ../docs:/docs:ro ``` ## Tightening a run ```bash # nothing persistent, nothing readable, nothing writable outside the repo pi --net none --ephemeral-home --read-only -- make build # an entirely blank sandbox for untrusted code: no project, no home, no skills pi --net none --no-cwd --ephemeral-home --read-only --no-agents -- bash # CI-ish: refuse to start if the policy or environment is not exactly right pi --strict-args --net allow --allow-file .ai-sandbox/allow --env-file .ci/env -- npm ci ``` ## Long-lived gate shared by several sandboxes ```bash ai-sandbox gate up --allow aipi.reeseapps.com --allow registry.npmjs.org # name=aisb-gate # port=18081 # proxy_env=export HTTPS_PROXY=http://100.64.0.1:18081 ai-sandbox --proxy 100.64.0.1:18081 --allow aipi.reeseapps.com --shell ai-sandbox gate logs # what did it allow/deny? ai-sandbox gate down ``` ## Debugging ```bash ai-sandbox --dry-run --allow example.com --env KEY # the exact podman invocation ai-sandbox --print-policy # effective policy, no secrets ai-sandbox --print-entrypoint | less # the enforcement script ai-sandbox --keep --net none -- false # leave the container for forensics ai-sandbox ps AISB_GATE_KEEP=1 pi --allow example.com --shell # keep the gate after exit ai-sandbox clean --all # "the ruleset would not install" (exit 77): check the host podman info | grep -i rootless grep "$(id -un)" /etc/subuid ``` ## Making it the default for a project `.ai-sandbox/config`: ```ini net = allow harness = pi cpus = 8 memory = 8g mount = ../specs:/specs:ro env = AI_API_TOKEN ``` `.ai-sandbox/allow`: ``` aipi.reeseapps.com registry.npmjs.org ``` Then `pi` alone in that directory is the whole invocation — and because both files are in the repository, the sandbox policy gets reviewed with the code. ## An unattended pipeline: phases in the sandbox, pushes on the host The nightly case: `.agents/phases/todo/` full of task files, nobody watching, an agent that must not be able to reach the git remote or touch the host. ```bash # ~/.bashrc.d/ai-sandbox.bash -- the launcher, the way you already write it sandbox-pi () { export AI_API_TOKEN=$(pass aipi/dev-token) ai-sandbox --allow aipi.reeseapps.com --allow github.com \ --allow pi.dev --allow release-assets.githubusercontent.com \ --env AI_API_TOKEN } ``` ```bash cd ~/Projects/foo ai-sandbox-autophase --status # what it resolved, what is pending, the git plan ai-sandbox-autophase --dry-run # the exact child and gate commands, nothing run ai-sandbox-autophase # every pending task, in pipeline order ``` What it does with that function is the interesting part. A script cannot see an interactive shell's functions, so it reads the rc files, takes the *definition*, and starts each child as: ``` sandbox-pi --no-tty --no-interactive -- pi --no-approve --session-dir .agents/phase-sessions --name --mode json ``` `pass` runs again per unit, so a run that starts at 23:00 is not holding a token fetched at 09:00. Reading the definition is all the probe does: `--status` and `--dry-run` never call the launcher, fetch a token, build an image or start a container. | you want | flag | |---|---| | a different launcher | `--pi-cmd sandbox-claude`, `--pi-cmd pi`, or a whole command line | | the function somewhere else | `--rc-files ~/.bashrc.d/zz-ai.bash` | | fewer flags of its own | `--sandbox-args ''` (the defaults are `--no-tty --no-interactive`) | | the gate on the host, because the toolchain lives there | `--validate-in host` (warns: the gate file is in the writable mount) | | one task, one phase, a short run | `--unit 03_api/02_routes.md`, `--phase 03_api`, `--max-units 4` | | commits that stay local while you watch | `--no-push` | | no warm-up container (it costs a build the first time) | `--no-preflight` | Where the work goes when it finishes: the executor agents never commit; the driver commits each completed phase on the host and pushes it, checks again before starting the next phase, and pushes whatever is left at the end of the run. A failed push stops the run and prints the command that finishes it — the commit is never lost, and the next run's push sweeps it up: ``` [2026-10-28 02:14:07] pushing 1 unpushed commit(s) on main (between 03_api and 04_docs) (pushed: 9f1c2ab phase: 03_api) ``` Inside the sandbox the agent has the repository but not the remote, and it is told so in its own prompt (`--no-notes` to drop that block) — `git status`, `git diff`, `git log` work; `git push`, `fetch`, `pull` and `clone ` are not attempted, so no fixer round is spent rediscovering that. That is also why the gate runs in the sandbox: `.agents/validate.sh` is inside the writable mount, and a jailer the prisoner can edit is not a jailer. One consequence of committing out here is worth naming: a repository is not only what git records, it is what git **runs** — `.git/config`, and `.git/hooks/` especially. So the driver's git calls run with an empty hooks path, no pager, no system config and no `ext::` transport, and the run refuses to deliver if a sandboxed unit changed either: ``` ✗ ERROR: refusing to deliver (commit/push) -- the sandbox changed this repository's own git configuration. look at it by hand: git config --local --list; ls -l .git/hooks if the change was yours: re-run with --trust-git-changes ``` The commits and the work stay exactly where they are; nothing is pushed and no hook runs either way. A project whose tests need something the image lacks gets it from the launcher: ```bash sandbox-pi () { export AI_API_TOKEN=$(pass aipi/dev-token) ai-sandbox --allow aipi.reeseapps.com --allow pypi.org --allow files.pythonhosted.org \ --install 'python3-pip ripgrep' --env AI_API_TOKEN } ``` `doctor`, `inspect`, `clean` and the rest of ai-sandbox work unchanged underneath it — `--status` prints the policy the launcher will apply, without applying it.