A sandbox wrapper for AI coding agents (Linux: bwrap, macOS: sandbox-exec). Isolates tools like Claude Code, GPT Codex, OpenCode, and Crush so they can only access what you explicitly allow.
brew tap akitaonrails/tap && brew install ai-jailcargo install ai-jailmise use -g github:akitaonrails/ai-jail# Run directly without installing
nix run github:akitaonrails/ai-jail
# Install to your profile
nix profile install github:akitaonrails/ai-jailDownload prebuilt binaries from the Releases page:
# Linux x86_64
curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz | tar xz
sudo mv ai-jail /usr/local/bin/
# macOS ARM (Apple Silicon)
curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-macos-aarch64.tar.gz | tar xz
sudo mv ai-jail /usr/local/bin/cargo build --release
cp target/release/ai-jail ~/.local/bin/- Linux: bubblewrap (
bwrap) must be installed:- Arch:
pacman -S bubblewrap - Debian/Ubuntu:
apt install bubblewrap - Fedora:
dnf install bubblewrap - If
bwrapis in a non-standard location (e.g. Nix store), setBWRAP_BIN=/absolute/path/to/bwrap. - The Nix flake package already sets
BWRAP_BINautomatically.
- Arch:
- macOS:
/usr/bin/sandbox-execis used (legacy/deprecated Apple interface).
cd ~/Projects/my-app
# Run Claude Code in a sandbox
ai-jail claude
# Run bash inside the sandbox (for debugging)
ai-jail bash
# See what the sandbox would do without running it
ai-jail --dry-run claudeOn first run, ai-jail creates a .ai-jail config file in the current directory by default. Subsequent runs reuse that config. Commit .ai-jail to your repo so the sandbox settings follow the project. Use --no-save-config for ephemeral runs without creating or updating the project config.
The default mode favors usability over maximum lockdown. These are intentionally open by default:
- Docker socket passthrough auto-enables when
/var/run/docker.sockexists (--no-dockerdisables it). - Display passthrough mounts
XDG_RUNTIME_DIRon Linux, which can expose host IPC sockets. - Environment variables are inherited (tokens/secrets in your shell env are visible in-jail).
ai-jail applies multiple overlapping security layers:
- Namespace isolation (bwrap): PID, UTS, IPC, mount namespaces. Network namespace in lockdown.
- Landlock LSM (V3 filesystem + V4 network): VFS-level access control independent of mount namespaces.
- Seccomp-bpf syscall filter: blocks ~30 dangerous syscalls (module loading,
ptrace,bpf, namespace escape, etc.). Lockdown blocks additional NUMA/hostname syscalls. - Resource limits: RLIMIT_NPROC (4096/1024 lockdown), RLIMIT_NOFILE (65536/4096 lockdown), RLIMIT_CORE=0. Prevents fork bombs and limits resource abuse.
- Sensitive /sys masking: tmpfs overlays hide
/sys/firmware,/sys/kernel/security,/sys/kernel/debug,/sys/fs/fuse. Lockdown also masks/sys/module,/sys/devices/virtual/dmi,/sys/class/net.
Each layer can be individually disabled (--no-seccomp, --no-rlimits, --no-landlock) if it causes issues.
For hostile/untrusted workloads, use --lockdown (see below).
ai-jail is a thin wrapper around OS-level sandboxing, so its security properties depend on the backend:
bwrap(Linux): namespace + mount sandboxing in userspace, plus Landlock LSM for VFS-level access control (Linux 5.13+).sandbox-exec/ seatbelt (macOS): legacy policy interface to Apple sandbox rules.
Some things to keep in mind:
- All backends depend on host kernel correctness. Kernel escapes are out of scope.
- These are process sandboxes, not hardware isolation. A VM runs a separate kernel and gives a stronger boundary.
- Timing/cache side channels and scheduler interference still exist in process sandboxes.
- Linux and macOS primitives are not equivalent; cross-platform policy parity is approximate.
sandbox-execon macOS is a deprecated interface. It works today but Apple could remove it.
If you need full isolation against unknown malware, use a disposable VM and treat ai-jail as one layer, not the whole story.
--lockdown switches to strict read-only, ephemeral behavior for hostile workloads.
ai-jail --lockdown claudeThis:
- Mounts the project read-only.
- Disables GPU, Docker, display passthrough, and mise.
- Ignores
--rw-mapand--mapflags. - Mounts
$HOMEas bare tmpfs (no host dotfiles). - Linux:
--clearenvwith minimal allowlist,--unshare-net,--new-session. - macOS: clears env to minimal allowlist, strips network and file-write rules from SBPL profile.
Persistence: --lockdown alone doesn't write .ai-jail (keeps runs ephemeral). Persist it with ai-jail --init --lockdown. Undo with --no-lockdown.
--init always writes config, so it cannot be combined with --no-save-config.
| Resource | Access | Notes |
|---|---|---|
/usr, /etc, /opt, /sys |
read-only | System binaries and config |
/dev, /proc |
device/proc | Standard device and process access |
/tmp, /run |
tmpfs | Fresh temp dirs per session |
$HOME |
tmpfs | Empty home, then dotfiles layered on top |
| Project directory (pwd) | read-write | The whole point |
GPU devices (/dev/nvidia*, /dev/dri) |
device | For GPU-accelerated tools |
| Docker socket | read-write | If /var/run/docker.sock exists |
| X11/Wayland | passthrough | Display server access |
/dev/shm |
device | Shared memory (Chromium needs this) |
In --lockdown, project is mounted read-only and host write mounts are removed.
Your real $HOME is replaced with a tmpfs. Dotfiles and dotdirs are selectively layered on top:
Never mounted (sensitive data):
.gnupg,.aws,.ssh,.mozilla,.basilisk-dev,.sparrow
Mounted read-write (AI tools and build caches):
.claude,.crush,.codex,.aider,.config,.cargo,.cache,.docker
Everything else: mounted read-only.
Hidden behind tmpfs:
~/.config/BraveSoftware,~/.config/Bitwarden~/.cache/BraveSoftware,~/.cache/chromium,~/.cache/spotify,~/.cache/nvidia,~/.cache/mesa_shader_cache,~/.cache/basilisk-dev
Explicit file mounts:
~/.gitconfig(read-only)~/.claude.json(read-write)
Local overrides (read-write):
~/.local/state~/.local/share/{zoxide,crush,opencode,atuin,mise,yarn,flutter,kotlin,NuGet,pipx,ruby-advisory-db,uv}
PID, UTS, and IPC namespaces are isolated. Hostname inside is ai-sandbox. The process dies when the parent exits (--die-with-parent).
--new-session is on for non-interactive runs and always in --lockdown. In --lockdown, Linux also unshares network.
On Linux 5.13+, ai-jail applies Landlock restrictions as defense-in-depth on top of bwrap. Landlock controls what the process can do at the VFS level, independent of mount namespaces. This closes attack vectors that bwrap alone doesn't cover: /proc escape routes, symlink tricks within allowed mounts, and acts as insurance against namespace bugs.
- Uses ABI V3 (Linux 6.2+) for filesystem rules with best-effort degradation to V1 on 5.13+ or no-op on older kernels.
- On Linux 6.5+, a second V4 ruleset adds network restrictions: lockdown mode denies all TCP bind/connect (defense-in-depth alongside
--unshare-net). - Applied in the parent process before spawning bwrap, so restrictions inherit through fork+exec.
- In
--lockdown, Landlock rules are stricter: project is read-only, no home dotdirs, only/tmpis writable, no network. - Disable with
--no-landlockif it causes issues with specific tools.
Enable a persistent status line on the bottom row of your terminal:
ai-jail -s claude # dark theme
ai-jail -s=light claude # light themeThe bar shows the project path, running command, ai-jail version, and a green ↑ when an update is available. It uses a PTY proxy to keep the bar visible even when the child application resets the screen. The preference is stored in $HOME/.ai-jail and persists across sessions.
When running codex through the PTY proxy, ai-jail also injects a redraw key on terminal resize to force the app to repaint at the new width. The default is ctrl-shift-l for codex sessions. In practice, terminals collapse shifted control letters, so ctrl-shift-l and ctrl-l send the same control byte to the app.
Override or disable that global behavior in $HOME/.ai-jail:
status_bar_style = "pastel"
resize_redraw_key = "ctrl-l"
# or:
# resize_redraw_key = "disabled"If mise is found on $PATH, the sandbox automatically runs mise trust && mise activate bash && mise env before your command. This gives AI tools access to project-specific language versions. Disable with --no-mise.
ai-jail [OPTIONS] [--] [COMMAND [ARGS...]]
| Command | What it does |
|---|---|
claude |
Run Claude Code |
codex |
Run GPT Codex |
opencode |
Run OpenCode |
crush |
Run Crush |
bash |
Drop into a bash shell |
status |
Show current .ai-jail config |
| Any other | Passed through as the command |
If no command is given and no .ai-jail config exists, defaults to bash.
| Flag | Description |
|---|---|
--rw-map <PATH> |
Mount PATH read-write (repeatable) |
--map <PATH> |
Mount PATH read-only (repeatable) |
--hide-dotdir <NAME> |
Never bind-mount the named home dotdir into the sandbox (e.g. .my_secrets). Leading dot is optional. Repeatable. Cannot hide dotdirs required for tool operation (.cargo, .config, .cache, etc.) — those emit a warning and stay visible. |
--allow-tcp-port <PORT> |
Permit outbound TCP to PORT in lockdown mode (repeatable). Skips --unshare-net and uses Landlock V4 NetPort rules to deny everything else. Requires Linux ≥ 6.5; hard-fails otherwise. No effect outside lockdown or on macOS. |
--lockdown / --no-lockdown |
Enable/disable strict read-only lockdown mode |
--landlock / --no-landlock |
Enable/disable Landlock LSM (Linux 5.13+, default: on) |
--seccomp / --no-seccomp |
Enable/disable seccomp syscall filter (Linux, default: on) |
--rlimits / --no-rlimits |
Enable/disable resource limits (default: on) |
--gpu / --no-gpu |
Enable/disable GPU passthrough |
--docker / --no-docker |
Enable/disable Docker socket |
--display / --no-display |
Enable/disable X11/Wayland |
--mise / --no-mise |
Enable/disable mise integration |
--ssh / --no-ssh |
Share ~/.ssh read-only + forward SSH_AUTH_SOCK (default: off) |
--pictures / --no-pictures |
Share ~/Pictures read-only (default: off) |
--save-config / --no-save-config |
Enable/disable automatic .ai-jail writes |
-s, --status-bar[=STYLE] |
Enable persistent status line. STYLE is pastel (default, random palette per session), dark, or light |
--no-status-bar |
Disable persistent status line |
--exec |
Direct execution mode (no PTY proxy, no status bar) |
--clean |
Ignore existing config, start fresh |
--dry-run |
Print the bwrap command without executing |
--init |
Create/update config and exit (don't run) |
--bootstrap |
Generate smart permission configs for AI tools |
-v, --verbose |
Show detailed mount decisions |
-h, --help |
Show help |
-V, --version |
Show version |
# Share an extra library directory read-write
ai-jail --rw-map ~/Projects/shared-lib claude
# Read-only access to reference data
ai-jail --map /opt/datasets claude
# No GPU, no Docker, just the basics
ai-jail --no-gpu --no-docker claude
# Run a one-shot command and capture its output
result=$(ai-jail --exec -- my-script.sh --flag1 --flag2)
# Suspicious/untrusted workload mode
ai-jail --lockdown bash
# See exactly what mounts are being set up
ai-jail --dry-run --verbose claude
# Create config without running
ai-jail --init --no-docker claude
# Allow SSH inside the sandbox (agent forwarding + keys read-only)
ai-jail --ssh claude
# Share ~/Pictures read-only (e.g. for image analysis)
ai-jail --pictures claude
# Run without creating/updating .ai-jail
ai-jail --no-save-config claude
# Regenerate config from scratch
ai-jail --clean --init claude
# Pass flags through to the sub-command (after --)
ai-jail -- claude --model opusCreated in the project directory on first run. Example:
# ai-jail sandbox configuration
# Edit freely. Regenerate with: ai-jail --clean --init
command = ["claude"]
rw_maps = ["/home/user/Projects/shared-lib"]
ro_maps = []
no_gpu = true
lockdown = trueWhen CLI flags and an existing config are both present:
command: CLI replaces config for the current run, but a CLI-passed command is not auto-persisted when the project already has a stored command — soai-jail codexafterai-jail clauderuns codex for that session without rewriting.ai-jail's stored default. Useai-jail --init <command>to explicitly change the stored command. First-run bootstrap (no stored command yet) still persists the CLI command as the new default.rw_maps/ro_maps: CLI values are appended (duplicates removed)- Boolean flags: CLI overrides config (
--no-gpusetsno_gpu = true) --save-config/--no-save-configoverrideno_save_config- Config is updated after merge in normal mode when config saving is enabled; lockdown skips auto-save
| Field | Type | Default | Description |
|---|---|---|---|
command |
string array | ["bash"] |
Default command to run inside sandbox. Set by first run or by --init; not overwritten when a different command is passed on the CLI. |
rw_maps |
path array | [] |
Extra read-write mounts |
ro_maps |
path array | [] |
Extra read-only mounts |
no_gpu |
bool | not set (auto) | true disables GPU passthrough |
no_docker |
bool | not set (auto) | true disables Docker socket |
no_display |
bool | not set (auto) | true disables X11/Wayland |
no_mise |
bool | not set (auto) | true disables mise integration |
ssh |
bool | not set (off) | true shares ~/.ssh read-only + forwards SSH_AUTH_SOCK |
pictures |
bool | not set (off) | true shares ~/Pictures read-only |
no_save_config |
bool | not set (enabled) | true disables automatic .ai-jail writes |
no_landlock |
bool | not set (auto) | true disables Landlock LSM (Linux only) |
no_seccomp |
bool | not set (auto) | true disables seccomp syscall filter (Linux only) |
no_rlimits |
bool | not set (auto) | true disables resource limits |
lockdown |
bool | not set (disabled) | true enables strict read-only lockdown mode |
Status bar preferences (no_status_bar, status_bar_style, resize_redraw_key) are stored in $HOME/.ai-jail (global user config), not in per-project .ai-jail files. status_bar_style accepts "dark", "light", or "pastel" — pastel rotates through a curated set of soft pastel palettes (with high-contrast foreground), picking a new one at random for each session. Set it back to "dark" or "light" to disable the rotation. resize_redraw_key is used only by the PTY/status-bar path on terminal resize; accepted values are ctrl-l, ctrl-shift-l (same wire encoding as ctrl-l), or disabled. If unset, codex gets the ctrl-shift-l default and other commands stay off.
When a boolean field is not set, the feature is enabled if the resource exists on the host. no_save_config is the exception: when unset, config auto-save is enabled in normal mode.
ai-jail doesn't support Windows natively and probably never will. The sandbox depends on Linux namespaces (via bwrap) and macOS seatbelt profiles (via sandbox-exec). Windows has nothing equivalent in userspace. AppContainers exist but they're a completely different API, need admin privileges for setup, and the security model doesn't map to what bwrap does. A Windows port would be a separate project, not a backend swap.
If you're on Windows, run ai-jail inside WSL 2. WSL 2 runs a real Linux kernel, so bwrap works normally.
- Install WSL 2 if you haven't:
wsl --install- Open your WSL distro (Ubuntu by default) and install bubblewrap:
sudo apt update && sudo apt install bubblewrap- Build ai-jail from source inside WSL:
cd ~/Projects
git clone https://github.com/nicholasgasior/ai-jail.git
cd ai-jail
cargo build --release
cp target/release/ai-jail ~/.local/bin/- Run it from inside WSL against your project directory:
cd /mnt/c/Users/you/Projects/my-app
ai-jail claudeWSL 2 mounts your Windows drives under /mnt/c/, /mnt/d/, etc. The sandbox sees the Linux filesystem, so all the mount isolation works as expected. Your Windows files are accessible through those mount points.
One thing to watch: WSL 2 filesystem performance is slower on /mnt/c/ (the Windows side) than on the native Linux filesystem (~/). For large projects, cloning into ~/Projects/ inside WSL instead of working from /mnt/c/ makes a noticeable difference.
GPL-3.0. See LICENSE.