Skip to content

vu1n/pillbox

Repository files navigation

pillbox

A secure, fast-loading sandbox for coding agents. 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 mounted

That's the happy path. The rest of this file is reference.

What pillbox is

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, and pi. 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 send pushes input, session subscribe streams it to a UI / bot / orchestrator, session watch reads it in your terminal. Build a frontend on the exact surface you use by hand.
  • Remote when you want it. The same bundle runs on a remote backend (--remote) — extra, not required. Local is the whole product on its own.

Why "pillbox-as-bundle"

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 / vault are 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 claude lives in the global pillbox and is reused across every project.
  • Local-first; travels when you want. The whole thing runs on your machine with zero cloud dependency. When you do want it elsewhere, pillbox run --remote NAME sends the same bundle — workspace, vault material, config — to a remote backend behind the placement trait. Extra, not required.

Where state lives

~/.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
│   ├── remotes/                           # registered VPS / E2B remotes
│   └── sessions/                          # detached session records (local Docker, E2B, SSH)
├── 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/
│       ├── remotes/                       # per-pillbox remote overrides
│       ├── sessions/                      # sessions started from this pillbox
│       ├── repo-password                  # 0600 — rustic repo encryption password
│       └── repo/                          # local rustic repository (local backend only)
└── cache/                                 # versioned helper scripts (e.g. e2b-helper-vX.mjs)

The state-dir key is the absolute path of the directory holding pillbox.toml, with / replaced by -. Human-readable, greppable, unique per host.

pillbox.toml

# Required
name = "my-project"

# Optional — default agent for `pillbox run`
agent = "claude"          # or "codex"

# 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 surface

Lifecycle

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.

Run + remotes + sessions

Command What it does
pillbox run [opts] [-- args] Launch the agent against the current pillbox (local Docker by default).
pillbox run --remote NAME [--from-bookmark B] Launch on a registered remote VPS (ssh://) or E2B sandbox (e2b://).
pillbox run --detach [--remote NAME] [--label TEXT] Start the session in the background; print the reattach id. Works locally and on ssh:// / e2b:// remotes.
pillbox remote add NAME URL [--agent A] Register a remote — ssh://user@host[:port] or e2b://TEMPLATE_ID.
pillbox remote list/info/rm Manage the remote registry.
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.

Secrets / env / auth / vault

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.

Workspace

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.

Other

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.

Inheritance rules

What Read Write default --global
Secrets project + global (project wins) resolved pillbox force global
Env bundles project + global (project wins) resolved pillbox force global
Remotes 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 / remotes. Sessions are runtime state and stay tied to the pillbox that started them.

Remote backends (deprecated)

⚠️ Deprecated direction (2026-06-01). --remote (ssh://, e2b://, docker://) still ships but is on the way out. "Remote" is now Cloudflare-managed or pillbox running locally on the VPS; the local runtime is pivoting Docker → libkrun microVM (see docs/libkrun-sandbox.md). Don't build new work on the remote backends.

# SSH to a VPS you already installed pillbox on:
pillbox remote add prod-vps ssh://deploy@vps.example.com
pillbox run --remote prod-vps

# E2B managed sandbox (requires `node` + `npm i -g e2b` + $E2B_API_KEY):
pillbox remote add prod-cloud e2b://my-pillbox-template
pillbox run --remote prod-cloud

# Backgrounded session — reattach later from any shell:
pillbox run --remote prod-cloud --detach --label "nightly-build"
# pillbox: ✓ session `abc123def456` started in background.
#          pillbox session attach abc123def456  # reattach

pillbox session attach abc123def456     # streams the PTY back; Ctrl-A D detaches
pillbox session detach abc123def456     # from another shell
pillbox session rm abc123def456         # kill sandbox + remove record

Workspace handoff: remote runs require an S3-shaped workspace backend in v0.6 — the local pillbox and the remote share the same bucket / endpoint. The launch path snapshots cwd as the remote base unless --from-bookmark is passed; the remote hydrates that snapshot before the agent starts and pushes a result snapshot after it exits. Local-rustic transport over the wire is a planned PR 4.1.

Vault handoff: real secret values cross the network once, over the encrypted channel (SSH stdin or the E2B Files API), into the remote pillbox's vault session memory. The blob is never written to disk on the SSH path; E2B stages it through 0600 temp files locally and in sandbox /tmp, then unlinks after the in-sandbox pillbox reads it.

SSH vs E2B parity: detached sessions (--detach, session attach/detach/rm) work uniformly across local Docker, E2B, and SSH remotes — each carries the attach-transport frames over its own byte pipe (docker exec, an E2B raw-pty pty-relay, ssh stdio).

Sessions and the detach hotkey

When you attach to a session (initial run OR pillbox session attach), the local terminal proxies the session's PTY (local Docker, e2b, or ssh). 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).

Hard reset from v0.5

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. --vault routes 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. --vault makes 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 local Docker backend. Remote backends (E2B microVMs, VPSes) move execution to a hardware-isolated host when this is the wrong trust boundary.

Same posture as gh, aws, docker, kubectl. Pillbox is a sandbox runner, not a secrets manager.

Status

Pre-alpha. v0.6 is the major reshape (pillbox-as-bundle identity + remote backends + sessions). Roadmap:

  • v0.1–v0.5 ✅ Claude / Codex sandboxing, secrets + env bundles, pillbox.toml v1, credential vault (Anthropic + Codex + API keys), CI.
  • v0.6 PR 1SandboxBackend trait + sidecar mode.
  • v0.6 PR 2 ✅ Pillbox-as-bundle CLI redesign.
  • v0.6 PR 3 ✅ Workspace backends (rustic_core — local + S3/R2).
  • v0.6 PR 4 ✅ RemoteSsh backend.
  • v0.6 PR 5 ✅ RemoteE2b backend.
  • v0.6 PR 6 ✅ Sessions (list/attach/detach) across local Docker, E2B, and SSH.
  • v0.6 PR 7 ✅ Polish + docs/README rewrite.
  • v0.6 PR 8docker:// remote (run an OCI runner image on any reachable daemon) + the drive/read surface (session send / subscribe / watch) — the interactive event substrate, live-verified.
  • v0.7+ the §0 event substrate as a first-class gateway; substrate pivot Docker → libkrun microVM (docs/libkrun-sandbox.md) — secure VM boundary, no daemon, macOS-native; the remote backends deprecate out as "remote" becomes Cloudflare-managed / local-on-box.

Build

# 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 run

To 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] table

See docs/runner-image.md for the image contract, build recipe, and how Renovate keeps the harnesses current.

Documentation

License

MIT OR Apache-2.0

About

Sandboxed coding agents with one-command auth (gh-auth-style for Claude Code / Codex / opencode)

Resources

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors