Like sudo, but in reverse. Sandboxing you won't notice — until you need it.
Type sbox, and your shell is sandboxed. The project stays writable, the rest of your system disappears — but it still feels like home, because sbox brings along everything that makes your shell yours:
$PATH, mounted read-only$EDITOR resolved and availablecurl, npm install, nix build — it all just worksAnd when you need to lock things down or open them up, everything is configurable: bind mounts, port forwarding, network modes, audio passthrough, persistent state, and more.
Under the hood, sbox uses bubblewrap for isolation and pasta (passt) for user-mode networking — dual-stack (IPv4 + IPv6), no root, no daemon, no Docker.
For automatic sandboxing of direnv environments, see the direnv-sandbox integration.
No install needed:
nix shell github:DavHau/sbox#sbox
Then:
sbox # sandbox the current directory
sbox --help # see all options
Add sbox as a flake input and enable the NixOS module:
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
sbox.url = "github:DavHau/sbox";
};
outputs = { nixpkgs, sbox, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
sbox.nixosModules.sbox
{
programs.sbox = {
enable = true;
# optional configuration:
bind."$HOME/.cache" = {};
allowedTCPPorts = [ 5432 ];
network = "isolated";
};
}
];
};
};
}
# home.nix
{ inputs, ... }:
{
imports = [ inputs.sbox.homeManagerModules.sbox ];
programs.sbox = {
enable = true;
bind."$HOME/.cache" = {};
};
}
All options live under programs.sbox:
programs.sbox = {
enable = true;
# Extra paths to mount read-write inside the sandbox
bind."$HOME/.cache" = {};
# Mount a GitHub-only SSH key into the sandbox (read-only)
bindReadOnly."$HOME/.ssh/id_ed25519_github".to = "$HOME/.ssh/id_ed25519";
bindReadOnly."$HOME/.ssh/id_ed25519_github.pub".to = "$HOME/.ssh/id_ed25519.pub";
# Mount extra device nodes with device access
# /dev/dri/*, /dev/nvidia*, /dev/kfd, and /dev/kvm are mounted automatically.
bindDevices = [ "/dev/net/tun" ];
# Persist paths across sandbox sessions
persist = [ "$HOME/.claude" ];
# Keep each session's $HOME and /tmp on disk (default). Set
# sessionState.enable = false for tmpfs-only sandboxes.
sessionState.keepLast = 5;
# Forward host TCP ports into the sandbox
allowedTCPPorts = [ 5432 6379 ];
# Expose sandbox TCP ports to the host
exposedTCPPorts = [ 3000 8000 ];
# Mount the parent directory of the project inside the sandbox
allowParent = "off"; # "off" (default), "read", or "write"
# Join an existing sbox in the same project directory (default: true)
share = true;
# Shell history mode: "host" (shared, default), "project" (per-project), "off"
shareHistory = "host";
# Seed the sandbox with a writable copy of ~/.ssh/known_hosts (default: true)
shareKnownHosts = true;
# Network mode: "isolated" (default), "blocked", or "host"
network = "isolated";
# Run a command in the sandbox's network namespace before it starts,
# e.g. to route the session through a per-sandbox WireGuard VPN
networkSetup = "sbox-net-wireguard $HOME/.config/wireguard/mullvad.conf";
# Allow audio passthrough (PipeWire)
allowAudio = false;
# Extra packages on PATH inside the sandbox
packages = [];
# Environment variables inside the sandbox
environment = {};
# Shell commands to run when entering the sandbox
shellHook = "";
};
Any options configured via the NixOS/HM module are baked into the sbox wrapper, so the command inherits your system configuration by default. Extra flags passed on the command line are appended on top and take precedence: for modal flags like --network, the last occurrence wins, so sbox --network isolated overrides a module-configured network = "blocked". On the command line, --allow-parent accepts read (alias ro) or write (alias rw); any other value is rejected with an error.
By default, every sandbox session keeps its $HOME and /tmp contents on disk after it exits, under ${XDG_STATE_HOME:-~/.local/state}/sbox/projects/<project path>/sessions/. Each session still starts fresh — previous sessions' state is never mounted automatically.
sbox --resume opens an interactive picker of this project's sessions (newest first); sbox --resume N reattaches the N-th most recent (1 = last), and sbox --resume <ID> an exact session by ID. Sessions currently in use cannot be resumed.sbox --ephemeral runs a tmpfs-only sandbox that leaves nothing behind (module equivalent: sessionState.enable = false).sessionState.keepLast (default 5, CLI --keep-last N) are kept per project, live sessions are never pruned, and 0 keeps everything.sbox state list shows saved sessions across projects (the current directory's project first, marked (current)), sbox state path prints the current project's state directory, and sbox state gc --older-than Nd (or --all) bulk-cleans old session state.State from the legacy --persist-based layout is migrated to the new layout automatically on first launch.
Full details and examples: docs/session-state.md.
By default shareHistory = "host": the host's bash, zsh, fish, and nushell history files are bind-mounted read-write into every sandbox. Set shareHistory = "project" for per-project history, or "off" to disable:
programs.sbox.shareHistory = "project";
By default shareKnownHosts = true: the sandbox gets a writable copy of the host's ~/.ssh/known_hosts, so SSH host verification works and new host keys can be recorded — without ever touching the host file. In session-state mode the copy lives in the session's $HOME and survives --resume; ephemeral sandboxes discard it on exit. Disable with:
programs.sbox.shareKnownHosts = false;
"isolated" (default) — full internet (IPv4 + IPv6) via pasta"blocked" — loopback only, port forwarding still works"host" — no network isolationprograms.sbox = {
network = "blocked";
allowedTCPPorts = [ 5432 ]; # only PostgreSQL
};
--net-setup CMD (module: networkSetup) runs CMD inside the sandbox's
network namespace after networking is up and before the sandbox starts. The
bundled sbox-net-wireguard helper turns a wg-quick style WireGuard config
(Mullvad exports work as-is) into a leak-safe tunnel: routes and an
nftables filter in the sandbox's namespace let nothing but the encrypted
stream to the endpoint leave, DNS goes through the tunnel, and sandboxed
code cannot change any of it. A hook that fails aborts the launch.
sbox --net-setup 'sbox-net-wireguard ~/.config/wireguard/mullvad-se-sto.conf'
Mullvad users can skip the config file: import the optional sbox-mullvad
module and name a location on the command line. The first launch registers
a device key with the account of the local Mullvad app.
imports = [ sbox.nixosModules.sbox sbox.nixosModules.sbox-mullvad ]; # or homeManagerModules
programs.sbox.mullvad.enable = true;
sbox --net-setup 'sbox-net-mullvad sg'
Details, the hook contract for custom scripts, and the leak model: docs/net-setup.md.
nix build .#checks.x86_64-linux.vm-sbox
nix build .#checks.x86_64-linux.vm-audio
For automatic sandboxing when cd-ing into project directories with .envrc, see the direnv-sandbox sub-project.
MIT
Nix
83.5%
Shell
13.2%
Nushell
2.4%
Like sudo, but in reverse. Sandboxing you won't notice — until you need it.
Type sbox, and your shell is sandboxed. The project stays writable, the rest of your system disappears — but it still feels like home, because sbox brings along everything that makes your shell yours:
$PATH, mounted read-only$EDITOR resolved and availablecurl, npm install, nix build — it all just worksAnd when you need to lock things down or open them up, everything is configurable: bind mounts, port forwarding, network modes, audio passthrough, persistent state, and more.
Under the hood, sbox uses bubblewrap for isolation and pasta (passt) for user-mode networking — dual-stack (IPv4 + IPv6), no root, no daemon, no Docker.
For automatic sandboxing of direnv environments, see the direnv-sandbox integration.
No install needed:
nix shell github:DavHau/sbox#sbox
Then:
sbox # sandbox the current directory
sbox --help # see all options
Add sbox as a flake input and enable the NixOS module:
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
sbox.url = "github:DavHau/sbox";
};
outputs = { nixpkgs, sbox, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
sbox.nixosModules.sbox
{
programs.sbox = {
enable = true;
# optional configuration:
bind."$HOME/.cache" = {};
allowedTCPPorts = [ 5432 ];
network = "isolated";
};
}
];
};
};
}
# home.nix
{ inputs, ... }:
{
imports = [ inputs.sbox.homeManagerModules.sbox ];
programs.sbox = {
enable = true;
bind."$HOME/.cache" = {};
};
}
All options live under programs.sbox:
programs.sbox = {
enable = true;
# Extra paths to mount read-write inside the sandbox
bind."$HOME/.cache" = {};
# Mount a GitHub-only SSH key into the sandbox (read-only)
bindReadOnly."$HOME/.ssh/id_ed25519_github".to = "$HOME/.ssh/id_ed25519";
bindReadOnly."$HOME/.ssh/id_ed25519_github.pub".to = "$HOME/.ssh/id_ed25519.pub";
# Mount extra device nodes with device access
# /dev/dri/*, /dev/nvidia*, /dev/kfd, and /dev/kvm are mounted automatically.
bindDevices = [ "/dev/net/tun" ];
# Persist paths across sandbox sessions
persist = [ "$HOME/.claude" ];
# Keep each session's $HOME and /tmp on disk (default). Set
# sessionState.enable = false for tmpfs-only sandboxes.
sessionState.keepLast = 5;
# Forward host TCP ports into the sandbox
allowedTCPPorts = [ 5432 6379 ];
# Expose sandbox TCP ports to the host
exposedTCPPorts = [ 3000 8000 ];
# Mount the parent directory of the project inside the sandbox
allowParent = "off"; # "off" (default), "read", or "write"
# Join an existing sbox in the same project directory (default: true)
share = true;
# Shell history mode: "host" (shared, default), "project" (per-project), "off"
shareHistory = "host";
# Seed the sandbox with a writable copy of ~/.ssh/known_hosts (default: true)
shareKnownHosts = true;
# Network mode: "isolated" (default), "blocked", or "host"
network = "isolated";
# Run a command in the sandbox's network namespace before it starts,
# e.g. to route the session through a per-sandbox WireGuard VPN
networkSetup = "sbox-net-wireguard $HOME/.config/wireguard/mullvad.conf";
# Allow audio passthrough (PipeWire)
allowAudio = false;
# Extra packages on PATH inside the sandbox
packages = [];
# Environment variables inside the sandbox
environment = {};
# Shell commands to run when entering the sandbox
shellHook = "";
};
Any options configured via the NixOS/HM module are baked into the sbox wrapper, so the command inherits your system configuration by default. Extra flags passed on the command line are appended on top and take precedence: for modal flags like --network, the last occurrence wins, so sbox --network isolated overrides a module-configured network = "blocked". On the command line, --allow-parent accepts read (alias ro) or write (alias rw); any other value is rejected with an error.
By default, every sandbox session keeps its $HOME and /tmp contents on disk after it exits, under ${XDG_STATE_HOME:-~/.local/state}/sbox/projects/<project path>/sessions/. Each session still starts fresh — previous sessions' state is never mounted automatically.
sbox --resume opens an interactive picker of this project's sessions (newest first); sbox --resume N reattaches the N-th most recent (1 = last), and sbox --resume <ID> an exact session by ID. Sessions currently in use cannot be resumed.sbox --ephemeral runs a tmpfs-only sandbox that leaves nothing behind (module equivalent: sessionState.enable = false).sessionState.keepLast (default 5, CLI --keep-last N) are kept per project, live sessions are never pruned, and 0 keeps everything.sbox state list shows saved sessions across projects (the current directory's project first, marked (current)), sbox state path prints the current project's state directory, and sbox state gc --older-than Nd (or --all) bulk-cleans old session state.State from the legacy --persist-based layout is migrated to the new layout automatically on first launch.
Full details and examples: docs/session-state.md.
By default shareHistory = "host": the host's bash, zsh, fish, and nushell history files are bind-mounted read-write into every sandbox. Set shareHistory = "project" for per-project history, or "off" to disable:
programs.sbox.shareHistory = "project";
By default shareKnownHosts = true: the sandbox gets a writable copy of the host's ~/.ssh/known_hosts, so SSH host verification works and new host keys can be recorded — without ever touching the host file. In session-state mode the copy lives in the session's $HOME and survives --resume; ephemeral sandboxes discard it on exit. Disable with:
programs.sbox.shareKnownHosts = false;
"isolated" (default) — full internet (IPv4 + IPv6) via pasta"blocked" — loopback only, port forwarding still works"host" — no network isolationprograms.sbox = {
network = "blocked";
allowedTCPPorts = [ 5432 ]; # only PostgreSQL
};
--net-setup CMD (module: networkSetup) runs CMD inside the sandbox's
network namespace after networking is up and before the sandbox starts. The
bundled sbox-net-wireguard helper turns a wg-quick style WireGuard config
(Mullvad exports work as-is) into a leak-safe tunnel: routes and an
nftables filter in the sandbox's namespace let nothing but the encrypted
stream to the endpoint leave, DNS goes through the tunnel, and sandboxed
code cannot change any of it. A hook that fails aborts the launch.
sbox --net-setup 'sbox-net-wireguard ~/.config/wireguard/mullvad-se-sto.conf'
Mullvad users can skip the config file: import the optional sbox-mullvad
module and name a location on the command line. The first launch registers
a device key with the account of the local Mullvad app.
imports = [ sbox.nixosModules.sbox sbox.nixosModules.sbox-mullvad ]; # or homeManagerModules
programs.sbox.mullvad.enable = true;
sbox --net-setup 'sbox-net-mullvad sg'
Details, the hook contract for custom scripts, and the leak model: docs/net-setup.md.
nix build .#checks.x86_64-linux.vm-sbox
nix build .#checks.x86_64-linux.vm-audio
For automatic sandboxing when cd-ing into project directories with .envrc, see the direnv-sandbox sub-project.
MIT
Nix
83.5%
Shell
13.2%
Nushell
2.4%