The image fctl runs its agents in: Debian with Claude Code, git, the GitHub CLI, Python, Rust, Node.js, TypeScript, Bun, and Playwright with its browsers, for arm64 and amd64.
docker pull ghcr.io/hyperstructure/sandbox
docker run --rm -it ghcr.io/hyperstructure/sandbox bashfctl hosts pull it themselves: each session runs in a container made from the tag of the host's own version (hosts of fctl 0.3.0 and earlier build their own copy instead). It also works as a base for images with more tools (Building on it).
- Debian 13 (trixie), with
LANG=C.UTF-8andTZ=UTC. - Claude Code, the native build, in
~/.local/bin: the newest release when the image was built. - fctl, the release the tag names, in
/usr/local/bin. Agents name and describe their session withfctl edit. - Python 3.15.0 as
python3andpythonfor every user (uv's build of CPython, in/opt/uv/python), and uv 0.13.4. - Rust 1.99.1 through rustup, with rustfmt, Clippy and the
wasm32-unknown-unknowntarget. - Node.js 26 with npm and corepack 0.36.0 (with its yarn and pnpm shims),
TypeScript 7.0.2 (
tsc) and Bun 1.4.4. - Playwright 1.64.1 (
playwright), with its headless Chromium and its WebKit in~/.cache/ms-playwright, and the system libraries and fonts (emoji, Chinese, Japanese and Thai among them) that its Chromium, Firefox and WebKit need (Browsers). - ffmpeg, ImageMagick (
convert,identify,compare,montage), pngquant and Xvfb (xvfb-run), for recordings, images and headed browsers. - build-essential (gcc, make), cmake, pkg-config and OpenSSL's headers.
- git, the GitHub CLI, ripgrep, fd, jq, ShellCheck, sqlite3, curl, wget, rsync, lsof, fuser, tree, file, unzip, zip, xz and zstd.
The versions are set at the top of provision.sh, which
installs everything.
| Tag | fctl inside |
|---|---|
0.4.0 |
0.4.0. Hosts of that version start their sessions from it. |
0.4 |
The newest 0.4 release. |
latest |
The newest release. |
Each image is built from this repository's main with that release of
fctl and the newest Claude Code. A new fctl release gets its image minutes
after it is published: fctl's Release workflow starts the build. The
newest release's image is rebuilt as things change:
- A change merged to
main: right away. - A new Claude Code release: in the daily run, at 05:41 UTC. fctl turns off Claude Code's auto-updater, so an image's Claude Code is as old as the image.
- Debian's updates: at least once a week.
Images of earlier releases stay as they were last built.
A rebuild moves the tag. To keep one build, pin its digest:
ghcr.io/hyperstructure/sandbox:0.4.0@sha256:…. Its labels say what's in
it:
| Label | Value |
|---|---|
org.opencontainers.image.version, sh.fctl.version |
fctl's version |
sh.fctl.claude-code.version |
Claude Code's version |
org.opencontainers.image.revision |
the commit of this repository it was built from; images built elsewhere have none |
org.opencontainers.image.created |
when it was built |
docker image inspect ghcr.io/hyperstructure/sandbox \
--format '{{ index .Config.Labels "sh.fctl.claude-code.version" }}'fctl and images built FROM this one rely on its layout, which stays the
same from release to release. check.sh checks every build for
it.
- The user is
agent, uid 1000, with the home directory/home/agentand sudo without a password: the sandbox is the boundary. Sessions run asagent, in~/workspace. - The PATH starts with
~/.local/bin(Claude Code and uv's tools),~/.cargo/binand~/.bun/bin, then/opt/fctl/bin, where fctl copies its supervisor, fctl-agent, when a session starts. The image sets it, and/etc/profile.d/fctl.shsets it for login shells. fctl-agent passes Claude the PATH, HOME and the locale but none of the image's other environment variables, so every tool works from the PATH alone. - Rust, Bun and Playwright's browsers belong to
agent, in~/.rustup,~/.cargo,~/.bunand~/.cache/ms-playwright, so agents add toolchains, targets, global tools and browsers without sudo (cargo install,bun add -g,playwright install firefox, and for Pythonuv tool install). Runcargo,rustup,bunandplaywright installasagent. - Python is marked externally managed: packages go into a virtual
environment (
uv venv,python3 -m venv) or come withuv run --with. Debian's Python 3.13, which Node.js depends on, stays in/usr/bin, after/usr/local/binon the PATH. - npm leaves out its funding and audit notices and its update check
(
/usr/etc/npmrc);npm auditstill runs when asked.
Playwright's headless Chromium and its WebKit, which stands in for
Safari, are installed for the playwright on the PATH, with the system
libraries that its Chromium, Firefox and WebKit need. From the command
line:
playwright screenshot --browser webkit --color-scheme dark http://localhost:5173 home.pngA script imports Playwright from a node_modules of its own. Installing
the image's version there finds its browsers in place:
npm install "playwright@$(playwright --version | cut -d' ' -f2)"Other versions, a project's @playwright/test among them, download their
browsers with npx playwright install, and so do the browsers the image
leaves out: Firefox, and Chromium's full build, which headed runs under
xvfb-run and channel: "chromium" take. The libraries are there, so
none of it needs sudo or --with-deps.
An image that installs something as root switches back to agent at the
end:
FROM ghcr.io/hyperstructure/sandbox:0.4
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client \
&& rm -rf /var/lib/apt/lists/*
USER agentTo run it in fctl, name it in a project's Settings → Sandbox → Image, or as
image in a host's host.toml
(Sandbox images in fctl's docs).
Apple silicon hosts need arm64 images.
docker build -t sandbox .
docker run --rm -i sandbox sh -s < check.sh
docker run --rm -i --init sandbox bash -s < check-fctl.sh # fctl's development in it, 10 minutes or so--build-arg FCTL_VERSION=0.4.0 picks the fctl release (default: the
latest) and --build-arg CLAUDE_VERSION=… Claude Code's (default: the
latest). On an fctl host, fctl host image build builds this repository's
main with the host's driver (Apple container or Docker) under the
host's image name, or prepares the Lima VM that sessions are cloned from
with provision.sh.
The Image workflow (.github/workflows/image.yml)
publishes it: each architecture builds on a runner of its own and is
checked with check.sh before the two are tagged as one image. fctl's
Release workflow starts it for each release; it also looks for releases
without an image every hour. Pull requests that change the image build and
check both without publishing.
Sessions that work on fctl itself build, test and run it in the image, so
pull requests that change it also develop fctl in both architectures' builds
(the Develop fctl workflow, .github/workflows/fctl.yml):
check-fctl.sh runs what fctl's CI checks, a session
through its local stack and one through its standalone control plane, an
npm install that must leave its lockfile as it is, and screenshots of its
web app with Playwright. It checks hyperstructure/fctl's branch of the pull
request's name when there is one (an fctl session that changes both
repositories names their branches alike), and main otherwise.
fctl's firecracker driver runs each session in a
Firecracker microVM, made from this
image, which boots a kernel published from here too:
docker buildx imagetools inspect ghcr.io/hyperstructure/sandbox-kernel:6.18.59It is Linux's long-term release in kernel/VERSION,
configured as Firecracker configures its own CI's guests
(kernel/microvm-x86_64.config and
kernel/microvm-aarch64.config, from
Firecracker's resources/guest_configs): every driver built in, with ext4,
overlayfs, vsock, TAP devices, cgroups, nftables and the balloon's free
page reporting. kernel/fctl.config adds only
/proc/config.gz. The image holds the kernel as /vmlinux, and its
configuration as /vmlinux.config.
| Tag | Kernel |
|---|---|
6.18.59 |
Linux 6.18.59. Each fctl release pins the one its hosts boot. A tag never moves: a change to how the kernel is built takes a new one, 6.18.59-2 for the same release. |
6.18 |
The newest 6.18 built here. |
kernel/build.sh # out/vmlinux, on Linux, for this machine
docker build --output type=local,dest=out kernel # the same, in a container
kernel/check.sh out/vmlinux # boots it in Firecracker, with /dev/kvmThe Kernel workflow (.github/workflows/kernel.yml)
builds it for both architectures, boots it where the runner has KVM (amd64's),
and publishes a version of kernel/VERSION that isn't published yet when
kernel/ changes on main. A new kernel release is a change to
kernel/VERSION and kernel/SHA256SUMS (from kernel.org's
sha256sums.asc), and then one to the version fctl pins.
Open an issue for a tool the image lacks or something that doesn't work in it, and report vulnerabilities privately: SECURITY.md. AGENTS.md describes how changes are made, for people and for agents.
The files in this repository are under the MIT license, but for
kernel/microvm-*.config, which come from Firecracker, under the Apache
License 2.0. The image contains software under its own licenses, among them
Claude Code, which
Anthropic's terms
govern. The kernel is Linux, under the GPL 2.0: its source is the release on
kernel.org that kernel/VERSION names, built with kernel/build.sh.