Skip to content
hyperstructurePublic

About

The default sandbox fctl's agents run in: Debian with Claude Code, git, the GitHub CLI, Python, Rust, Node.js, TypeScript and Bun, available for arm64 and amd64.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Sandbox

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 bash

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

What's inside

  • Debian 13 (trixie), with LANG=C.UTF-8 and TZ=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 with fctl edit.
  • Python 3.15.0 as python3 and python for 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-unknown target.
  • 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.

Tags

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" }}'

Layout

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/agent and sudo without a password: the sandbox is the boundary. Sessions run as agent, in ~/workspace.
  • The PATH starts with ~/.local/bin (Claude Code and uv's tools), ~/.cargo/bin and ~/.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.sh sets 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, ~/.bun and ~/.cache/ms-playwright, so agents add toolchains, targets, global tools and browsers without sudo (cargo install, bun add -g, playwright install firefox, and for Python uv tool install). Run cargo, rustup, bun and playwright install as agent.
  • Python is marked externally managed: packages go into a virtual environment (uv venv, python3 -m venv) or come with uv run --with. Debian's Python 3.13, which Node.js depends on, stays in /usr/bin, after /usr/local/bin on the PATH.
  • npm leaves out its funding and audit notices and its update check (/usr/etc/npmrc); npm audit still runs when asked.

Browsers

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

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

Building on it

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 agent

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

Building it

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.

The kernel

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

It 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/kvm

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

Questions and changes

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.

License

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.

About

The default sandbox fctl's agents run in: Debian with Claude Code, git, the GitHub CLI, Python, Rust, Node.js, TypeScript and Bun, available for arm64 and amd64.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages