A secure, fast-loading sandbox for coding agents.
What it is Β Β·Β Commands Β Β·Β Build Β Β·Β Security
Run claude, codex,
opencode, or pi in a hardened container that hands the agent credentials it
can use but never see β then put a nice frontend on top, or just drive it
from your terminal.
pillbox init # one-time: create the global pillbox
pillbox auth login --agent claude # one-time per agent (OAuth in a sandbox)
cd ~/work/my-project
pillbox new --name my-project # writes pillbox.toml + per-project state
pillbox run # sandbox + claude + cwd mountedThat's the happy path. The rest of this file is reference.
A local-first tool. The sauce is on your machine: a coding agent in a fast, hardened sandbox, with credentials it can't leak and a live session you can build on. No cloud, no account, no platform required.
- Multi-agent. One surface over
claude,codex,opencode, andpi. Swap the agent, keep the workflow, the vault, and the session stream. - Secure by construction. The agent runs sandboxed, and the vault stub-swaps secrets at the egress β the agent sends a key it never sees, so a leaked or prompt-injected credential is worthless.
- A frontend on top, or use it directly. Every session is a durable, live
event stream you can drive:
session sendpushes input,session subscribestreams it to a UI / bot / orchestrator,session watchreads it in your terminal. Build a frontend on the exact surface you use by hand. - Local-only, two backends. Runs against libkrun (a local microVM, the
default β needs KVM/HVF, on a
--features libkrunbuild) or Docker (the no-KVM compat backend, cross-platform, opt out to it viaPILLBOX_BACKEND=docker). A managed/Cloudflare "remote" tier is in progress: its Β§0-gateway substrate (a per-session Durable Object) is proven live on Cloudflare's free tier, but it's not yet apillbox runtarget.
Coding agents need credentials, state, and a workspace to act on. Mixing those into one bundle gives you:
- One mental model. A pillbox is the thing you create, list, run,
remove.
claude/codex/secret/env/auth/vaultare not top-level subjects β they're per-pillbox concerns. - Project isolation. Per-project secrets, per-project env bundles, per-project vault state, per-project workspace history. One project's leases never collide with another's.
- Shared agent auth. A single
pillbox auth login --agent claudelives in the global pillbox and is reused across every project. - Local-first. The whole thing runs on your machine with zero cloud dependency. A managed/Cloudflare tier for running the same bundle elsewhere is in progress (its Β§0 gateway substrate is validated on Cloudflare β see below), but pillbox runs fully local today.
~/.pillbox/ # 0700
βββ global/ # global pillbox
β βββ secrets/ # cross-project secrets (project shadows)
β βββ env/ # cross-project env bundles
β βββ auth/{claude,codex}/ # agent OAuth state (always global today)
β βββ vault/ # CA + key for vault sessions
β βββ sessions/ # detached session records (local Docker / libkrun)
βββ projects/
β βββ -Users-vuln-work-foo/ # `/Users/vuln/work/foo` with `/` β `-`
β βββ meta.json # { name, created_at, agent_default, workspace }
β βββ secrets/ # overrides global on key conflict
β βββ env/
β βββ auth/ # reserved (per-project auth β v0.7)
β βββ vault/
β βββ sessions/ # sessions started from this pillbox
β βββ repo-password # 0600 β rustic repo encryption password
β βββ repo/ # local rustic repository (local backend only)
βββ cache/ # versioned helper scripts
The state-dir key is the absolute path of the directory holding
pillbox.toml, with / replaced by -. Human-readable, greppable,
unique per host.
# Required
name = "my-project"
# Optional β default agent for `pillbox run`
agent = "claude" # claude | codex | codex-serve | opencode | pi
# Workspace backend (default: local)
[workspace]
backend = "local" # or "s3"
# s3-only:
# endpoint = "https://<account>.r2.cloudflarestorage.com"
# region = "auto"
# bucket = "my-bucket"
# prefix = "pillbox/"
# access_key_env = "R2_ACCESS_KEY" # env-var NAME, not the secret value
# secret_key_env = "R2_SECRET_KEY"Discovery walks up from cwd looking for pillbox.toml (like
.gitignore or Cargo.toml). First match wins. Pass --pillbox NAME
to bypass discovery and operate on a specific named pillbox.
| Command | What it does |
|---|---|
pillbox init |
Create the global pillbox at ~/.pillbox/global/. Idempotent. |
pillbox new [--name N] [--agent A] [workspace flags] |
Create a project pillbox in cwd. |
pillbox list [--json] |
Every pillbox on disk. |
pillbox rm NAME |
Delete a project pillbox by name or key. Refuses global. |
pillbox info [--json] |
Show the current pillbox. |
| Command | What it does |
|---|---|
pillbox run [opts] [-- args] |
Launch the agent against the current pillbox (local libkrun microVM by default on a --features libkrun build; PILLBOX_BACKEND=docker for the no-KVM compat backend). |
pillbox run --detach [--label TEXT] |
Start the session in the background; print the reattach id. Works on both local backends. |
pillbox session list [--json] |
Sessions started from this pillbox. |
pillbox session info ID [--json] |
One session (accepts unique id prefix β₯ 4 chars). |
pillbox session attach ID |
Reattach to a detached session. Detach again with Ctrl-A D. |
pillbox session detach ID |
Signal a currently-attached pillbox to detach (no-op if already detached). |
pillbox session send ID TEXT |
Drive a detached session β push TEXT to the agent's PTY as if typed (orchestrator / bot / IDE). |
pillbox session subscribe ID [--from SEQ] |
Read (machine): stream the durable event log over WebSocket, one JSON Event per frame. |
pillbox session watch ID [--from SEQ] |
Read (human): render the event stream to this terminal β messages, tools, thinking, the attention signal. |
pillbox session rm ID |
Tear down the backend (kill sandbox) and remove the record. |
| Command | What it does |
|---|---|
pillbox secret add/list/show/rm [--global] |
Manage secrets (project default; --global writes to global). |
pillbox env load/list/show/rm [--global] |
Manage env bundles (same scoping). |
pillbox auth login/list/rm --agent A |
Manage agent OAuth state (always global). |
pillbox vault ca/status |
Inspect the per-pillbox vault CA. |
pillbox sidecar [--bind] [--json] |
Run the credential vault as a standalone process. |
| Command | What it does |
|---|---|
pillbox push [--tag T] [--message M] [--json] |
Snapshot cwd into the pillbox's rustic repository. |
pillbox pull [--snapshot HANDLE | --bookmark NAME] |
Restore cwd from a snapshot or bookmark (defaults to latest). |
pillbox snapshot list/show/rm |
Manage snapshots in the pillbox's repo. |
pillbox bookmark list/show/set/rm |
Manage named bookmarks that point at snapshots. |
pillbox workspace rekey |
Rotate the rustic repo encryption password. |
| Command | What it does |
|---|---|
pillbox doctor [--json] |
Diagnose Docker, image, perms, $HOME. |
pillbox version |
Print pillbox + runner-image versions. |
pillbox completions SHELL |
Emit shell completions (bash, zsh, fish, β¦). |
--pillbox NAME is global β works on every per-pillbox command to
override cwd-based discovery.
| What | Read | Write default | --global |
|---|---|---|---|
| Secrets | project + global (project wins) | resolved pillbox | force global |
| Env bundles | project + global (project wins) | resolved pillbox | force global |
| Auth | global | global | (implicit; accepted for fwd-compat) |
| Vault | per-pillbox | per-pillbox | n/a |
| Sessions | per-pillbox (no inheritance) | resolved pillbox | n/a |
A project pillbox always sees the global pillbox as a fallback for secrets / env. Sessions are runtime state and stay tied to the pillbox that started them.
pillbox runs locally against one of two backends:
- libkrun (default on a
--features libkrunbuild) β a local microVM (macOS/HVF, Linux/KVM); needs KVM/HVF and libkrun installed (plus a codesign on macOS). See docs/libkrun-sandbox.md. - Docker (the no-KVM compat backend, cross-platform; opt out to it via
PILLBOX_BACKEND=docker) β the agent runs in the runner container on your local daemon. A build compiled without--features libkrunis Docker-only.
Both backends support the full session surface, including detach/reattach.
A managed/Cloudflare remote tier β running the same bundle off your
machine β is in progress. Its load-bearing piece, the Β§0 gateway (a
per-session Cloudflare Durable Object that holds the event log, assigns seq,
attests the actor, arbitrates the driver, and fans out subscribe), is built
and proven live on Cloudflare's free tier. What remains is running a real
agent in the managed container and wiring pillbox run to target it, so it is
not yet a runnable backend. See docs/managed-tier.md and
cloudflare-spike/. It returns with a different shape than the old --remote
backends.
When you attach to a session (initial run OR pillbox session attach), the local terminal proxies the session's PTY. To detach
without killing the session:
- Ctrl-A then D β works from the attached terminal.
pillbox session detach <id>β works from any shell.
Either way the session record stays in the registry; the backend
keeps running until pillbox session rm <id>. Inside the sandbox,
Ctrl-A Ctrl-A delivers a literal Ctrl-A to the PTY (so readline /
shell beginning-of-line still works).
v0.6 is a deliberate identity reset. There is no migration shim. If
~/.pillbox/ contains the v0.5 layout (data/, secrets/, env/,
or vault/ at the top level), v0.6 refuses to run and prints a
pointer like:
pillbox: pillbox init failed. detected v0.5 pillbox state (~/.pillbox/data/, β¦). v0.6 is a hard reset β no migration shim.
Next: mv ~/.pillbox ~/.pillbox.v0.5-backup && pillbox init
Back up, init, re-login. Auth state, secrets, and env bundles do not carry over.
Threat model (one screen β see docs/security.md for the full version)
Pillbox defends against:
- An agent reading host environment variables, the user's real
~/.claude/~/.codex/~/.gh. The sandbox only mounts the resolved pillbox's auth dir. - The login flow contaminating future runs. Login containers are one-shot.
- Host tools accidentally consuming pillbox state β everything is
namespaced under
~/.pillbox/. - Workspace data leaking through cloud backends. Rustic encrypts
on the client; a stolen bucket alone can't be decrypted (password
is local-only at
<state_dir>/repo-password). - Real API keys reaching the agent process.
--vaultroutes traffic through a stub-swap MITM so leaked API keys are useless.
Pillbox does not defend against:
- A prompt-injected agent exfiltrating credentials it was given on
purpose.
--vaultmakes API keys useless but subscription tokens are still mountable. - Stolen unencrypted disk / backups. Files are plaintext at 0600. Disk encryption (FileVault / LUKS / BitLocker) is the at-rest defense.
- Container escape or kernel attacks on the no-KVM Docker compat backend
(
PILLBOX_BACKEND=docker). The default libkrun backend moves execution into a microVM with a hardware-isolated boundary; the Docker compat backend shares the host kernel, so it is the weaker boundary.
Same posture as gh, aws, docker, kubectl. Pillbox is a
sandbox runner, not a secrets manager.
Pre-alpha. v0.6 is the major reshape (pillbox-as-bundle identity + sessions), local-only. Roadmap:
- v0.1βv0.5 β Claude / Codex sandboxing, secrets + env bundles, pillbox.toml v1, credential vault (Anthropic + Codex + API keys), CI.
- v0.6 β
SandboxBackendtrait + sidecar mode; pillbox-as-bundle CLI redesign; workspace backends (rustic_coreβ local + S3/R2); sessions (list/attach/detach) on the local backends; the drive/read surface (session send/subscribe/watch) β the interactive event substrate, live-verified. - libkrun backend β
local microVM, the default on a
--features libkrunbuild β a secure VM boundary, no daemon, macOS-native; Docker is the no-KVM compat fallback (PILLBOX_BACKEND=docker) (docs/libkrun-sandbox.md). - Β§0 managed gateway π§ the Β§0 substrate as a per-session Cloudflare
Durable Object (seq authority + actor attestation + driver arbitration +
subscribefan-out) β proven live on CF's free tier (cloudflare-spike/, docs/managed-tier.md). Running a real agent through it + apillbox runmanaged backend is the remaining slice. - v0.7+ that managed/Cloudflare remote tier for running the bundle off-box.
# Pull the canonical runner image (or build your own β see docs/runner-image.md)
docker pull ghcr.io/vu1n/pillbox-runner:latest
# Build + install the CLI
cd ~/code/pillbox && cargo install --path .
# First use
pillbox doctor # green?
pillbox init
pillbox auth login --agent claude
cd ~/work/my-project && pillbox new && pillbox runTo use a custom image (extra tools, newer harnesses):
PILLBOX_RUNNER_IMAGE=my-team/pillbox-runner:custom pillbox run
# or pin per-pillbox in pillbox.toml's [runner] tableSee docs/runner-image.md for the image contract, build recipe, and how Renovate keeps the harnesses current.
- AGENTS.md β agent-facing command reference (machine-readable)
- CHANGELOG.md β what shipped, by milestone
- docs/ β topic deep dives
- config.md β
pillbox.tomldescriptor + discovery - secrets.md β pillbox-scoped secrets + env bundles
- vault.md β per-pillbox credential vault
- observability.md β OTLP telemetry + Workshop integration
- shared-mcp.md β
--mcp NAME=URLshared-MCP attachments - runner-image.md β the runner image, overrides, custom builds
- recipes.md β copy-paste flows
- security.md β threat model
- config.md β
- SECURITY.md β vulnerability reporting
MIT OR Apache-2.0