coop runs Claude Code and Codex inside isolated virtual machines. On Linux, it spins up Firecracker microVMs backed by KVM. On macOS, it uses Lima with Apple's Virtualization.framework. Each VM gets its own filesystem, network stack, and Docker daemon. Agent CLIs never touch your host.
macOS (Lima backend)
- Lima installed with
limactlon yourPATH - Apple Silicon (arm64)
- Rosetta 2 for x86_64 guests on Apple Silicon:
softwareupdate --install-rosetta
Linux (Firecracker backend)
- KVM access (
/dev/kvmmust exist and be writable by your user) - x86_64 or arm64 architecture (x86_64 is the primary test target; arm64 builds are available but less exercised)
sudoprivileges (Firecracker uses jailer and TAP networking)curl,tar,e2fsprogs(formkfs.ext4,resize2fs)
coop is a Rust project. Install Rust, then:
cargo build --release
The binary lands at target/release/coop.
coop reads ~/.coop/config.toml by default. Override the path with --config. If the file doesn't exist, coop falls back to built-in defaults. Run coop init to generate a starter config file.
A minimal config (an empty file is valid; all fields have defaults):
Defaults: 2 vCPUs, 4 GiB RAM, 8 GiB template disk. Override any of them:
[vm]
vcpu_count = 4
mem_size_mib = 8192
template_size_gib = 20All VM artifacts (kernel, rootfs images, instance disks) live under ~/.coop/.
The guest runs as an unprivileged user (ubuntu, uid 1000, by default) with ~/.local/bin on PATH for every session. Override the username at setup with coop setup --guest-user <name>; see Guest user for details.
Forward your API keys and GitHub credentials into the guest:
github = "auto"
[vm]
vcpu_count = 4
mem_size_mib = 8192
[claude]
config_dir = "~/.claude"
[codex]
config_dir = "~/.codex"The github field controls how coop resolves a GitHub token for the guest:
"off"(default): disables GitHub auth forwarding"auto": checks$GITHUB_TOKENenv var first, falls back togh auth tokenif unset"env": requiresGITHUB_TOKENin your environment"pat": forwards a per-repo fine-grained PAT recorded under[github.pat."owner/repo"]. GitHub enforces the token's scope server-side — see GitHub auth for the full reference.
GitHub auth is off by default. Set github = "auto" (or run coop github setup-pat --repo owner/name for a scoped PAT) to enable it. coop up offers to run the PAT wizard inline the first time you bring up a project backed by a GitHub repo without auth configured.
coop picks up ANTHROPIC_API_KEY and OPENAI_API_KEY from your environment automatically. Setting them explicitly under claude.api_key or codex.api_key also works, but environment variables are preferred.
In a hurry? From your project directory, coop quickstart runs setup, brings up
an instance, and launches Claude Code in one command — building the default
image only if it is missing and reconnecting to an existing instance when one is
already running. The steps below walk through the same flow one command at a
time and give you per-instance control. See the quickstart
reference for details.
coop setup downloads the Firecracker binary and kernel (Linux) or configures Lima (macOS), then builds a template rootfs image. The template ships with base packages (git, curl, build-essential, Docker, and others), the GitHub CLI, Claude Code, and Codex.
coop setup
Install language toolchains into the template with --profile:
coop setup --profile python,node
Built-in profiles: python, node, c, fuzz, rust, go. Combine them with commas (e.g. --profile python,node,rust). Use coop profiles list to inspect what each one installs.
You can also let up build a profile-derived image on demand:
coop up --profile python,node
This creates or refreshes an image named from the sorted profile list
(node-python here), then creates the project instance from it.
Skip confirmation prompts with -y:
coop setup -y --profile python
Setup is idempotent. Rerunning with the same profiles skips completed work. Pass --rebuild to force a fresh template build.
For normal project work, use coop up from your project directory:
cd ~/code/my-project
coop up
coop up is project-oriented and re-runnable. It creates an instance the
first time, reuses it if it is already running, and restarts it after
coop stop. By default it copies/syncs the project into /workspace.
Choose mount transport explicitly:
coop up . --mount
On macOS/Lima this is live filesystem sharing. On Linux/Firecracker it is a one-time sync.
Mount additional data directories when creating the project instance:
coop up . --extra-mount ~/data:/data
Or clone a remote repository directly into /workspace inside the guest:
coop up --git-repo https://github.com/trailofbits/coop.git
Tune a project environment at startup (each flag is repeatable where it makes sense, and works on both coop up and coop start):
coop up --forward-port 3000 # tunnel a guest port to the host
coop up --env RUST_LOG=debug # set a guest env var
coop up --post-start "npm install" # run a command after every boot
See the command reference and configuration reference for the full behavior of --forward-port, --env, and --post-start.
After the environment is running, connect to it:
coop shell
coop claude
coop codex
coop start
coop start starts existing stopped instances. Use it after coop stop when
you want to boot the same VM disk again. If exactly one stopped instance
exists, the name is optional.
Restart a specific stopped instance:
coop start my-project
Restart by project path when the instance was created for that project:
coop start --workspace ~/code/my-project
Project creation options belong to coop up, not coop start:
coop up ~/code/my-project --disk 40 --mount
coop up ~/code/my-project --profile python,node
coop up --git-repo https://github.com/trailofbits/coop.git
Skip Claude Code and Codex credential/config injection:
coop start my-project --no-agents
Launch Claude Code inside the VM:
coop claude
During VM startup, coop writes ~/.claude/settings.json in the guest with defaultMode: bypassPermissions and skipDangerousModePermissionPrompt: true, so Claude Code runs without permission prompts by default. The VM itself is the isolation boundary. For permission prompts (coop launches claude with --permission-mode default):
coop claude --ask
Pass extra arguments through to claude:
coop claude -- --model opus
Launch Codex inside the VM:
coop codex
Pass extra arguments through to codex:
coop codex -- --model gpt-5
Open a shell in the VM:
coop shell
Run a command non-interactively:
coop shell -- ls /workspace
coop exec -- docker ps
coop status
For a specific instance:
coop status my-project
Running instances report resource usage: load average, memory, and disk.
Push local changes into a running VM:
coop push
Pull guest changes back to the host:
coop pull
Both commands default to the workspace path recorded by coop up. Override with --dir:
coop push --dir ~/other-dir
coop pull --dir ~/other-dir
Stop an instance (preserves disk state):
coop stop my-project
Destroy an instance (deletes its disk and resources):
coop destroy my-project
Remove everything, including all instances, images, kernel, and Firecracker binary:
coop destroy --all
Build multiple template images with different profiles:
coop setup --image python-dev --profile python
coop setup --image polyglot --profile python,node,rust
Create a project environment from a specific image:
coop up . --image python-dev
List images:
coop images
Delete an image:
coop images --delete python-dev
| Command | Description |
|---|---|
coop validate |
Check config and prerequisites without changing anything |
coop logs |
Stream VM serial console logs (-f to follow) |
coop vscode |
Open VS Code connected to the guest via SSH |
coop ssh-config |
Install a coop-<name> SSH alias for ad-hoc ssh/scp/rsync |
coop resize --size +20 |
Grow a stopped instance's disk by 20 GiB |
coop resize --size 100 |
Set a stopped instance's disk to 100 GiB |
coop resize --mem 8192 --vcpus 4 |
Change a stopped instance's memory and vCPUs |