DavHau/sbox

Like sudo, but in reverse

Nix

63

105 commits

updated Sep 10, 2026

See the code

README

sbox

sbox

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:

  • Your shell — bash, zsh, fish, or nushell, with your rc files and prompt
  • Your tools — every program on your $PATH, mounted read-only
  • Your history — shell history shared from the host (or per-project, your choice)
  • Your git config — global gitconfig and jj config, ready to commit
  • Your SSH known hosts — so SSH host verification works out of the box
  • Your editor$EDITOR resolved and available
  • Your GPU — NVIDIA, DRI, and AMD ROCm devices passed through
  • Networkingcurl, npm install, nix build — it all just works

And 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.

Try it out

No install needed:

nix shell github:DavHau/sbox#sbox

Then:

sbox           # sandbox the current directory
sbox --help    # see all options

Installation

NixOS Module

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 Manager

# home.nix
{ inputs, ... }:
{
  imports = [ inputs.sbox.homeManagerModules.sbox ];

  programs.sbox = {
    enable = true;
    bind."$HOME/.cache" = {};
  };
}

Configuration

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.

Session state

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).
  • Old sessions are pruned at launch: the newest 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.

Hardening

Shell history

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";

SSH known_hosts

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;

Network mode

  • "isolated" (default) — full internet (IPv4 + IPv6) via pasta
  • "blocked" — loopback only, port forwarding still works
  • "host" — no network isolation
programs.sbox = {
  network = "blocked";
  allowedTCPPorts = [ 5432 ];  # only PostgreSQL
};

Per-sandbox VPN

--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.

Running tests

nix build .#checks.x86_64-linux.vm-sbox
nix build .#checks.x86_64-linux.vm-audio

Direnv Integration

For automatic sandboxing when cd-ing into project directories with .envrc, see the direnv-sandbox sub-project.

License

MIT

Contributors

DavHau

103 commits

aciceri

2 commits

DavHau/sbox

Like sudo, but in reverse

Nix

63

105 commits

updated Sep 10, 2026

See the code

README

sbox

sbox

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:

  • Your shell — bash, zsh, fish, or nushell, with your rc files and prompt
  • Your tools — every program on your $PATH, mounted read-only
  • Your history — shell history shared from the host (or per-project, your choice)
  • Your git config — global gitconfig and jj config, ready to commit
  • Your SSH known hosts — so SSH host verification works out of the box
  • Your editor$EDITOR resolved and available
  • Your GPU — NVIDIA, DRI, and AMD ROCm devices passed through
  • Networkingcurl, npm install, nix build — it all just works

And 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.

Try it out

No install needed:

nix shell github:DavHau/sbox#sbox

Then:

sbox           # sandbox the current directory
sbox --help    # see all options

Installation

NixOS Module

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 Manager

# home.nix
{ inputs, ... }:
{
  imports = [ inputs.sbox.homeManagerModules.sbox ];

  programs.sbox = {
    enable = true;
    bind."$HOME/.cache" = {};
  };
}

Configuration

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.

Session state

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).
  • Old sessions are pruned at launch: the newest 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.

Hardening

Shell history

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";

SSH known_hosts

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;

Network mode

  • "isolated" (default) — full internet (IPv4 + IPv6) via pasta
  • "blocked" — loopback only, port forwarding still works
  • "host" — no network isolation
programs.sbox = {
  network = "blocked";
  allowedTCPPorts = [ 5432 ];  # only PostgreSQL
};

Per-sandbox VPN

--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.

Running tests

nix build .#checks.x86_64-linux.vm-sbox
nix build .#checks.x86_64-linux.vm-audio

Direnv Integration

For automatic sandboxing when cd-ing into project directories with .envrc, see the direnv-sandbox sub-project.

License

MIT

Contributors

DavHau

103 commits

aciceri

2 commits

Languages

Nix

83.5%

Shell

13.2%

Nushell

2.4%