Preparing a rootfs
Every sandbox runs on a prepared rootfs: an ordinary directory on your host holding a complete Linux root filesystem, with your shell, your tools and your agents already installed. hort never builds, pulls or installs anything. It mounts that directory as the read-only base of every sandbox and gives each sandbox its own disposable writable layer on top, so ten sandboxes share one copy on disk.
You prepare it once, and again whenever you want to change what is inside.
What a rootfs must provide
- A shell at
/bin/sh. Sessions drop into a shell, andhort uprefuses a rootfs without/bin/sh. - Tolerance of an arbitrary user. hort maps your own host user into the sandbox. Do not rely on a fixed
USERor uid; do not set one. - Your agents, baked in. Claude Code, Aider, Codex CLI, Gemini CLI: whatever you run must already be installed. hort installs nothing.
- A world-writable
/workdir. It is where hort binds the worktree. Create it withmkdir -p /workdir && chmod 1777 /workdir. hort refuses a rootfs whose/workdiris not world-writable. - No command that starts an agent. hort starts its own idle process as the container's first process. An image
CMDorENTRYPOINTis ignored.
Useful in practice, though not checked: ca-certificates (anything using TLS needs them), git only if tools you run expect the binary to exist (git commands still do not work inside, see Concepts), and the shell you use on the host if you want it inside (see the shell key).
Agents that refuse to run as root
Inside a sandbox your session is uid 0 of its own user namespace. On the host it is your own unprivileged user, and it holds no capabilities, so being "root" in there buys nothing on the machine. Some agents only look at the uid and refuse to run in their unrestricted mode. Claude Code is one:
--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons
Claude Code accepts that mode as root when IS_SANDBOX=1 is set. Set it in the rootfs, where the login shell reads it. An image ENV line does not survive the export, which keeps files and drops image settings.
- For
shandbashlogin shells:printf 'export IS_SANDBOX=1\n' > /etc/profile.d/hort-sandbox.sh - For fish:
mkdir -p /etc/fish/conf.d && printf 'set -gx IS_SANDBOX 1\n' > /etc/fish/conf.d/hort-sandbox.fish
Other agents have their own switch, or none; check each one's documentation.
Building one from a Dockerfile
A Dockerfile is a convenient way to describe the environment. The repository ships example.Dockerfile: a Node 22 Debian base with bash, git, curl, ca-certificates, Claude Code and IS_SANDBOX=1. Build it with podman, then export the container's filesystem into a directory:
podman build -t hort-devbox -f example.Dockerfile .
cid=$(podman create hort-devbox)
mkdir -p ~/.local/share/hort/devbox
podman export "$cid" | tar -x -C ~/.local/share/hort/devbox
podman rm "$cid"
chmod 1777 ~/.local/share/hort/devbox/workdir
The final chmod is not optional: extracting as a normal user drops the mode the image gave /workdir.
Then point hort at the directory, in ~/.config/hort/config.json or a project's .hort.json:
{
"rootfs": "~/.local/share/hort/devbox",
}
and check it:
$ hort doctor
...
configuration
rootfs ready
Use an absolute path or one starting with ~/. A relative path is read against whatever directory you run hort from.
debootstrap or umoci unpack produce a rootfs directory just as well. Whatever the tool, the result has to meet the five requirements above.
A minimal Alpine rootfs
For trying hort out without an agent, a few megabytes of Alpine are enough:
FROM docker.io/library/alpine:3.22
RUN apk add --no-cache ca-certificates iproute2
RUN mkdir -p /workdir && chmod 1777 /workdir
Build and export it exactly as above, into a directory of its own.
Changing a rootfs
A running sandbox uses its rootfs as the lower layer of a mounted overlay, and changing that layer under a live overlay is undefined. Never modify a rootfs directory that a live sandbox is using. Export the new version into a new directory (for example ~/.local/share/hort/devbox-2), point the configuration at it, and new sandboxes pick it up. Existing sandboxes keep running on the old one until you hort down them; then delete the old directory.
Also export into a directory of its own, never into a directory that already holds another rootfs.
What hort adds on top
You do not need to provide these; hort sets them up in every sandbox:
- A writable home at
/home/hortand a writable/tmp, both in memory and gone when the sandbox goes down, withHOMEand theXDG_*directories pointing into that home. - Your read-only dotfiles and credentials, placed under
/home/hortat the same relative path they have under your host home. /etc/resolv.confin an open-egress sandbox (and none under an allowlist). It is written into the sandbox's own layer and shadows whatever the rootfs carries.- The hostname, set to the sandbox name.
- For an agent with
notify.stopHook, the Claude Code settings file that reports completion.
The rootfs itself is never written to.