shandbox

Last update 30 May 2026. History↓

shandbox is a simple Linux sandboxing script that serves my needs well. Perhaps it works for you too? No dependencies between a shell and util-linux (unshare and nsenter).

In short, it aims to provide fairly good isolation for personal files (i.e. your $HOME) while being very convenient for day to day use. It's designed to be run as an unprivileged user - as long as you can make new namespaces you should be good to go. By default /home/youruser/sandbox shows up as /home/sandbox within the sandbox, and other than standard paths like /usr, /etc, /tmp, and so on it's left for you to either copy things into the sandbox or expose them via a mount. There's a single shared sandbox (i.e. processes within the sandbox can see and interact with each other, and the exposed sandbox filesystem is shared as well), which trades off some ease of use for the security you might get with a larger number of more targeted sandboxes. On the other hand, you only gain security from a sandbox if you actually use it and this is a setup that offers very low friction for me. The network is not namespaced (although this is something you could change with a simple edit). If you do want more than one sandbox environment, see the relevant section below.

Usability is both subjective and highly dependent on your actual use case, so the tradeoffs may or may not align with what is interesting for you! Bubblewrap is an example of a mature alternative unprivileged sandboxing tool that offers a lot of configurability as well as options with greater degrees of sandboxing. Beyond that, look to Firecracker based solutions or gvisor. shandbox obviously aims to provide a reasonable sandbox as much as Linux namespaces alone are able to offer, but if you're looking for a security property stronger than "makes it harder for something to edit or access unwanted files" it's down to you to both carefully review its implementation and consider alternatives. The recent spate of disclosed local privilege escalation vulnerabilities is helpful to keep in mind as a reminder of the limits of this namespacing based approach.

Usage example

$ shandbox run uvx pycowsay
initialised sandbox at /home/asb/sandbox
created default ssh config at /home/asb/sandbox/.ssh/config
to add an init hook, create an executable script at: /home/asb/sandbox/.shandbox_meta/init
started (pid 1589289)
Installed 1 package in 5ms

  ------------
< Hello, world >
  ------------
   \   ^__^
    \  (oo)\_______
       (__)\       )\/\
           ||----w |
           ||     ||
$ shandbox status
running (pid 1589364)

log:
  2026-02-11 13:02:51 stopped
  2026-02-11 13:05:06 started (pid 1589289)
$ shandbox add-mount ~/repos/medley
mounted /home/asb/repos/medley -> /home/sandbox/medley
$ shandbox run ls -lh /home/sandbox/medley/README.md
-rw-r--r-- 1 sandbox users 2.7K Feb 11 20:02 /home/sandbox/medley/README.md
$ shandbox run touch /home/sandbox/medley/write-attempt
touch: cannot touch '/home/sandbox/medley/write-attempt': Read-only file system
$ shandbox remove-mount /home/sandbox/medley
unmounted /home/sandbox/medley
$ shandbox add-mount --read-write ~/repos/medley
mounted /home/asb/repos/medley -> /home/sandbox/medley
$ shandbox run touch /home/sandbox/medley/write-attempt
$ shandbox list-mounts
/home/sandbox                /dev/mapper/root[/home/asb/sandbox]
/home/sandbox/medley         /dev/mapper/root[/home/asb/repos/medley]

shandbox enter will open a shell within the sandbox for easy interactive usage. As a convenience, if the current working directory is in $HOME/sandbox (e.g. $HOME/sandbox/foo) then the working directory within the sandbox for shandbox run or shandbox enter will be set to the appropriate path within the sandbox (/home/sandbox/foo in this case). i.e., the case where this mapping is trivial. Environment variables are not passed through.

You can also explicitly control the working directory used by shandbox run or shandbox enter by setting SB_PWD to an absolute in-sandbox path. If SB_PWD isn't set, paths within the sandbox home are translated to /home/sandbox/..., and some host paths that are directly visible in the sandbox (such as /tmp, /usr, /etc, and similar) are used as-is.

Functionality overview

Minimum requirements and Ubuntu compatibility

Two core requirement are the ability to create a new user namespace, and a recent enough util-linux release (2.41 or newer should work). The earliest Ubuntu release known to work is 25.10 (25.04 won't work, as its util-linux is too old).

Recent Ubuntu releases restrict unprivileged user namespaces through AppArmor, meaning additional settings are required. Chromium's AppArmor user namespace restrictions notes describe this policy and workarounds.

To change the relevant setting non-persistently:

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

You can alternatively add an AppArmor profile covering the path you install shandbox to. e.g. put this at /etc/apparmor.d/shandbox and then do sudo service apparmor reload:

abi <abi/4.0>,
include <tunables/global>

profile shandbox /usr/local/bin/shandbox flags=(unconfined) {
  userns,
}

Self-contained sandbox directories

A sandbox is represented by a normal directory, defaulting to $HOME/sandbox. The files visible as /home/sandbox live directly in that directory, and shandbox's own state lives under .shandbox_meta inside it. That means a sandbox is self-contained: you can create another one with shandbox new ~/other-sandbox, select it by setting SANDBOX_DIR (using the absolute path it prints, or a shell-expanded path such as ~/other-sandbox), and it will have its own root layout, runtime directory, pid files, log, init hook, and ssh socket directory.

For example:

$ shandbox new ~/other-sandbox
$ SANDBOX_DIR=~/other-sandbox shandbox run pwd
/home/sandbox
$ shandbox new ~/throwaway-sandbox
$ SANDBOX_DIR=~/throwaway-sandbox shandbox status
stopped

Sandboxes in different SANDBOX_DIR have independent state and home directories. The contents of .shandbox_meta is hidden from inside the sandbox by mounting an empty tmpfs over it. I don't personally use separate sandboxes outside of testing purposes. But it's simple functionality to provide and it's easy to imagine cases where this is useful.

Sharing ssh connections

One aspect of this I'm pretty pleased with is the mechanism for exposing an ssh connection without having to share any key material or password, or set up credentials specifically for the sandbox. shandbox share-ssh will create an ssh ControlMaster and expose the control socket in the sandbox home directory. The sandbox can use this connection for as long as that ssh process lives.

e.g.:

$ shandbox share-ssh buildbox user@example.com
shandbox share-ssh: connecting (user@example.com) using /home/asb/sandbox/.ssh/sockets/ext%buildbox
shandbox share-ssh: connected
shandbox share-ssh: from inside the sandbox, use ssh ext%buildbox

Then from inside the sandbox:

ssh ext%buildbox

The ext%... name format is recognised thanks to a config fragment installed in ~/.ssh/config within the sandbox.

Init hooks

The main way of customising sandbox setup outside of hacking on the shandbox script yourself is through an "init script" which will be called for every shandbox start (implicit or explicit). Just place your script in .shandbox_meta/init, and if you want a default one that is copied into that location for you when creating a new sandbox then put it in $XDG_CONFIG_HOME/.shandbox/default-init.

As the script is executed for each shandbox start, you should either ensure it is idempotent or have it create and check for some marker file so it exits early for subsequent invocations.

The following environment variables are passed through:

A trivial example that adds a default mount:

#!/bin/sh

"$SHANDBOX_SELF" add-mount ~/repos/src src

Implementation approach

The core sandboxing functionality is provided by the Linux namespaces functionality exposed by unshare and nsenter. The script's implementation should be quite readable but I'll try to summarise some key points here.

The goal is that:

To implement that:


Article changelog