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.
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
Flag
Config key
What it does
Risk
--network
network = true
The 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
--ssh
ssh = true
Your ~/.ssh, read only, and your SSH agent socket, so git push works.
Opens one thing
--docker
no_docker = false
The Docker socket. This is effectively root on your machine, because the daemon can start containers that mount the host.
Weakens the jail
--tailscale
tailscale = true
The Tailscale socket.
Opens one thing
--systemd-user
systemd_user = true
The systemd user bus. The agent can ask your user manager to run services on the host. Linux only.
Weakens the jail
Agent and environment
Flag
Config key
What it does
Risk
--agent-state
agent_state = true
The 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]>
none
Passes 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-env
inherit_env = true
Passes your whole shell environment, every secret in it included. Avoid it.
Weakens the jail
none
env_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
Flag
Config key
What it does
Risk
--toolchains
no_toolchains = false
Your 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
--github
github = true
Your 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
--aws
aws = true
Your ~/.aws, read only. Anything in the jail can act as you on AWS while the credential is valid.
Opens one thing
--kube
kube = true
Your ~/.kube, read only. Anything in the jail can talk to your clusters as you.
Opens one thing
--gcloud
gcloud = true
Your ~/.config/gcloud, read only. Anything in the jail can act as you on Google Cloud.
Opens one thing
--docker-config
docker_config = true
Your ~/.docker/config.json, read only, which holds your registry logins. Anything in the jail can use them.
Opens one thing
Desktop and hardware
Flag
Config key
What it does
Risk
--gpu
no_gpu = false
GPU devices, and with them the driver's attack surface. Linux only.
Opens one thing
--kvm
kvm = 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
--display
no_display = false
The Wayland socket and nothing else from your session. Linux only.
Opens one thing
--x11
x11 = true
The X11 socket. X11 lets a program log keystrokes and take screenshots.
Weakens the jail
--audio
audio = true
PipeWire and PulseAudio sockets and /dev/snd. Anything in the jail can record and play audio. Linux only.
Opens one thing
--host-shm
host_shm = true
The host's /dev/shm, which opens shared memory with processes outside the jail.
Opens one thing
--pictures
pictures = true
Your ~/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
Flag
Config key
What it does
Risk
--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
Flag
Config key
What it does
Risk
--no-landlock
no_landlock = true
Turns off Landlock, the kernel's own copy of the file rules. Refused under --lockdown.
Weakens the jail
--no-seccomp
no_seccomp = true
Turns off the filter that blocks dangerous system calls.
Weakens the jail
--no-rlimits
no_rlimits = true
Turns off the limits that stop runaway processes.
Weakens the jail
--no-private-home
private_home = false
Gives up the fresh home directory. This is broad access to your real home. Prefer --map for the one path you need.
Appends 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-show
none
Prints 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-verify
none
Walks 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
Flag
What it does
--dry-run
Prints the sandbox command and runs nothing. It never writes a config file.
--init
Writes or updates the project .ai-jail and exits.
--clean
Ignores the project .ai-jail for this run.
--bootstrap
Generates permission configs for the AI tools themselves.
status
Shows the current .ai-jail configuration.
-v, --verbose
Shows 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/ claudeai-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
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.
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.
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.