Throwaway runtime buckets for AI agents.
Resolve, fetch, and run any CLI tool in an isolated ephemeral environment — without installing it globally.
Borrowing concepts from pkgx (bottle format, dist server, env composition, companion deps) and exosphere's exo-hydra provisioning pipeline (resolve → install → compose → exec).
AI agents need ad-hoc runtimes: "run this with Node 20", "test this with Python 3.11", "build with Go 1.22 + CMake". Installing these globally pollutes the host and creates version conflicts.
buckets gives each command its own throwaway runtime — fetched on demand
from dist.pkgx.dev, cached locally, cleaned up when done.
Any agent with shell access (bro-cli's bash tool, a dispatched Claude Code
horse, any runner) can call buckets directly once it's on PATH — it's a
plain CLI, no MCP/tool registration needed. For concurrent fleet agents,
prefer bucket-bridge (jokersquad/bin/bucket-bridge, symlinked to
~/.local/bin/) over calling buckets directly: it's a transparent wrapper
that points every agent at one shared BUCKETS_CACHE_DIR
(~/.cache/squadron-buckets) instead of each agent maintaining its own
cache, so N agents resolving the same spec (node@20) don't each
redundantly download the same bottle. Safe under concurrency — see
install.rs's atomic-rename install.
# Run one-off commands
buckets run node@20 -- script.js --flag arg
buckets run python@3.11 -- python3 -c "print('hello from a bucket')"
buckets run rust@latest -- rustc --version
# Multi-package environments
buckets run node@20 python@3.11 -- node -e "console.log('and python is at', process.env.PATH)"
buckets run go@1.22 cmake@latest -- go version
# Language registries (BUCKETS-15) — install into the shared cellar
buckets run pypi:six@1.16 -- python3 -c "import six; print(six.__version__)"
buckets run npm:is-number@7 -- node -e "console.log(require('is-number')(3))"
buckets env pypi:requests@2.31 npm:is-number@7 # PYTHONPATH + NODE_PATH
# Open an interactive shell with the runtime in PATH
buckets shell node@20
buckets shell python@3.11 --shell /bin/zsh
# Print the composed environment as shell exports (composable)
eval "$(buckets env node@20)"
buckets env node@20 python@3.11 --json
# See what a spec resolves to (no installation)
buckets info node@20
buckets info git@latest
# List cached installations
buckets list
# Reclaim disk space — the cache only grows by default, this is how you
# prune it back. A bare `buckets clean` with no target is refused (the
# cache dir is often shared across concurrent agents); pass a project,
# --all, or both --all and --older-than.
buckets clean node@18 # remove one version
buckets clean node # remove every version of a project
buckets clean --all --dry-run # preview a full wipe first
buckets clean --all --older-than 30d # remove entries not touched in 30 days
# Persistent sessions — an OverlayFS mount shared across multiple `exec`
# calls, so files written by one command survive into the next (unlike
# `run`, which is a fresh sandbox every time).
buckets session start node@20
buckets session exec <session-id> -- node another-script.js
buckets session list
buckets session stop <session-id> --purge
# Isolated virtual networks — buckets in the same net share a private
# loopback (127.0.0.1) with no host network access, and a port can be
# forwarded in from the host.
buckets net create dev-net
buckets net run dev-net node@20 -- node server.js
buckets net expose dev-net 8080:3000
buckets net rm dev-net
# Herds — a fleet of bucket replicas with restart/scale/health-reconcile,
# each replica getting HERD_NAME/HERD_REPLICA_INDEX env vars to self-identify.
buckets herd deploy worker --spec node@20 --replicas 5 --net herd-net -- node worker.js
buckets herd status worker
buckets herd scale worker --replicas 8
buckets herd stop worker
# Build/test/run a real project — clone (git URL) or use (local path),
# detect the build system, resolve the toolchain it needs, build inside
# a sandboxed bucket. Doesn't touch the host filesystem outside the
# project dir + the resolved toolchain's own cache.
buckets build /path/to/repo
buckets build https://github.com/owner/repo --test
buckets build . --test --run
# Ephemeral worktrees — a task gets its own working copy at a fresh
# branch (git worktree add, not a full clone — cheap, shares the repo's
# object store). Build/test it like any other local path. "Destroyed
# once you merge": removing an unmerged worktree's branch is refused by
# git itself (git branch -d's own safety check) unless --force.
buckets worktree create /path/to/repo my-task-branch
buckets build "$(buckets worktree create /path/to/repo my-task-branch)" --test
buckets worktree remove /path/to/repo /path/to/repo-my-task-branch my-task-branch
buckets worktree list /path/to/repo
# GUI buckets — run a GUI app against a fresh, isolated Xvfb X server
# (not the host's real display). Only the throwaway server's own socket
# + a session-scoped Xauthority cookie are visible inside the sandbox.
buckets gui --screenshot /tmp/out.png --timeout 5 -- glxgears
buckets gui node@20 --width 1280 --height 800 -- node gui-script.js
# Site buckets — open a URL with a real, OS-enforced per-origin storage
# sandbox (revives the intent behind exosphere-apps' site-capsulizer,
# which was found unenforced scaffolding — see the README's "Site
# buckets" section below). Defaults to a headless browser binary
# (surfer) on PATH; --gui runs a windowed one (super-surfer) in a
# fresh Xvfb.
buckets site https://example.com
buckets site https://example.com --gui --screenshot /tmp/site.png --timeout 5
buckets site https://example.com --incognito # ephemeral storage, removed on exitbuckets build <dir> --sched routes a cargo build through
buildsched's admission-controlled pipeline (bob): per-crate
jobs gated on memory watermarks, PSI pressure, and disk headroom instead of one
monolithic cargo build. Opt-in at compile time — the buildsched cargo
feature (OFF by default, cargo build --features buildsched) adds a path dep
on the sibling buildsched repo; the default build stays fully self-contained.
Cargo projects with a single [package] only: virtual workspaces and non-cargo
projects print a one-line notice and fall back to the normal build path. Sched
builds always run unsandboxed (the pipeline spawns its own cargo subprocess
tree, which can't live inside the per-command bwrap wrap); the bucket toolchain
env is still injected into every pipeline subprocess.
run/shell/build all execute under bubblewrap
(bwrap) — a fresh mount + PID namespace, not just an isolated toolchain
version. Only the resolved toolchain's own install dirs (read-only) and
the invocation/project directory (read-write) are visible inside; nothing
else on the host is. Network is off by default for run/shell (most
tool invocations don't need it) and on for build (package registries
do). Falls back to a plain unsandboxed subprocess with a warning if
bwrap isn't installed — use --no-sandbox to opt out explicitly.
buckets gui runs a GUI command against a brand-new Xvfb
X server, not the host's real :0. Borrows the concept from
x11docker — a session-scoped
MIT-MAGIC-COOKIE Xauthority cookie, generated fresh per session and bound
into the sandbox alongside exactly ONE file (the specific /tmp/.X11-unix/X<N>
socket, never the whole directory) plus DISPLAY/XAUTHORITY env vars.
Deliberately a nested server, not a --hostdisplay-style reuse of the real
session — X11 has no native per-client window isolation, so a fresh Xvfb
instance means nothing on the real display is ever exposed. Verified live:
a client with the wrong/missing XAUTHORITY is refused by the X server
("Authorization required, but no authorization protocol specified");
the right cookie succeeds. Session cleanup (Xvfb process, socket, cookie
file) happens on drop regardless of how the command exited.
vs. squadron/bin/display-up: a lighter-weight sibling tool exists in
squadron for a different shape of problem — attaching automation
(xdotool/import, or interactd's computer_use/vision_start/
atspi_start) to an app you're launching yourself rather than sandboxing
a command for you. display-up allocates a per-agent Xvfb display
number (flock-serialized, idempotent, keyed by agent identity) but does
NOT provide buckets gui's Xauthority-gated/bwrap-sandboxed containment
— any process that guesses the right :N can still connect. Use
buckets gui/buckets site --gui when you want the command sandboxed
too (real OS-level containment); use display-up when you just need a
private display surface to avoid colliding with another agent's session
on the shared host :0 (the incident that motivated it: blind xdotool
automation during surfer-browser's SHELL-SPLIT-3 spammed input into
another agent's window). The two don't conflict — buckets gui's
XvfbSession could reuse display-up's allocation logic later if the
two ever want to converge, but that's not needed today.
buckets site <url> runs a browser against a URL with a real, OS-enforced
per-origin storage sandbox — reviving the intent behind exosphere-apps'
exo-site-capsulizer (storage-capsule/net-capsule/worker-capsule), which
was found to be ~95% unenforced scaffolding (check_request() never
called, storage VFS dead code, workers never started) and replaced in
surfer-browser with a shim explicitly documented as "not an enforcement
layer" — real privacy there is a JS fetch/XHR interceptor, still
in-process, still not real OS isolation.
buckets site gets the enforcement for free from the sandboxing this
project already has: each host gets its own read-write bind
($BUCKETS_CACHE_DIR/sites/<host>/, persistent — real browsing-profile
semantics — or --incognito for a tempdir removed on exit) and nothing
else on the host is visible inside the sandbox. Headless by default
(surfer on PATH); --gui runs a windowed browser (super-surfer) inside
a fresh Xvfb, same mechanism as buckets gui. Neither browser binary is
built by buckets itself — build it once in surfer-browser (cargo build --release -p surfer --features cli for headless, -p super-surfer --features bliss for GUI), then point buckets site at it (PATH lookup
by default, or --browser-bin <path>).
Not built: per-domain/third-party network filtering (still just the
existing coarse network on/off toggle) and building the browser binary
itself (a separate, one-time cargo build, not part of this command).
<tool>[@<version>]
| Spec | Meaning |
|---|---|
node@20 |
Node.js 20.x (latest in ^20 range) |
python@=3.11.0 |
Exact Python 3.11.0 |
rust@latest |
Latest stable Rust |
node |
Latest (same as @latest) |
go@^1.22 |
Standard caret semver |
go@>=1.22 |
Greater-or-equal |
rust@~1.70 |
Tilde (patch-level) semver |
| Alias | Resolves to |
|---|---|
node |
nodejs.org |
python |
python.org |
rust |
rust-lang.org |
go |
golang.org |
git |
git-scm.com |
cmake |
cmake.org |
ripgrep / rg |
BurntSushi/ripgrep |
curl |
curl.se |
gh |
github.com/cli |
docker |
docker.com |
kubectl |
kubernetes.io/kubectl |
terraform |
terraform.io |
aws |
amazon.com/aws-cli |
neovim |
neovim.io |
tmux |
tmux.github.io |
| ... and 45+ more | See src/index.rs |
- Parse spec → project + semver constraint
- Resolve alias → full pkgx project name
- Collect companions ��� auto-include deps (e.g. openssl for curl, cmake)
- Resolve versions → check cache first, then remote
versions.txt - Install → download
.tar.xzbottle, XZ-decompress, extract, atomic rename - Symlink → create
v*,v<major>,v<major.minor>→ latest version - Compose env → PATH, LD_LIBRARY_PATH, CPATH from installation dirs
- Run or export the environment
| Command | Description |
|---|---|
run <specs> -- <cmd> |
Resolve, install, and exec a command |
shell <specs> |
Open an interactive shell with the runtime |
env <specs> |
Print shell exports (--json for structured output) |
info <specs> |
Show resolution without installing |
list |
Show cached installations |
clean <project>[@version] |
Remove one cached project or version |
clean --all [--older-than <30d|12h|2w>] [--dry-run] |
Bulk-remove cached installations to reclaim disk space |
build <path-or-url> [--test] [--run] |
Detect + build (+ test/run) a real project, sandboxed |
worktree create <repo> <branch> [--from <base>] |
Create an ephemeral worktree (prints its path) |
worktree remove <repo> <path> <branch> [--force] |
Remove a worktree + its branch (git refuses if unmerged, unless --force) |
worktree list <repo> |
List existing worktrees |
gui [specs] -- <cmd> [--screenshot <path>] [--timeout <secs>] [--width <N>] [--height <N>] |
Run a GUI command in a sandboxed bucket against a fresh Xvfb X server |
site <url> [--browser-bin <path>] [--gui] [--incognito] [--screenshot <path>] [--timeout <secs>] |
Open a URL with a real per-origin storage sandbox |
session start <specs> [--tmpfs|--zram] [--size <N>] [-- <cmd>] |
Start a persistent OverlayFS session (files survive across exec calls) |
session exec <session-id> -- <cmd> |
Run a command inside an existing session |
session stop <session-id> [--purge] |
Stop a session (--purge also removes its overlay state) |
session list [--json] |
List active sessions |
net create <name> / net rm <name> / net ls |
Create/remove/list named virtual networks |
net run <name> <specs> -- <cmd> |
Run a bucket joined to a named network |
net expose <name> <host_port>:<bucket_port> |
Forward a host port into a virtual network |
herd deploy <name> --spec <spec> [-n <replicas>] [--net <name>] [--restart <policy>] -- <cmd> |
Deploy a fleet of bucket replicas |
herd ls / herd status <name> |
List herds / show per-replica status |
herd scale <name> -n <replicas> |
Scale a herd to a new replica count |
herd stop <name> |
Stop and remove a herd |
| Env var | Default | Description |
|---|---|---|
BUCKETS_DIST_URL |
https://dist.pkgx.dev |
Distribution server URL |
BUCKETS_CACHE_DIR |
~/.cache/buckets/ or ~/.buckets/ |
Local cache directory |
BUCKETS_WORKTREE_DIR |
sibling of the source repo | Parent directory for worktree create (see below) |
BUCKETS_WORKTREE_DIR defaults to creating each worktree as a sibling of
its source repo (/path/to/repo-my-branch next to /path/to/repo), not
a fixed cache location — required for relative sibling path-dependencies
(../other-repo) to keep resolving correctly from inside the worktree.
sudo flare-up --agent myagent --quiet # once per session (needs root)
buckets --flare run node@20 -- node script.js--flare points BUCKETS_CACHE_DIR at an already-provisioned zram-backed
ext4 mount (squadron/bin/flare-up) instead of host disk — same
resolve/install/sandbox pipeline, just backed by compressed RAM that
evaporates on reboot instead of accumulating on an SSD. Deliberately does
not provision a session itself: flare-up needs root and is meant to
be session-scoped (run once per agent/session, reused across many
buckets invocations), not re-provisioned per command. --flare errors
clearly if no session is live rather than silently falling back to host
disk — run sudo flare-up first. See
/workspace/projects/FLARE_FIREFLY_DESIGN.md for the full design and why
this doesn't repeat the earlier zram-for-inference performance problems.
- Bottle format:
.tar.xzfromdist.pkgx.dev/<platform>/<arch>/<project>/v<version>.tar.xz - Cache layout:
~/.buckets/<project>/v<version>/bin/... - Symlink version scheme:
v*→ latest,v20→ latest v20.x.x - Companion packages: auto-included deps (openssl for curl, etc.)
- Env composition: scan
bin/,lib/,include/,share/,man/per installation - Shell exports format:
export PATH="/path/to/bucket/bin:$PATH" - JSON output:
--jsonfor structured consumption - Multi-package:
buckets run node@20 python@3.11 -- node -e "..."
cargo install --path .
# or, from crates.io (published as `crush-buckets`, binary is still `buckets`)
cargo install crush-bucketsThis repo doubles as its own Claude Code plugin marketplace — install it directly from GitHub, no separate marketplace repo needed:
/plugin marketplace add nixpt/buckets
/plugin install buckets@buckets
The plugin ships a buckets-usage skill that teaches a model when to reach
for a throwaway bucket instead of a global install, plus the core command
reference. It does not vendor a compiled buckets binary (a public
OSS repo shouldn't ship arch-specific blobs) — install buckets itself via
one of the two commands above first.
buckets is a clean-room reimplementation, not a fork — no source from the projects below is copied here, only the formats/behaviors listed under "Features borrowed from pkgx" above, plus the general resolve→install→compose→exec pipeline shape.
- pkgx (Max Howell, Jacob Heider;
Copyright 2022–23 pkgx inc.; Apache-2.0) — the bottle format,
dist.pkgx.devdistribution protocol, cache/symlink layout, companion-package resolution, and env-composition approach are all pkgx's design, reimplemented here. - exosphere's
exo-hydraand exo-light'sexo-hydra— the same pkgx-derived provisioning pipeline shape (resolve → install → compose → exec), adapted for this project's standalone/sync use case. See/workspace/projects/CLAUDE.md's provisioning-lineage note for how the three relate.
Real-world adoption note (2026-07-11): of the three pkgx-derived
implementations above, buckets is currently the only one with real
downstream consumers — interactd's BucketsTool (shells out to the
buckets CLI) and flame's flame-core::kindle (a real compile-time
Rust dependency, buckets = { path = "../../../buckets", optional = true },
one of flame's three "fuel" backends alongside local paths and language
registries — see flame's own README: "Buckets remains the fleet
tool-bottle CLI; Flame is the workspace OS"). Both exo-hydra copies
remain fully built but honestly self-documented as unwired (their own
READMEs say so) — resolve/install/manifest pipelines with no consumer
calling into them yet.
Dual-licensed under MIT OR Apache-2.0, your choice — see LICENSE-MIT and
LICENSE-APACHE (the standard convention across the Rust ecosystem).
(Acknowledgments above are for design/format credit, not a licensing
obligation — no pkgx source is redistributed.)