Skip to content
Menu
Do I need it?What an agent can reach on your machineHow it worksThe layers between the agent and your systemCompareBuilt-in sandboxes, Docker and VMsConfigureOpen only what a task needsSecurityThe threat model and its limitsDownloadBinaries, checksums and release notesInstall
Configure

Closed by default. You hold the switches.

With no options, the agent gets your project, your build tools on Linux, its own login and its own API host, and no network beyond that and a short list of package registries. You turn things down one switch at a time, and lockdown closes most of them at once.

For the person at the keyboard.

Three places a setting can come from

Settings come from a file inside the project, a file in your home directory and the flags you type. They are listed from least to most authority.

↔ Swipe sideways to see the full diagram

A repository you cloned cannot open its own jail.

Project .ai-jail

ai-jail treats it as untrusted: it can tighten the jail and never open it. It can shrink an allow_hosts or forward_ports list and never add to one. ai-jail never follows it through a symlink, and the agent sees an empty file in its place.

Global ~/.ai-jail

It is yours, so ai-jail trusts it. It holds a base table for every run, plus [commands.<name>] tables chosen by the first word of the command.

Command-line flags

Flags have the highest authority. Most switches come in pairs, such as --network and --no-network, so a flag can also close what a file opened.

In a command table, single values replace the base and lists such as maps and masks are added to it.

~/.ai-jail
# base: applies to every commandhide_dotdirs = [".my_secrets"]mask = [".env", ".env.*"] # only when the command starts with "claude"[commands.claude]network = trueenv_pass = ["ANTHROPIC_BASE_URL"]mask = ["*.pem"]

The switches

Every row is off until you ask, except the two cyan rows, which are on by default. Each capability flag has a --no- twin that closes it again. A red badge marks a switch that weakens the jail. Security has the full list and the reasons.

Network and services

FlagConfig keyWhat it doesRisk
--networknetwork = trueThe whole network. Anything the agent can read, it can send out. If the agent needs only a few hosts, --allow-host is tighter. Without either flag, the sandbox reaches only a known agent's own API host and, on Linux, the package registries that toolchains open through the proxy; --no-network closes both.Weakens the jail
--allow-host <HOST>allow_hosts = [...]The sandbox reaches only the listed host and its subdomains, over TCP through ai-jail's built-in proxy. You can repeat the flag. Since 2.8.1 the list adds to the default, so a known agent keeps its own API host. It cannot be combined with --network or --browser, and a project file may only shrink the list.Opens one thing
--forward-port <PORT>forward_ports = [...]One port of the host's loopback, relayed onto the sandbox's own loopback. A local MCP server, an editor socket or a dev database on the host's 127.0.0.1 answers at the same port inside, and every other host port stays shut. Repeat the flag for more ports. Linux only, and refused with --network and for ports below 1024. It works with --allow-host and under --lockdown. The service behind the port is trusted in full, with no allowlist and no audit record. A project file can only shrink the list. Where forward_ports goes in the config file.Weakens the jail
--sshssh = trueYour ~/.ssh, read only, and your SSH agent socket, so git push works.Opens one thing
--dockerno_docker = falseThe Docker socket. This is effectively root on your machine, because the daemon can start containers that mount the host.Weakens the jail
--tailscaletailscale = trueThe Tailscale socket.Opens one thing
--systemd-usersystemd_user = trueThe systemd user bus. The agent can ask your user manager to run services on the host. Linux only.Weakens the jail

Agent and environment

FlagConfig keyWhat it doesRisk
--agent-stateagent_state = trueThe agent's own login. The invoked harness's state, such as ~/.claude, ~/.codex or Devin's ~/.local/share/devin and ~/.config/devin, is mounted read and write, and its API key is copied from your shell when set: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY, XAI_API_KEY, MOONSHOT_API_KEY. The agent starts logged in, and anything in the jail can use that login. --no-agent-state gives a logged-out run. Off under lockdown, and a project file can only turn it off.On by default
--claude-dir <PATH>claude_dir = "..."Uses the given directory as Claude's state and sets CLAUDE_CONFIG_DIR.Opens one thing
--env <NAME[=VALUE]>nonePasses one variable from your shell, or sets one with NAME=VALUE. Repeatable and never saved to disk. On Linux the values travel to the sandbox through an in-memory file and never appear on the launcher's command line, but a literal NAME=VALUE is still visible on ai-jail's own command line, so for a secret prefer --env NAME or --env-from-file.Opens one thing
--env-from-file <PATH>env_from_file = [...]Reads strict KEY=VALUE lines from a file and passes them like --env, which wins on conflicts. The file must be yours, a regular file, mode 0600 or stricter, and outside the project, or the launch fails. ai-jail never saves the values to disk, and the config key works from the global file only.Opens one thing
--secret <KEY=host>secret_hosts = { ... }Keeps the real value of KEY out of the sandbox. The agent sees a placeholder, and the proxy puts the real value only into requests to that one host. Needs --allow-host, and the key must already come in through --env or --env-from-file. Global file only, never saved. It covers clients that can talk plain HTTP to the proxy for that host; HTTPS tunnels are untouched.Opens one thing
--inherit-envinherit_env = truePasses your whole shell environment, every secret in it included. Avoid it.Weakens the jail
noneenv_pass = ["NAME"]The same as --env, from the global file only. A project file that sets it is ignored.Opens one thing

Toolchains and tool credentials

FlagConfig keyWhat it doesRisk
--toolchainsno_toolchains = falseYour compilers and package managers read only, their dependency caches in a store the jail owns, and the package registries reachable through the filtered proxy. Linux only, and on by default there; on macOS nothing is mapped and no registry is reachable. Lockdown turns it off, and a project file can only turn it off. Details below.On by default
--githubgithub = trueYour gh config directory, read only, plus your active github.com token forwarded as GH_TOKEN for that launch, so gh is logged in even when the token lives in the system keyring. ai-jail runs gh auth token on the host and passes the result through an in-memory file, never on a command line or on disk; --dry-run never fetches it, and a GH_TOKEN you pass with --env skips the lookup. Only github.com and only the active account. Anything in the jail can still act as you on GitHub.Opens one thing
--awsaws = trueYour ~/.aws, read only. Anything in the jail can act as you on AWS while the credential is valid.Opens one thing
--kubekube = trueYour ~/.kube, read only. Anything in the jail can talk to your clusters as you.Opens one thing
--gcloudgcloud = trueYour ~/.config/gcloud, read only. Anything in the jail can act as you on Google Cloud.Opens one thing
--docker-configdocker_config = trueYour ~/.docker/config.json, read only, which holds your registry logins. Anything in the jail can use them.Opens one thing

Desktop and hardware

FlagConfig keyWhat it doesRisk
--gpuno_gpu = falseGPU devices, and with them the driver's attack surface. Linux only.Opens one thing
--kvmkvm = true/dev/kvm, for hardware-accelerated virtual machines such as QEMU or an Android emulator. It exposes the host kernel's KVM interface. Guests get only the sandbox's network, and no tun or vhost devices. Linux only.Opens one thing
--displayno_display = falseThe Wayland socket and nothing else from your session. Linux only.Opens one thing
--x11x11 = trueThe X11 socket. X11 lets a program log keystrokes and take screenshots.Weakens the jail
--audioaudio = truePipeWire and PulseAudio sockets and /dev/snd. Anything in the jail can record and play audio. Linux only.Opens one thing
--host-shmhost_shm = trueThe host's /dev/shm, which opens shared memory with processes outside the jail.Opens one thing
--picturespictures = trueYour ~/Pictures, read only.Opens one thing
--browser[=hard|soft]browser_profile = "hard"A separate browser profile. hard (the default) keeps nothing between runs, soft keeps a profile under ~/.local/share/ai-jail/browsers.Opens one thing

Files

FlagConfig keyWhat it doesRisk
--map <PATH|SOURCE:DEST>ro_maps = [...]Mounts one more path read only. Write SOURCE:DEST to mount it somewhere else. Repeatable.Opens one thing
--rw-map <PATH|SOURCE:DEST>rw_maps = [...]Mounts one more path read and write. Repeatable.Opens one thing
--overlay-map <PATH>overlay_maps = [...]Mounts a path copy-on-write: the agent's writes go to a side layer and the original stays untouched. Linux only; on macOS it becomes a read-only mount.Opens one thing
--hide-dotdir <NAME>hide_dotdirs = [...]Never mounts the named dot-directory, for example .my_secrets. Repeatable.

Layers you can turn off, and should not

FlagConfig keyWhat it doesRisk
--no-landlockno_landlock = trueTurns off Landlock, the kernel's own copy of the file rules. Refused under --lockdown.Weakens the jail
--no-seccompno_seccomp = trueTurns off the filter that blocks dangerous system calls.Weakens the jail
--no-rlimitsno_rlimits = trueTurns off the limits that stop runaway processes.Weakens the jail
--no-private-homeprivate_home = falseGives up the fresh home directory. This is broad access to your real home. Prefer --map for the one path you need.Weakens the jail

How it works explains what each layer does.

Visibility

FlagConfig keyWhat it doesRisk
--audit-logaudit_log = trueAppends one record per launch to ~/.local/share/ai-jail/history.jsonl (directory 0700, file 0600): time, command, effective capabilities, config sources, exit code and duration. With --allow-host, every allowed or denied connection is recorded too, and with --secret, every injection. Each line carries the hash of the one before it. It is off by default, and a project file can turn it off but not on.Opens one thing
--audit-shownonePrints a readable summary of the log and exits: launches, filtered-network decisions and secret substitutions. It does not check the chain. Exit 0 after a display, 1 for malformed records or a read error, 2 when there is no log. Launch records can include command arguments, so keep secrets out of them.
--audit-verifynoneWalks the audit log's hash chain and exits: 0 when it is intact, 1 at the first broken link, 2 when there is no log. Records written before 2.2 are reported as legacy, which is not an error.

Utility

FlagWhat it does
--dry-runPrints the sandbox command and runs nothing. It never writes a config file.
--initWrites or updates the project .ai-jail and exits.
--cleanIgnores the project .ai-jail for this run.
--bootstrapGenerates permission configs for the AI tools themselves.
statusShows the current .ai-jail configuration.
-v, --verboseShows every mount as the jail is built.

Run ai-jail --help for the rest: the status bar, worktrees, mise and terminal options. The toolchains key is inverted like the older ones: no_toolchains = true turns them off.

Hide secrets inside the project

Your .env file lives in the project, and the project is readable and writable. With the network on, the agent can send out whatever it can read there. Masking takes those files away.

↔ Swipe sideways to see the full diagram

The rest of the project stays as it was.

--mask <PATH|GLOB>

Replaces every match with an empty placeholder. The agent sees that the file exists and gets no content, so tools that only check for it keep working. The mask changes only what the sandbox sees. A helper on the host that acts for the agent, such as the Docker daemon or an MCP server, still reads the real file.

--deny-path <PATH|GLOB>

Makes every match a permission error.

--mask-except and --deny-path-except

Carve one path back out of a rule. They weaken protection, so a project file cannot set them.

As flags
# quote globs so ai-jail gets the pattern, not your shell's expansionai-jail --mask .env --mask '.env.*' --mask '*.pem' --deny-path secrets/ claude ai-jail --mask '.env.*' --mask-except .env.example claude
.ai-jail
# a project file may tighten, so this works in the repository toomask = [".env", ".env.*", "*.pem"]deny_paths = ["secrets/"]

Keep API keys out of every config file

Masking covers files in the project. For the agent's own key, pass it from your shell with --env NAME, or from a file only you can read with --env-from-file. Neither is ever written to a .ai-jail file, and a project file cannot set them. With filtered egress on, --secret KEY=host also keeps the real value out of the sandbox; the recipe below shows it.

terminal
# the file must be yours, mode 0600 and outside the projectchmod 600 ~/.secrets/claude.envai-jail --env-from-file ~/.secrets/claude.env claude
~/.ai-jail
# global file only; a project file that sets this is ignored[commands.claude]env_from_file = ["~/.secrets/claude.env"]

Your toolchains come along

A build needs compilers, package managers and their caches. On Linux, ai-jail brings them in by default: the tools read only, the caches in a store the jail owns, and the package registries reachable through the filtered proxy.

↔ Swipe sideways to see the full diagram

The host's own caches stay outside, so a jailed build cannot poison what the host builds from. The proxy lets the registries and the agent's own API host through and nothing else.

Tools, read only

Since 2.4.1, ai-jail maps mise installs and config read only. For Rust it maps ~/.cargo/bin and ~/.rustup the same way, so cargo and rustc resolve. Cargo's credentials and config.toml stay out. Because mise cannot install anything into a read-only map, ai-jail sets MISE_NOT_FOUND_AUTO_INSTALL=false inside, so a missing tool fails once instead of on every call.

A cache the jail owns

Dependency caches live under ~/.local/share/ai-jail/cache/ and survive between sessions. Cargo's registry and git caches go there. So does the default cache of pip, uv, Go, npm, yarn, pnpm, bun, deno, Maven, Gradle, Coursier, Composer, Crystal and Zig, when that tool's directory exists on your machine.

Registries, through the proxy

With toolchains on and no network flag given, the sandbox reaches crates.io, npm, PyPI, the Go proxy, Maven Central, Clojars, Packagist and github.com, each with its subdomains, through the same proxy as --allow-host. A known agent's own API host passes the same way, on macOS too; the registries are Linux only.

terminal
# the default on Linuxai-jail claude # caches on, no network at allai-jail --no-network claude # nothing mapped, nothing reachableai-jail --no-toolchains --no-network claude

Turning it down

The default is tuned for getting work done. Each step below closes one more thing, and --lockdown closes most of them at once. Security explains what each one protects against.

  • The default: the agent's login and its own API host, your toolchains on Linux, the package registries, and the project read and write.
  • --no-network: fully offline. With an --allow-host list as well, the sandbox reaches only the hosts you name, without the automatic defaults.
  • --no-agent-state: no login. The harness's state stays out and its API key is not copied.
  • --no-toolchains: no dependency caches and no package registries.
  • --mask, --deny-path and --hide-dotdir take single paths away, and a narrow --map or --rw-map replaces a broad grant. They change what the sandbox sees; a helper on the host, such as the Docker daemon or a service behind --forward-port, still reads the real files.
  • --lockdown: the project read only, no login, no toolchains, no registries, strict Landlock and seccomp, and still not a VM.

Lockdown: let an agent read code it must not change

--lockdown is for reviews and audits. The agent can read the project and answer questions about it, and it cannot change a file in it.

↔ Swipe sideways to see the full diagram

The agent and project are the same, and only a temporary directory is writable.

What changes

  • The project is mounted read only
  • Only /tmp is writable on Linux. On macOS nothing is
  • No dot-directories from your home, no extra maps, no overlays
  • The environment is cleared and PATH is fixed to system directories
  • Landlock is mandatory: if it cannot be applied, the launch fails
  • Tighter limits on processes, open files and file size
  • No display, audio, GPU, KVM, Docker, SSH agent or mise, whatever else you passed
  • No toolchain caches, registry egress or tool credentials
  • No agent login, so the harness starts logged out
terminal
ai-jail --lockdown claude

Recipes

Copy one. To see what it builds, run it with --dry-run first.

A cloud agent, day to day

A known agent starts logged in and reaches its own API host through the filtered proxy, plus the package registries on Linux. When a task needs more, put allow_hosts in the global file with the extra hosts, or open the whole network with network = true.

~/.ai-jail
# nothing to add: login and api.anthropic.com come by defaultai-jail claude # when a task needs more, once in ~/.ai-jail (added to the API host and, on Linux, the registries)[commands.claude]allow_hosts = ["api.example.com"]# or open the whole network: network = true

An agent ai-jail does not know

ai-jail allows the API host on its own for five agents. Any other agent gets its host from you with --allow-host. If the host is missing from the list, the launch warning names it.

terminal
ai-jail --allow-host api.example.com myagent# a known agent needs no flag: ai-jail allows its API host by itself

A logged-out run

When the agent should have no credentials at all, --no-agent-state keeps its state directory and its API key out. It starts logged out, and whatever it logs into inside the fresh home is gone when the run ends.

terminal
ai-jail --no-agent-state claude

Allowed hosts in the global file

The list applies to every run of that command, on top of the agent's own API host. On Linux, github.com and the package registries are already reachable, so you list them only on macOS. An allowed host is trusted as a whole: the agent can ask it for anything your credentials there allow.

~/.ai-jail
[commands.claude]allow_hosts = ["api.example.com"]

A local MCP server or database

Without --network the sandbox has its own loopback, so a server on your host's 127.0.0.1 is out of reach. --forward-port relays that one port and nothing else, on Linux. In the global file, put forward_ports under the command's table and never in the base table: the flag is refused with --network, which --browser normally implies, and on macOS, so a base entry would fail those launches.

~/.ai-jail
ai-jail --forward-port 49374 claude # or once in ~/.ai-jail, under the command's table[commands.claude]forward_ports = [49374]

A key the agent never sees

The sandbox gets a placeholder in ANTHROPIC_API_KEY, and ai-jail's proxy puts the real key only into requests to api.anthropic.com. The client must send plain HTTP to the proxy for that host, which the http:// base URL does; ai-jail then carries the request to the real host over TLS. A client that only opens HTTPS tunnels keeps working, without substitution.

terminal
# the key lives in the 0600 file; the sandbox only ever sees AIJAIL-PHANTOM-...ai-jail --allow-host api.anthropic.com \  --env-from-file ~/.secrets/claude.env \  --secret ANTHROPIC_API_KEY=api.anthropic.com \  --env ANTHROPIC_BASE_URL=http://api.anthropic.com claude

Git push over SSH

--ssh mounts ~/.ssh read only and forwards your SSH agent socket. It does not open the network, so the recipe pairs it with --network. For a push over HTTPS with your gh token, see the GitHub CLI recipe.

terminal
ai-jail --network --ssh claude

Build and test inside the jail

Nothing to add on Linux. cargo build, npm install or go get fetch from the registries through the proxy and cache into the jail's own store, and mise installs come along read only. For a run that touches nothing and reaches nothing, turn both off.

terminal
# default on Linux: caches on, registries reachable through the proxyai-jail claude # nothing mapped, nothing reachableai-jail --no-toolchains --no-network claude

GitHub CLI inside the jail

--github mounts your gh config directory read only and forwards your active github.com token as GH_TOKEN, so gh and git push over HTTPS are logged in, keyring or not. On Linux, github.com and its subdomains are already in the default list, so you add no host; on macOS add --allow-host github.com. Anything in the jail can then act on GitHub as you.

terminal
ai-jail --github claude # macOS: github.com is not in the default list thereai-jail --github --allow-host github.com claude

A browser for the agent

--browser alone gives a browser that cannot load a page or open a window. On Linux it also needs --network and --display, and an X11 browser needs --x11 in place of --display.

terminal
# Waylandai-jail --browser=soft --network --display chromium# X11ai-jail --browser=soft --network --x11 chromium

Separate Claude profiles

Keep work and personal logins apart by pointing each run at its own Claude directory.

terminal
ai-jail --network --claude-dir ~/.claude-work claude

Try a risky refactor on a copy

An overlay keeps the agent's writes in a side layer under .ai-jail-overlays in the project. Compare it with the original afterward and keep what you like. Linux only.

terminal
ai-jail --network --overlay-map ~/Projects/my-app/src claude

Old configs keep working

Backward compatibility is a project rule: the project never removes a flag or a config key. Unknown keys are ignored and missing ones take their defaults, so a file written for an old version still loads.

Older switches keep their inverted names, where true turns the thing off: no_gpu, no_docker, no_display, no_mise, no_toolchains, no_landlock, no_seccomp, no_rlimits. That is why --gpu is saved as no_gpu = false. Newer switches use plain names such as network = true. --allow-tcp-port is still parsed and still refuses to launch, because it cannot fence UDP. Its error points at --allow-host.

Put your agent behind bars

ai-jail is a single binary that needs no daemon and no root. You add one word in front of the command you already run.

terminal
brew tap akitaonrails/tap && brew install ai-jailai-jail claude