Skip to main content

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.json is 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. --net opts 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 /proc and /dev), a tmpfs over $HOME and /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:

On the host, reading the credential file, listing ~/.ssh, and fetching https://example.com all succeed. Inside an aisan box with permissions bypassed, the agent runs the same three and each one fails: no credential file, no key directory, no DNS.

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: resolved binds, egress backends, and environment

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
  ...

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

  • BoxSpec is frozen, non-defaulting data. A reviewer can read a call site and see what is mounted without simulating default resolution. Limits is 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, and BindOver read 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 --net reach 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 -> BoxSpec functions, 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; some distributions restrict unprivileged user namespaces by default.
  • systemd-run --user is optional. Cgroup limits are skipped when the command is absent. On a host without a usable user manager, disable them explicitly with Limits(use_cgroup=False).
  • Interactive sessions require the corresponding host CLI (claude, codex, or opencode) 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-auth extra 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.

--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_tools

Today 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.

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.

Plugin commands

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/repo

The 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, opencode or explain is 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 claude scans 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.path order, 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.

Relationship to sandbox-runtime

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.

Status

Pre-1.0. Treat the API as unstable.

License

MIT (see LICENSE).

Metadata

Release files for aisan 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aisan 0.1.1
File Size Uploaded
aisan-0.1.1.tar.gz 566.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aisan 0.1.1
File Interpreter ABI Platform
aisan-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 755.8 kB

Release files / aisan-0.1.1.tar.gz

Download URL aisan-0.1.1.tar.gz
Size 566.1 kB
Tags Source
SHA-256 checksum
How to use checksums
ada5423c325840425bd715a094f2e8ff948191e4be7494dc699fdc56bd0a5109
BLAKE2b-256 checksum
How to use checksums
10a301a0838d6c0c1d461fbbe3573f0e6b28a5124b6c64d26f15356401ccdaba
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.1-py3-none-any.whl

Download URL aisan-0.1.1-py3-none-any.whl
Size 189.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca73bfdec1d6190af7c09af34216f52d41f31d0568cd57d7996d31db3b665dc7
BLAKE2b-256 checksum
How to use checksums
d9fcd4c96fe5d2b479795de8d047c05a68e99345c585f47b0f636ee76c67d435
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 history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page