ptrbox

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 1 Imported by: 0

README

        _        _
  _ __ | |_ _ __| |__   _____  __
 | '_ \| __| '__| '_ \ / _ \ \/ /    ptrbox
 | |_) | |_| |  | |_) | (_) >  <     disposable Claude Code sandboxes, one VM per repo
 | .__/ \__|_|  |_.__/ \___/_/\_\
 |_|

CI

ptrbox gives every repo its own VM - Lima on your Mac, Multipass on a Windows PC - with Claude Code inside and a kernel-enforced firewall around it. The agent can install packages, run tests and edit code. It cannot reach the internet except through a domain allowlist, cannot see anything on your Mac except that one project directory, and holds no credentials except its own Claude token.

The problem

Running a coding agent directly on your Mac gives it your files, your ssh keys, your browser sessions, and your network. Permission prompts help, but only if you read every one.

ptrbox runs the agent in a VM instead. The isolation comes from the hypervisor and a firewall in the guest kernel, so it holds no matter what the agent does — including --dangerously-skip-permissions. The guarantees come from the machine, not from the agent asking first.

Why use this

  • Egress is default-deny, per sandbox. An nftables firewall in each guest allows exactly one route out: a CONNECT-only squid proxy with a domain allowlist. Each VM has its own list, so granting pypi.org to one project grants it to nothing else. ptrbox logs --denied shows what was blocked and which VM asked. (A sandbox with PTRBOX_OPENCODE gets one more route: LM Studio's port on your Mac, and nothing else.)
  • One directory, no keys. The project repo is the only mount. Git push happens on the host, where your ssh keys stay. The only credential in a VM is the Claude token — injected from the macOS Keychain, and only after every security assertion has passed.
  • No root, ever. Passwordless sudo is removed and setuid binaries are stripped on every boot. The agent cannot undo the firewall.
  • VMs are disposable. Everything a sandbox contains — distro, sizing, runtimes, packages, allowlist — is declared in host-side config and installed at create time. That config survives ptrbox rm, so deleting a VM loses nothing: ptrbox new rebuilds the same box in minutes. If a sandbox is in a bad state, delete it and make another.
  • Only what you ask for. Language runtimes (node, Python via uv, Go) and browser testing are each one flag, off by default. A VM without node has no npm, and no npm registry in its allowlist — every runtime you skip is attack surface the agent never has.
  • Verified before handover. ptrbox new reboots the VM to activate the firewall, then checks everything: direct egress blocked, proxy working, denied domains refused, sudo gone, requested runtimes present. If any check fails, your token never enters the VM.
  • The proxy is sandboxed too. Squid parses input every agent can influence, so it runs in its own VM with no mounts and no credentials — an exploit lands in an empty guest, not on your Mac. It starts with the first sandbox and stops with the last.
macOS host
├── 127.0.0.1:8888              port forward into the proxy VM - the host's
│                               only part of the egress path
├── git + ssh keys              stay on the host; you push from the host
├── ~/code/<repo>/              the ONLY directory a sandbox VM can see
├── Lima VM "ptrbox-proxy"      squid, CONNECT-only domain allowlist
│    └── no mounts, no credentials, nothing else installed
└── Lima VM per repo (vz + virtiofs)
     ├── mount:    ~/code/<repo> -> /workspace   (nothing else is mounted)
     ├── nftables: default-deny egress; only the proxy and DNS
     │             (+ LM Studio's port on the Mac, if PTRBOX_OPENCODE)
     ├── no sudo:  passwordless root removed, setuid stripped, every boot
     └── claude    any permission mode - the VM is the boundary

Quick start

Requires macOS on Apple Silicon, Lima (brew install lima), Go 1.24+ to build the CLI (brew install go), and a Claude Pro/Max/Team subscription.

go install github.com/PtrMzk/ptrbox/cmd/ptrbox@latest
ptrbox install                # dependency checks, proxy VM, config files

claude setup-token            # prints a 1-year token; then store it:
security add-generic-password -a "$USER" -s claude-sandbox-token -w

ptrbox new ~/my-code/my-git-repo   # creates the repo if needed, builds its VM
ptrbox shell my-git-repo      # lands in /workspace (ssh lima-my-git-repo works too)
claude

ptrbox install is idempotent — safe to re-run after every upgrade, and it never overwrites an edited file without asking. It never installs packages on the Mac for you; a missing dependency prints the brew install line and stops. ptrbox new takes a few minutes: it shows you the plan first (and offers to edit it), provisions with the network open, then reboots to activate the firewall and verifies everything before handing the VM over.

Windows

The same sandboxes on a Windows 11 PC, with Multipass on Hyper-V in place of Lima. Guests are Ubuntu 24.04 (the image Multipass has); the token lives in Credential Manager; everything else reads the same.

winget install --exact --id Canonical.Multipass
winget install --exact --id GoLang.Go        # 1.24+, to build the CLI below
# once, in an ADMIN PowerShell: the private switch every ptrbox VM is wired to
New-VMSwitch -Name ptrbox -SwitchType Internal
New-NetIPAddress -InterfaceAlias "vEthernet (ptrbox)" -IPAddress 172.31.255.1 -PrefixLength 24
# once, with no ptrbox VM running (it restarts the multipass daemon)
multipass set local.privileged-mounts=true

go install github.com/PtrMzk/ptrbox/cmd/ptrbox@latest
# go install writes to %USERPROFILE%\go\bin, which Go's own installer does not
# put on PATH - run it once by full path, and it prints the line that adds the
# directory; the commands below want a new terminal after that
%USERPROFILE%\go\bin\ptrbox.exe install   # checks the switch and the setting, then the proxy VM

claude setup-token            # then store it - /pass alone prompts, so it stays out of history:
cmdkey /generic:claude-sandbox-token /user:token /pass

ptrbox new my-git-repo        # under %USERPROFILE%\code by default
ptrbox shell my-git-repo      # the agent's shell, in /workspace

ptrbox install refuses to go on until the switch, its address and the mounts setting are there, printing exactly those lines - ptrbox never elevates and never changes a host setting itself. Two accounts live in a Windows sandbox: Multipass's own login user keeps its sudo, because the daemon needs it on every boot to re-establish the repo mount, and the agent runs as a second user with no sudo and no route to the first; vm/verify.sh asserts both. Dev servers in a sandbox are reached at that VM's address on the switch, 172.31.255.<n>:<port>, not on localhost. After a host reboot, ptrbox start <vm> repairs a sandbox the daemon brought back without its mount. Not on Windows yet: PTRBOX_OPENCODE (LM Studio on the host is not routable from a sandbox there), and Debian guests.

Your repo is a live two-way mount, so the agent's edits appear on your Mac immediately. Review and push from the host, where your keys are:

cd ~/my-code/my-git-repo
git add -p && git commit && git push

Done with a project? ptrbox rm my-git-repo destroys the VM and reclaims the disk. The repo, the VM's config and its allowlist are untouched.

Commands

Command What it does
ptrbox install [--yes] [--update] Host setup: dependencies, the proxy VM, config files. Idempotent; --update refreshes seeded files, carrying your settings.
ptrbox new <repo> Create the repo if needed, provision and verify its VM.
ptrbox rm <repo|vm> Destroy a VM and its artifacts. Leaves the repo alone.
ptrbox start <repo|vm> Start a stopped VM, bringing the proxy VM up first.
ptrbox stop <repo|vm> Stop a VM; the proxy VM stops with the last sandbox.
ptrbox shell <repo|vm> Open a shell in the sandbox, in /workspace. Starts it, proxy first, if it is stopped.
ptrbox logs [--denied] Read the proxy log. --denied shows what was blocked.
ptrbox allow <vm> [domain…] Add domains to that sandbox's allowlist, or open it in $EDITOR; --list prints it. Applies live.
ptrbox sync-proxy Push hand-edited allowlists to the proxy now.

A bare name lands under ~/code; anything with a slash is used as a path. Prefer ptrbox start/stop over limactl start/stop for sandboxes — a sandbox started without its proxy has no network at all.

When a build can't reach something

ptrbox logs --denied                  # find the domain (localport= says which VM asked)
ptrbox allow my-git-repo files.example.com   # grant it to that sandbox; validates and reloads

Each sandbox's list starts as a copy of the template (~/.config/ptrbox/allowed_domains.txt) minus the groups for runtimes it does not have. The per-VM file survives ptrbox rm, so a re-create keeps its egress; delete the file to reset. If an edit doesn't parse, the previous version is restored automatically. Every entry widens what the VM can reach — keep each list small.

Configuration

ptrbox install writes ~/.config/ptrbox/config, annotated, with every line commented out — configuring is uncommenting, not assembling. It covers the guest distro, the repo root, VM sizing, runtimes, extra apt packages, the Claude model and the in-VM git identity. Every key also works as a PTRBOX_* environment variable, which wins over the file.

Settings that describe one sandbox rather than your Mac can be set per VM:

# ~/.config/ptrbox/vms/thesis
PTRBOX_EXTRA_PACKAGES="texlive-latex-recommended latexmk"
PTRBOX_MEMORY=4GiB

Layers, lowest first, each stating only what it changes:

built-in defaults  <  ~/.config/ptrbox/config  <  ~/.config/ptrbox/vms/<vm>  <  environment

ptrbox new bakes the result into the VM, so editing a file does nothing until you re-create that sandbox — and because the file outlives the VM, a re-create brings the same sandbox back. Per-VM config is host-side, never read from the repo mount, so an agent cannot choose what its own sandbox contains.

What's inside a VM

Debian 13 (or Ubuntu 24.04), Claude Code, tmux, build tooling — plus the runtimes you ask for, and only those. Each is per-VM settable and off by default:

PTRBOX_NODE=true               # npm/npx, and therefore npx-based MCP servers
PTRBOX_NODE_VERSION=22.11.0    # or lts, the default
PTRBOX_PLAYWRIGHT=true         # Chromium/GTK libraries + the Playwright CDNs
PTRBOX_UV=true                 # Python, via uv
PTRBOX_GO=true                 # the upstream Go toolchain, under $HOME
PTRBOX_OPENCODE=true           # the opencode agent, using LM Studio on your Mac
PTRBOX_EXTRA_PACKAGES="ripgrep sqlite3"   # any apt packages, checked by name

Claude Code is always installed: it is a native binary and needs no runtime. PTRBOX_OPENCODE adds a second agent that talks to LM Studio on your Mac instead of a cloud model: ptrbox new asks LM Studio which models it serves (and stops if it is not running), opens exactly one extra firewall rule to its port, and pre-configures opencode with those models and every phone-home switched off. Set PTRBOX_LMSTUDIO_PORT if you moved LM Studio off 1234. If a runtime or package you asked for fails to install, ptrbox new fails — immediately, not a week later when something needs it. Anything that needs root is an edit to vm/provision/ followed by a re-create — the template stays the single source of truth for what a sandbox contains.

Development

make build    # dist/ptrbox
make lint     # go vet, plus shellcheck on the guest scripts
make test     # ~250 cases against a faked limactl and Keychain
make check    # lint, test, and the Windows/macOS cross-builds; needs no Mac, no VM, no network
make smoke    # the real thing: recreates a scratch VM (macOS only; tests/smoke.ps1 on Windows)

make windows-build   # cross-compile ptrbox.exe + smoke.ps1 for a PC with no Go on it

make check runs anywhere, including Linux and CI, because every external command is faked — both backends' CLIs, against one simulated guest — and it covers the whole lifecycle plus the security invariants asserted against the generated VM configs, lima's and cloud-init's alike. Changing what a VM contains means changing vm/, then make golden: the diff to the golden files is the review surface. The host CLI depends on the Go standard library and nothing else.

The layout follows where code runs, because that boundary is the security model: cmd/ and internal/ run on your Mac, vm/ becomes the guests, host/ is the squid config and seed allowlist pushed into the proxy VM.

Limitations

  • macOS on Apple Silicon, or Windows 11 with Hyper-V. On Windows: Ubuntu guests only, no PTRBOX_OPENCODE, and sandboxes share one virtual switch with each other and the proxy (each sandbox's own firewall is what keeps them apart; an inbound rule set is planned).
  • At most 16 sandboxes at once: each holds one of the proxy's per-VM ports (ptrbox rm frees the slot).
  • The proxy VM idles at a few hundred MB of RAM while any sandbox runs; ptrbox errs toward leaving it up rather than cutting a live agent's network.
  • Guest DNS can still resolve external names (queries go out even though connections are blocked); closing that hole is planned.
  • uninstall and doctor are not implemented yet.

License

MIT, see LICENSE.

Documentation

Overview

Package ptrbox embeds the assets the binary installs: the Lima VM templates, the provisioning scripts that run inside them, the verification script, the squid config template plus its seed allowlist, and the annotated config file `ptrbox install` writes for you.

These stay as .yaml/.sh/.txt files rather than becoming Go string constants for two reasons. They execute inside Debian guests, so bash is their native language and rewriting them in Go is not on the table. And vm/ is the documented source of truth for what a sandbox VM contains - the review surface for every security-relevant change - which only works if the files are readable on their own terms.

Embedding them means the compiled binary carries everything it provisions: no PTRBOX_ROOT, no checkout to locate, nothing on disk that a later run could find modified. `go install` produces a complete ptrbox.

Index

Constants

This section is empty.

Variables

View Source
var Assets embed.FS

Assets holds the vm/, host/ and config/ trees, rooted exactly as they appear in the repository, so paths like "vm/provision/10-base.sh", "host/squid.conf.in" and "config/ptrbox.conf.example" mean the same thing in code, in tests and in docs.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
ptrbox command
Command ptrbox creates and manages sandboxed Claude Code VMs, one per repo.
Command ptrbox creates and manages sandboxed Claude Code VMs, one per repo.
internal
backend
Package backend is what ptrbox needs from whatever builds its VMs, said without naming the tool that does it.
Package backend is what ptrbox needs from whatever builds its VMs, said without naming the tool that does it.
backend/guestfake
Package guestfake simulates the inside of ptrbox's VMs: each one's filesystem, and what happens when a command is run in it.
Package guestfake simulates the inside of ptrbox's VMs: each one's filesystem, and what happens when a command is run in it.
cli
Package cli is ptrbox's command surface.
Package cli is ptrbox's command surface.
config
Package config resolves ptrbox's settings and the names and paths derived from them.
Package config resolves ptrbox's settings and the names and paths derived from them.
lima
Package lima is the whole of ptrbox's contact with limactl.
Package lima is the whole of ptrbox's contact with limactl.
lima/limafake
Package limafake simulates limactl.
Package limafake simulates limactl.
multipass
Package multipass is the whole of ptrbox's contact with Multipass: the backend on a Windows PC, Hyper-V underneath.
Package multipass is the whole of ptrbox's contact with Multipass: the backend on a Windows PC, Hyper-V underneath.
multipass/multipassfake
Package multipassfake simulates the multipass CLI on a Windows PC.
Package multipassfake simulates the multipass CLI on a Windows PC.
narrate
Package narrate turns limactl's log into ptrbox's voice.
Package narrate turns limactl's log into ptrbox's voice.
proxy
Package proxy manages the shared egress proxy VM.
Package proxy manages the shared egress proxy VM.
render
Package render turns the VM templates into the configs Lima is actually started with.
Package render turns the VM templates into the configs Lima is actually started with.
rendertest
Package rendertest holds the fixed template values used by the golden-file test and the security invariant assertions, plus the rendering helpers both of them need.
Package rendertest holds the fixed template values used by the golden-file test and the security invariant assertions, plus the rendering helpers both of them need.
ui
Package ui is ptrbox's output voice.
Package ui is ptrbox's output voice.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL