aisan
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/repo
That 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.
Nothing on faith: --explain
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:
$ 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
...
User bind specs
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.
As a library: unattended API jobs
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.
Design rules
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.
Requirements
- Linux, Python 3.12 or newer, and
bubblewrap(bwrap). User namespaces must be available to the invoking user. 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.
Installation
python -m pip install aisan
For 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 --net
Launcher options come before a literal --; arguments after it are passed to
the underlying client unchanged.
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.
Local checks
Prepare the development environment while network access is available:
uv sync
Enable the repository's offline pre-commit checks with:
git config core.hooksPath .githooks
The 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.
Relationship to sandbox-runtime
Anthropic's sandbox-runtime (srt) is the broader choice for a general confined coding agent and supports Linux, macOS, and Windows. aisan is Linux-only and concentrates on explicit mount composition plus credential-aware transports, including plaintext HTTP/2 REAPI traffic and sealing an existing directory around a writable hole.
Reviewing srt informed three decisions here: policy exceptions fail closed,
refusals state an actionable policy reason, and launch commands remain argv so
payload bytes never pass through a host shell. These are general security and
interface rules, independently implemented in aisan; no srt source code was
copied or adapted. The last detailed comparison used srt revision 121c6ac
(v0.0.71) on 2026-08-10, so current srt behavior should be checked before relying
on any feature difference.
Status
Pre-1.0. Treat the API as unstable.
License
MIT (see LICENSE).
Metadata
Release files for aisan 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aisan-0.1.0.tar.gz | 280.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aisan-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 461.3 kB
Release files / aisan-0.1.0.tar.gz
| Download URL | aisan-0.1.0.tar.gz |
|---|---|
| Size | 280.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eebe44e1c08e3dea086a1fda9a7a006ebd38ffb76e9d670850faeefe7936283b
|
|
BLAKE2b-256 checksum How to use checksums |
482d815ae7d2584a6cd381828817e1302f0084268db36fc84cdcf807035493a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / aisan-0.1.0-py3-none-any.whl
| Download URL | aisan-0.1.0-py3-none-any.whl |
|---|---|
| Size | 180.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90192f2b09533fa8c2a419785d87db89de93ffd0077f501422beefd76b3c0294
|
|
BLAKE2b-256 checksum How to use checksums |
98f92f076618ef4f3c5b653be9ce3e5241f4a9d33b6ed7f8839d37b2eab8a96a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|