Disposable, CWD-mounted dev VMs on Lima/QEMU, preloaded
with an AI-CLI toolchain — claude, codex, opencode, pi, and
stado — plus the Herdr terminal agent multiplexer, GitHub CLI
(gh), and Homebrew.
Project site: devbox.foobarto.me.
cd into a project, type devbox, and you're in a throwaway Linux VM with the
project mounted and the tools ready. Exit the shell and the VM is gone; the
project's resumable AI sessions remain in a narrow, owner-only host state
directory so the next clone can continue them.
cd ~/code/some-project
devbox # clone → mount CWD → shell in → delete on exit- Disposable. Each box is deleted on exit by default. Toolchains, package installs, credentials, processes, and caches disappear; only the project and its deliberately persistent AI session records remain.
- Isolated. Real dev work in a VM boundary, not your host. Only the mounted folder is visible to the box.
- Fast. A one-time golden image carries the heavy toolchain; each run is a cheap clone, not a re-provision.
- Deterministic. A folder always maps to the same box name, so
--keepboxes are easy to find and re-enter; different folders get different boxes. - Credential-safe. Secrets can stay on the host behind a proxy; the box need never hold them (see Auth).
- Lima ≥ 2.0 (
limactl) with a QEMU or VZ backend python3≥ 3.11 (for the proxy and.devbox.toml; standard library only)waypipeon the host when usingdevbox guiordevbox --guifrom a Wayland session
brew install foobarto/tap/devboxInstalls devbox and devbox-ai-proxy on your PATH. The current stable
GitHub release is
v1.3.1; source
archives are available from that release. Config lives under ~/.config/devbox/
(or $XDG_CONFIG_HOME/devbox).
Upgrade with brew upgrade foobarto/tap/devbox. For a development checkout of
the latest main, use brew install --HEAD foobarto/tap/devbox and upgrade it
with brew upgrade --fetch-HEAD foobarto/tap/devbox.
Check the installed version with devbox --version or
devbox-ai-proxy --version.
git clone https://github.com/foobarto/devbox.git "${XDG_DATA_HOME:-$HOME/.local/share}/devbox"
ln -s "${XDG_DATA_HOME:-$HOME/.local/share}/devbox/bin/devbox" ~/.local/bin/devboxdevbox [DIR] [FLAGS] spin up / attach a box for DIR (default: $PWD)
devbox gui [DIR] [FLAGS] [-- APP [ARGS...]] open a GUI-ready shell or run a guest Wayland app
devbox --gui|-G [DIR] [FLAGS] [-- APP ...] same GUI behavior through the main command
devbox build [--image N] [--force] build/refresh the golden image
devbox ls list devbox instances
devbox destroy NAME | --all | --goldens
devbox sessions [path|clear --yes] [DIR]
| flag | effect |
|---|---|
--image NAME, -i NAME |
base image for this box's golden (default ubuntu-24.04). See Images. |
--cpus N, -j N |
CPUs for this box (default 4). |
--memory SIZE, -M SIZE |
memory for this box, e.g. 12GiB (default 6GiB). |
--disk SIZE, -D SIZE |
disk ceiling, e.g. 80GiB (default 100GiB). Sparse, so it costs only what is written; grow-only. |
--keep, -k |
don't auto-delete the box on exit. |
--ephemeral-sessions, -e |
keep AI session records on this box's disposable disk instead of attaching the project's persistent session store. |
--ssh-agent, -s |
forward the host SSH agent into the box (git/GitHub) and configure signed Git commits. Host private keys never enter the VM — only the agent socket and selected public key are used. |
--proxy[=URL], -p[=URL] |
point the AI CLIs and gh at a host-side credential proxy; credentials stay on the host. Default http://host.lima.internal:4141. |
--traffic-audit[=connect|off], -T |
explicitly route normal web tooling through an audited CONNECT proxy and block direct TCP/UDP 80/443. off removes it from a kept box. It is separate from -a. |
--no-auth, -n |
explicitly disable Devbox-managed proxy, API-key, and copied-credential auth; removes its proxy/key profiles from an existing box. |
--api-keys[=FILE], -K[=FILE] |
inject API keys into the box from an env file (default ~/.config/devbox/api-keys.env). |
--with-creds, -c |
copy host AI-tool credential files into the box (OAuth logins for claude/codex without a proxy). Best-effort. |
--with-agent-config, -g |
copy an allowlisted set of non-secret Claude, Codex, OpenCode, and Stado settings, prompts, rules, and custom agents. Auth, histories, caches, and key directories are excluded; suspected credentials are skipped. |
--gui, -G |
start a GUI-ready Devbox shell through Waypipe; after the optional --, run one guest GUI app instead. |
-a |
shortcut for --with-agent-config --proxy --ssh-agent; it never enables --with-creds or GUI forwarding. |
--mount PATH[:ro|:rw], -m PATH[:ro|:rw] |
mount an extra host path into the box at the same path (default ro). Repeatable; applied at box creation. |
--copy SRC[:DEST], -C SRC[:DEST] |
copy an extra host file/dir into the box (DEST defaults to the basename in $HOME). Repeatable; works on new and existing boxes. |
--name NAME, -N NAME |
override the derived instance name. |
Agent boundaries: a Devbox agent cannot read host files or credentials by default.
--ssh-agentlets it use loaded SSH identities without extracting their private keys;--proxyprimarily prevents credential exfiltration by keeping tokens on the host, while still letting it make permitted provider requests. Give either capability only to trusted code. See agent capability security.
devbox gui and devbox --gui start a GUI-ready Devbox shell whose Wayland
applications display in the host session through
Waypipe over Lima's
per-instance SSH connection. It does not mount the host Wayland socket into the
guest. Put an application after -- when you want Devbox to run just that app.
devbox --gui . # launch GUI apps from the Devbox shell
devbox -G . # short form; -g remains --with-agent-config
devbox gui . -- weston-terminal # run one app and return when it exits
devbox --gui . -a -- code .
devbox gui . -- firefox --new-instanceThe host must be in an active Wayland session and have waypipe installed.
New goldens include the guest-side package; an older kept box installs it on its
first GUI launch. GUI apps use Waypipe's --no-gpu mode because Devbox does not
pass host DRM/render nodes into QEMU guests. This works with already-existing
Devboxes too: guest-side Waypipe is installed on demand. As with a normal
devbox run, the box is removed when the shell or app exits unless --keep is
supplied.
Security: GUI forwarding gives guest applications access to the host Wayland session. Devbox uses Waypipe over SSH and does not mount the host Wayland socket or GPU nodes, but it is not an isolation boundary. Use
--guiand-Gonly with projects and GUI applications you trust. See GUI forwarding security for the threat model and safe-use guidance.
Use -a for the usual agent-config + proxy + SSH-agent setup. Add --gui or
-G explicitly when you also want GUI forwarding, e.g.
devbox --gui -a -m ~/data:ro -C ~/.netrc. Build accepts -i and -f for
--image and --force; destroy accepts -A and -G for --all and
--goldens. Help and version are -h and -V.
devbox buildcreates a persistent golden Lima instance (devbox-golden-<image>) from a base image, provisions the toolchain (Homebrew + the AI and GitHub CLIs + build basics), verifies it, and stops it. One golden per base image.devbox [DIR]derives a deterministic instance name from(image, DIR), then:- if that box exists, attaches to it;
- else clones the golden (
limactl clone, fast — a copy of the already-provisioned disk, no re-install), mountsDIRwritable at the same path, and boots. - applies
DIR/.devbox.tomlif present (per-project setup — seeexamples/.devbox.toml), - mounts the project's owner-only AI session state and links each agent's native transcript/index paths to it,
- drops you into a shell in
DIR, - on exit, deletes the clone — unless that invocation uses
--keep.
Because the name is deterministic, re-running devbox in the same folder finds
the same box. That's what makes "one box per folder" and re-entering --keep
boxes work.
The first devbox run for an image takes longer because it must download the
base image and build, provision, and verify the golden image before it can make
the first clone. This is a one-time cost per golden; run devbox build ahead of
time when you do not want the first interactive launch to wait. Normal later
runs clone the completed golden and should be much faster.
Keep that fast path intact:
- Put slow, deterministic system setup in
[image].provisionand slow user-level setup in[image].provision_user. Those actions run while building the golden, not every time a box starts. - Keep
startsmall, idempotent, and safe to run on every entry. Avoid unconditional package upgrades, dependency downloads, source builds, database migrations, or other network-heavy work there. packagesis convenient and skips packages already present in a kept box, but a new disposable clone still has to install packages that are absent from its golden. Bake large or slow package sets into the golden instead.- Use
--keepwhen retaining guest-only compiler caches, package stores, or services matters more than getting a fresh disposable clone. Do not usedevbox build --forceunless you actually need to replace a golden. - On filesystems without reflink support, cloning copies more disk data. Keep goldens lean and avoid baking disposable caches or build outputs into them.
Resumable session state is persistent by default even when the VM is not.
Devbox gives each canonical project path a separate directory under
${XDG_STATE_HOME:-~/.local/state}/devbox/sessions/, mounts only that directory
read-write, and uses it for Claude Code, Codex (including the isolated
--proxy profile), OpenCode, Pi, and Stado session records. Existing kept
boxes gain the mount on their next entry and restart once if necessary.
The directory name is derived only from the canonical project path, not from
the selected image, golden, or disposable box, so rebuilding or replacing a
golden reattaches the same project's existing sessions.
Devbox refuses to attach the same store to two differently named boxes at once,
which avoids concurrent writers corrupting an agent's session database; destroy
the retained owner first or make the second box ephemeral.
Authentication remains separate: Claude/Codex/OpenCode auth files, provider
keys, and unrelated sessions already present on the host are not placed in the
session store. Session transcripts can still contain prompts, source snippets,
paths, and tool output, so treat the directory as sensitive state. It is mode
0700 and is also a deliberate cross-lifecycle trust channel: instructions in
an old transcript are available again when you resume it.
Use each tool's native resume command after entering the same project. For
example, Claude Code supports claude --continue / claude --resume, and
Codex supports codex resume --last or its session picker. Inspect or remove
the exact project store from the host with:
devbox sessions path .
devbox sessions clear . # confirms interactively; destroy a retained box first
devbox sessions clear --yes . # explicit non-interactive removaldevbox destroy intentionally leaves sessions intact. Use
--ephemeral-sessions (-e) when a run should leave no resumable agent state;
on a kept box that already has the host mount, the flag unlinks the native
agent stores and restarts the box once to remove that mount completely.
Override the host root with DEVBOX_SESSION_DIR when needed.
--image accepts several forms:
devbox --image ubuntu-24.04 # a Lima template name (default)
devbox --image debian-12
devbox --image fedora # dnf-based; base packages adapt
devbox --image archlinux # pacman-based
devbox --image template://ubuntu-25.04
devbox --image ~/vms/kali.yaml # a Lima config file
devbox --image ~/.local/share/lima-images/kali-2026.2-genericcloud-amd64.qcow2Each distinct image gets its own golden. Base-package provisioning auto-detects
apt / dnf / pacman; Homebrew and the AI CLIs are distro-agnostic.
Kali: Lima ships no Kali template, so pass a Kali cloud
.qcow2(or a.yamlreferencing one) via--image.
Installed ≠ authenticated. Three combinable strategies, pick per your setup:
| you want | use | where secrets live |
|---|---|---|
| keys/tokens never enter the box | --proxy |
host only |
| explicitly opt out of Devbox auth | --no-auth |
no new credentials injected |
| API keys (opencode, stado, OpenAI/Codex platform keys) | --api-keys |
copied into the box |
| Claude/Codex subscription OAuth without a proxy | --with-creds |
copied into the box |
| AI CLI settings, prompts, rules, and custom agents without auth | --with-agent-config |
allowlisted non-secret files copied into the box |
| nothing | (default) | you log in interactively inside the box |
The proxy supports API keys plus Claude, Codex, and GitHub CLI logins. A host CLI login
works with --proxy out of the box; its access token is read fresh and never
enters the box. See
proxy/README.md for the full explanation. --proxy is the
recommended default for disposable boxes, and it auto-starts the host proxy
(once, shared across boxes) — no separate launch step. Manage it with
devbox proxy [start|stop|status|refresh]. refresh updates every registered,
running box directly and never reads a project's .devbox.toml.
Every authenticated proxy request is also written to a host-owned, owner-only
audit log. It captures AI prompts/queries and GitHub API request payloads (with
known credential fields redacted), then records the outcome and classifies
GitHub writes such as create, modify, delete, and GraphQL mutations. Inspect it
with devbox proxy audit show, or create a private self-contained report with
devbox proxy audit export [FILE]. These logs can contain source snippets and
prompt content; see proxy audit logging before enabling
--proxy for sensitive work.
Every run pre-answers the first-run gates the AI CLIs would otherwise show: Claude Code's onboarding, folder-trust, and custom-API-key dialogs, and Codex's folder-trust and sign-in prompts. Deciding to start a Devbox for a folder is already the decision to run an agent in it, and the VM is the boundary — so the box comes up ready to work instead of asking the same question again.
Trust is seeded for the mounted project directory only: never $HOME, and never
the read-only paths added with --mount. An answer already on record is left
alone, and a real Codex auth.json copied in with --with-creds is never
overwritten. This also means a repository's own .claude/settings.json and
hooks run unprompted — see
agent capability security.
Use --traffic-audit when you want ordinary guest web tools to be auditable
too, rather than only the built-in AI and GitHub authentication routes:
devbox --traffic-audit # equivalent to --traffic-audit=connect
devbox --keep --traffic-audit # renew a kept box's short-lived capability
devbox --keep --traffic-audit=off # remove its profile and guest firewall ruleIt sets standard HTTP(S)_PROXY/ALL_PROXY variables with a short-lived
Devbox capability, then rejects direct TCP and UDP traffic to ports 80 and 443
inside the guest. Proxy-aware HTTPS traffic therefore uses CONNECT; its audit
record contains destination, timing, and byte counts, but not encrypted paths
or request bodies. Plain HTTP proxy requests can be recorded in detail because
they are not encrypted. Tools that ignore proxy variables, use certificate
pinning, or use non-web ports can fail or fall outside this coverage. The
generic proxy accepts only public destinations, so it cannot be used to reach
host loopback or private-network web services.
This is intentionally explicit and is not included in -a. It is an egress
guard for normal guest applications, not a containment boundary against a
process that has guest root/sudo and can remove the guest firewall. See
proxy audit logging and agent capability
security before granting it to untrusted
code.
For gh, log in once on the host with gh auth login; devbox --proxy gives
the guest CLI a dummy routing marker plus a short-lived Devbox proxy capability,
then injects the host token only inside a GitHub-only TLS proxy. The capability
is not a GitHub token, expires after eight hours, and is renewed every seven
hours by the long-lived host proxy daemon, independently of any devbox shell
or project manifest. It checks recorded box names once a minute, so a host
suspend or long idle is repaired promptly after resume without restarting the
guest. devbox proxy refresh forces the same update immediately.
To prevent an agent from accidentally bypassing the wrapper with Homebrew's
absolute path, --proxy copies the real gh binary into the managed private
wrapper directory and replaces Homebrew's public bin/gh link with the wrapper;
--no-auth restores the normal Homebrew link. This is command-routing hygiene,
not containment against hostile same-user guest code, which can still locate
and execute files it is permitted to access.
GitHub Enterprise hosts are not proxied. Git/GitHub SSH auth is separate: use --ssh-agent. It also enables automatic
SSH-format Git commit signatures through the forwarded agent. Devbox copies the
first public key exposed by ssh-add -L and the host Git name/email, then sets
Git's signing defaults inside the VM; the private key remains in the host
agent. Override the guest Git settings normally if you prefer another signing
method. Newly built golden images fetch GitHub's published SSH host keys from
the GitHub Meta API and place them in ~/.ssh/known_hosts, so GitHub SSH use
does not stop for a first-connection prompt.
--no-auth is the explicit opt-out for a kept box that was previously started
with --proxy or --api-keys; it removes Devbox's profile snippets before the
shell opens. It does not delete credentials created manually inside the VM, and
cannot be combined with --proxy, --api-keys, or --with-creds. It can be
combined with --with-agent-config, which never intentionally copies auth.
Use a .devbox.toml manifest in the project root to select an image, size the
box, bake a toolchain into its golden, install Homebrew packages, and run a
startup command. An explicit CLI flag always wins over the manifest.
start = "test -d node_modules || npm ci"
[image]
location = "ubuntu-24.04"
provision = '''
apt-get install -y --no-install-recommends postgresql-client
'''
provision_user = '''
brew install node python@3.12
'''
[resources]
cpus = 8
memory = "12GiB"
disk = "120GiB"image = "ubuntu-24.04" remains valid shorthand when you only need to pick a
base image. In TOML, every key after a [table] header belongs to that
table — so keep top-level keys above [image] and [resources].
[image].provision and .provision_user are baked into the golden at build
time (root and user mode respectively), not re-run per box — that's where a
heavy distro toolchain belongs, so each new box is a cheap clone rather than a
re-install. Custom provisioning is part of the golden's identity, so one project
can never silently redefine the golden another project clones from; editing it
builds a new golden, and devbox destroy --goldens cleans up the old one.
[resources].disk is a ceiling, not an allocation — Lima's qcow2 is sparse,
so a 120GiB box that has written 4GiB occupies 4GiB on the host. It is grow-only;
a request smaller than the golden's is refused with a warning rather than
silently applied. cpus and memory are applied per-box at clone time, so
changing them never requires rebuilding the golden.
The manifest can also declare ssh_agent, keep, proxy, api_keys,
with_creds, with_agent_config, mounts, copies, and no_auth. Because a project manifest is
repository-controlled input, Devbox prints every requested host-affecting
capability and startup command, then requires an explicit y before creating
or attaching to a box. Command-line flags remain explicit user choices and are
not included in that confirmation. See the complete annotated template:
examples/.devbox.toml.
The old executable .devbox hook is no longer run; Devbox emits a migration
warning when it finds one.
Configuration and generated golden metadata live under ~/.config/devbox/
(override with $DEVBOX_CONFIG_DIR):
~/.config/devbox/
├── config.toml # machine-wide [resources] defaults
├── devbox-golden-<image>.yaml # generated golden configs
├── api-keys.env # for --api-keys / the proxy (gitignored)
├── proxy.config.json # proxy routes (gitignored)
└── proxy-env # optional --proxy env template (uses __PROXY_URL__)
config.toml sets the defaults for every project on this machine:
[resources]
cpus = 8
memory = "12GiB"
disk = "150GiB"Resource precedence is CLI flags > .devbox.toml > config.toml > built-in
defaults (4 CPUs, 6GiB, 100GiB).
Persistent AI transcripts are state rather than configuration, so they live
separately under ${XDG_STATE_HOME:-~/.local/state}/devbox/sessions/ (override
with $DEVBOX_SESSION_DIR). devbox sessions path DIR resolves the exact
per-project directory.
Unit tests cover the pure logic (name derivation, image-stanza + golden-YAML generation, dispatch) and spin up no VM, so they're fast.
brew install bats-core # once
make hooks # once per checkout; enables credential guard
make test # or: bats test/The one limactl validate test skips automatically if limactl isn't
installed.
limactl clonecopies the golden disk. On a reflink-capable filesystem (btrfs/xfs) that's near-instant; elsewhere it's a full copy (still far cheaper than re-provisioning).- Golden images configure
systemd-resolvedto use Lima's virtual host resolver. This keeps DNS working on cloud images such as Kali that accept a DHCP route but omit its DNS option, and it preserves host VPN/split-DNS resolution rather than substituting public resolvers. - New goldens include Stado's Linux sandbox helpers:
bwrapfor process and filesystem isolation, pluspastafor proxy-only host-allowlist networking. On Ubuntu 24.04, Devbox enables AppArmor's dedicated, restricted bwrap profile; it does not disable Ubuntu's global user-namespace restriction, so standaloneunshareremains intentionally unavailable. - A box created before a
devbox build --forcekeeps the old toolchain until youdestroyand recreate it. --ssh-agentenables Lima's agent socket for a new or existing box. An existing box is restarted once if needed, so rundevbox --ssh-agentfrom the project directory to enable it. Your host agent must already be running and have a validSSH_AUTH_SOCK.