Sandboxed Nix environments with direnv auto-activation. Cross-platform: bwrap on Linux, sandbox-exec on macOS.
Built for running tools like Claude Code in isolated project directories.
┌─────────────────────────────────────────────────┐
│ You cd into a project directory │
│ ↓ │
│ direnv detects .envrc → runs nixcage hook │
│ ↓ │
│ nixcage reads nixcage.toml config │
│ ↓ │
│ You run: nixcage run <cmd> │
│ ↓ │
│ ┌─── Linux ───────┐ ┌─── macOS ────────────┐ │
│ │ bwrap │ │ sandbox-exec │ │
│ │ + namespaces │ │ + Seatbelt profiles │ │
│ │ + cgroups (opt) │ │ │ │
│ └────────┬────────┘ └────────┬─────────────┘ │
│ ↓ ↓ │
│ nix-shell --pure (packages from config) │
│ ↓ │
│ Your command runs in the sandbox │
└─────────────────────────────────────────────────┘
# Flake
nix profile install github:hamidr/nixcage
# Or clone + install locally
git clone https://github.com/hamidr/nixcage.git
cd nixcage
nix profile install .| Tool | Required | Install |
|---|---|---|
| Nix | ✅ | sh <(curl -L https://nixos.org/nix/install) |
| direnv | ✅ | nix-env -iA nixpkgs.direnv + shell hook |
| jq | ✅ | Bundled when installed via Nix; otherwise nix-env -iA nixpkgs.jq |
| bubblewrap | Linux only | Bundled when installed via Nix; otherwise nix-env -iA nixpkgs.bubblewrap |
| sandbox-exec | macOS only | Built-in (ships with macOS) |
# 1. Initialize a project (optionally with a preset)
cd ~/my-project
nixcage init # blank config
nixcage init --preset claude-code # or use a preset
# 2. Edit the config
$EDITOR nixcage.toml
# 3. Allow direnv
direnv allow
# 4. Run a command inside the sandbox
nixcage run claude
# Or enter an interactive sandboxed shell
nixcage shell[sandbox]
# "strict" — no network, tmpfs home, minimal access
# "standard" — project dir writable, network allowed
# "relaxed" — home readable, project writable, network allowed
level = "standard"
[sandbox.filesystem]
ro_bind = ["/data/models"] # extra read-only paths
rw_bind = ["/tmp/shared"] # extra read-write paths
blacklist = ["/home/user/.ssh"] # paths to hide (~ is expanded)
[sandbox.network]
allow = true
[sandbox.resources]
cpus = 4 # max CPU cores (Linux only, via systemd-run)
memory = "4G" # max RAM (Linux only)
[nix]
packages = ["nodejs_22"]
pure = true
store_mode = "readonly" # shared | readonly | copy | isolated
[cage]
command = "" # default command (empty = shell)
passthrough_env = ["TERM", "LANG", "ANTHROPIC_API_KEY"]| Command | Description |
|---|---|
nixcage init [--preset <name>] [dir] |
Set up a new cage |
nixcage reinit [--preset <name>] [dir] |
Destroy and re-initialize |
nixcage destroy [dir] |
Remove all nixcage files |
nixcage shell [--debug] [dir] |
Enter interactive sandboxed shell |
nixcage run [--debug] [dir --] <cmd> |
Run a command inside the sandbox |
nixcage status |
Show config, OS, and check dependencies |
nixcage list-presets |
List available presets for init |
nixcage version |
Print version |
Presets generate a pre-configured nixcage.toml tailored for a specific workflow:
nixcage init --preset claude-code| Preset | Description |
|---|---|
claude-code |
Standard sandbox, nodejs + claude-code, ~/.claude writable, impure env |
List available presets with nixcage list-presets.
Pass --debug to shell or run to capture sandbox denials:
nixcage shell --debug
nixcage run --debug -- my-command- Linux: wraps the sandbox with
strace, capturing failed file/network syscalls (requiresstraceinstalled) - macOS: streams
log streamfor Sandbox denial predicates, then summarizes unique denials with counts
This is useful for diagnosing why a command fails inside the cage — the summary shows exactly which paths or operations were blocked.
By default, nix-shell reads and writes your host's /nix/store. nixcage gives you control over this via store_mode in nixcage.toml:
| Mode | Reads host store? | Writes host store? | Speed | Isolation | Platform |
|---|---|---|---|---|---|
shared |
✅ | ✅ | Fastest | None | All |
readonly |
✅ (for cached pkgs) | ❌ | Fast | Good — default | All |
copy |
First run only | ❌ (local copy) | Medium | Strong | Linux only |
isolated |
❌ | ❌ | Slowest first run | Complete | Linux only |
[nix]
store_mode = "readonly" # recommended balanceHow it works: nixcage resolves packages on the host before entering the sandbox, then mounts the store read-only (or a copy) inside the cage. The sandboxed process can use cached packages but can't install new ones or pollute your store.
Note:
copyandisolatedmodes require Linux (bwrap bind mounts). On macOS, they fall back toreadonlywith a warning.
- Filesystem: only
/nix/store(read-only),/tmp(tmpfs), project dir (not mounted) - Network: disabled
- Home: tmpfs (empty)
- Use case: auditing untrusted code
- Filesystem:
/nix/store(ro), project dir (read-write) - Network: enabled
- Home: tmpfs (empty)
- Use case: running Claude Code on a project
- Filesystem:
/nix/store(ro), home (read-only), project dir (read-write) - Network: enabled
- Home: your real home, but read-only
- Use case: tools that need to read ~/.config, ~/.gitconfig, etc.
# Set up a project with the claude-code preset
mkdir ~/ai-project && cd ~/ai-project
nixcage init --preset claude-code
# Allow direnv and launch
direnv allow
nixcage run claudeThe claude-code preset generates a config with nodejs + claude-code packages, ~/.claude writable, SSH/git configs readable, and ANTHROPIC_API_KEY passed through. Customize the generated nixcage.toml as needed.
| Feature | Linux | macOS |
|---|---|---|
| Sandbox engine | bubblewrap (bwrap) | sandbox-exec (Seatbelt) |
| PID isolation | ✅ namespaces | ❌ not available |
| Network isolation | ✅ --unshare-net |
✅ sandbox profile |
| Filesystem isolation | ✅ bind mounts | ✅ Seatbelt rules |
| Resource limits | ✅ cgroups via systemd-run | ❌ not available |
| Store modes | All four modes | shared and readonly only |
When you cd into a nixcage project:
- direnv sees
.envrc .envrccallsnixcage _direnv_hook- The hook exports
NIXCAGE_ACTIVE=1,NIXCAGE_ROOT, etc. - It creates
cageandcagerunshell aliases - You use
nixcage runornixcage shellto enter the sandbox
Note: direnv itself does not sandbox anything — it just auto-loads the environment. The actual isolation happens when you invoke nixcage run or nixcage shell.
GPLv3