aisan runs a coding agent with everything in the box: the harness, its state, and your repo. Nothing else. The box has no network route and holds no credential. Model calls still work: each harness gets a host-side proxy that checks requests against an allowlist and attaches the real credential to traffic the box never sees.
python -m pip install aisan # or: uv tool install aisan
aisan claude /path/to/repoThat is a normal interactive Claude Code session (aisan codex and
aisan opencode work the same way), with three differences:
- Zero credentials in the box.
~/.claude/.credentials.jsonis never mounted. The token the client sees is a per-box placeholder; the proxy drops it and attaches the host's real credential: the subscription login by default, or a static API key with--api-key. A test asserts from inside a real box that the credential file does not exist. - Zero network by default. The box gets its own network namespace with no
route off the machine. The one egress is a loopback relay to the model
proxy over a Unix socket.
--netopts back into host networking when a task needs it; credential files stay unmounted and model calls still pass through the authenticated proxy. - Selected filesystem slices. The repo is bound rw at its real absolute
path, system directories ro (
/usr,/etc; fresh/procand/dev), a tmpfs over$HOMEand/tmp, and nothing else unless a bind spec names it. Local stdio MCP servers declared on the host are started inside the box, where they inherit its filesystem, cleared environment, and network namespace; remote MCP declarations and their authentication state stay on the host.
The same three commands, run on the host and then from inside the box:
Every launcher takes --explain: it prints the resolved profile and the
exact Bubblewrap argv from the same Box object used to launch, then exits.
Trimmed:
Text version
$ aisan claude /path/to/repo --explain
== inputs ==
harness claude-code
repo /path/to/repo
network own namespace (no route off the machine)
== egress backends (host half on a socket, in-box on loopback) ==
anthropic 127.0.0.1:8713 -> /tmp/aisan-proxy-59d1d1bc/anthropic.sock
== tmpfs mounts (mounted before binds; intended writable scratch) ==
[ 39] /tmp (2147483648)
[ 43] /home/user (1073741824 <- $HOME)
== binds in argv order (later shadows earlier on overlap) ==
system /usr /bin /lib /lib64 /sbin /etc /proc /dev
[ 45] rw-root /path/to/repo
[ 63] rw /home/user/.cache/aisan-claude/aisan-4475d1c31168
[ 66] ro /home/user/.config/git/config
[ 69] ro /tmp/aisan-proxy-59d1d1bc
== environment (the box's complete environment; --clearenv first) ==
CLAUDE_CONFIG_DIR=/path/to/repo/.aisan-claude-state
GIT_PAGER=cat
HOME=/home/user
PATH=/usr/bin
...
Presets cover the harness; --binds FILE (repeatable, TOML) covers your
project. The keys are ro, rw, overlay, and path entries prepended to
the box PATH:
ro = ["~/depot_tools"]
overlay = ["~/.cache/vpython-root.1000"]
path = ["~/depot_tools"]The path key grants nothing on its own: every entry must be covered by a
mount the same file names. include pulls in other spec files, expanded in
place and before the including file's own keys, so a growing collection
composes in an order the files state rather than one the command line
implies. examples/depot_tools.toml is a worked
example with the reasoning written down.
The same mechanism drives headless workloads. A preset is a pure
args -> BoxSpec function; Box compiles the spec, starts the backends, and
returns argv. The Vertex backend mints short-lived tokens host-side through
ADC impersonation, so a batch job's box carries no Google credential either.
This package was extracted from an autonomous patch pipeline that runs
model-driven build/test jobs against V8 worktrees; that pipeline remains its
first consumer.
The REAPI transport is the largest specialized core component. Remote build
clients such as siso can speak plaintext HTTP/2 to a local endpoint while the
real bearer stays on the host. It checks :authority and :path together,
injects credentials per HTTP/2 stream, and refuses in gRPC's own terms so a
policy decision is not mistaken for a retryable network failure.
BoxSpecis frozen, non-defaulting data. A reviewer can read a call site and see what is mounted without simulating default resolution.Limitsis the exception: an unset resource cap is not an unstated mount.- One ordered bind list, later wins, matching Bubblewrap's mount behavior.
Bind,Seal,Overlay, andBindOverread top to bottom. - Credential-aware egress in both network modes. Isolated boxes reach host
proxies through Unix sockets and in-box loopback relays. Interactive boxes
started with
--netreach authenticated host-loopback TCP listeners directly; a private runtime file supplies the per-session proxy token without placing it in process arguments. - Fail-closed request policy. A policy exception denies the request. Refusal messages name the policy reason rather than an internal callback.
- Presets as pure
args -> BoxSpecfunctions, rather than project switches hidden inside the sandbox compiler.
The model- and client-neutral core is roughly 3,100 lines of Python. That count
covers BoxSpec, the sandbox compiler, git bind policy, lifecycle and launcher,
inspection, the backend interface, relay, fail-closed policy, and the REAPI
transport. Provider/client adapters, presets, interactive session launchers,
and MCP importers are integrations outside that core count. The number is an
audit bound, not a comparison with another project's total source size.
- Linux, Python 3.12 or newer, and
bubblewrap(bwrap). User namespaces must be available to the invoking user; some distributions restrict unprivileged user namespaces by default. systemd-run --useris optional. Cgroup limits are skipped when the command is absent. On a host without a usable user manager, disable them explicitly withLimits(use_cgroup=False).- Interactive sessions require the corresponding host CLI (
claude,codex, oropencode) to be installed and already logged in. - RBE/V8 use additionally requires the relevant siso/depot_tools environment
and
luci-auth. - Vertex credential minting requires the
google-authextra and Application Default Credentials.
Boxes have no general network access by default. Interactive claude, codex,
and opencode sessions accept --net before the literal -- to share the
host network namespace. That exposes the internet, LAN/VPN routes, and
host-local services in both directions; configured model credential files stay
unmounted and model calls still pass through authenticated host proxies.
python -m pip install aisanFor Vertex credential minting:
python -m pip install 'aisan[google-auth]'This installs one human-facing command with inspection and interactive subcommands:
aisan explain --help
aisan claude /path/to/repo
aisan codex /path/to/repo
aisan opencode /path/to/repo
aisan codex /path/to/repo --netLauncher options come before a literal --; arguments after it are passed to
the underlying client unchanged.
--grant NAME adds a named grant: the mounts, PATH entries and environment
some tree needs inside a box with no network route.
aisan claude /path/to/v8 --grant depot_toolsToday the one grant is depot_tools, which supplies the checkout found through
autoninja on your PATH, vpython's venv store as an overlay, and the two
variables that stop depot_tools reaching for a network it has not got --
without the first of them gclient exits 255 on a git fetch it cannot make,
which reads as a broken checkout. The environment is why this is a grant rather
than a bind spec: a --binds file names paths, and the value that turns off an
auto-update is not one. Grants are applied before --binds, so a user file
still shadows them, and --explain renders the result. The name is not
tool-specific on purpose -- a CA bundle and the variable naming it, or a device
node and the library path that finds it, are the same shape.
Runtime dependencies are limited to aiohttp and h2. The Google credential
chain is optional. A boundary test walks the package AST and fails when a module
imports an undeclared third-party dependency.
Prepare the development environment while network access is available:
uv syncEnable the repository's offline pre-commit checks with:
git config core.hooksPath .githooksThe hook runs staged-file checks with uv run --offline --no-sync: committing
does not resolve, install, update, or download dependencies. The checks do not
rewrite files; run Ruff or scripts/add-license-headers.py explicitly to apply
a reported fix.
Out-of-tree commands register in the aisan.commands entry point group:
[project.entry-points."aisan.commands"]
jetski = "aisan_corp.cli.jetski:main"Installed alongside aisan, they are dispatched by name and need no wrapper binary of their own:
uv tool install aisan --with git+ssh://example.com/aisan-corp
aisan jetski /path/to/repoThe contract is the one the built-in launchers already follow: a callable
taking the tokens after the command name and returning an exit status, with
LaunchRefused handled by the dispatcher. Everything else a plugin imports
from aisan is internal and may change between versions.
Four rules the dispatcher enforces:
- Built-ins are not overridable. Installing a plugin installs its whole
dependency closure, and any distribution in it can register in this group
though only the plugin was trusted. A claim on
claude,codex,opencodeorexplainis refused and reported. - Discovery costs nothing on the built-in path. The group is read only when
the first token names no built-in, and when help is printed.
aisan claudescans no metadata and imports no plugin. - A broken plugin is not a broken aisan. The import happens on the path that asked for it; a failure names the plugin and leaves every other command working.
- Duplicate names resolve by sorting, not by
sys.pathorder, and the plugin that loses is named.
Plugins get no separate audit path: --explain belongs to the launcher, so a
plugin that builds a box should accept it and print the resolved profile the
same way the built-in launchers do. Note also that aisan binds its own venv
read-only into every box with egress, so a plugin installed beside it is
readable from inside the box.
Anthropic's sandbox-runtime is the broader cross-platform tool for a general confined coding agent; aisan is Linux-only and concentrates on whole-harness confinement, explicit mount composition, and credential-aware transports such as the plaintext HTTP/2 REAPI proxy.
Pre-1.0. Treat the API as unstable.
MIT (see LICENSE).
