coop runs Claude Code, Codex, and Grok Build 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(brew install lima) —coop setupfails without it - 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 untested)
sudoprivileges (Firecracker uses jailer and TAP networking)curl,tar,e2fsprogs(formkfs.ext4,resize2fs)- Setup also checks for
setfacl,unsquashfs,ssh, andrsync. Automatic installation of missing tools requiresapt-get; on other hosts, install the packages providing the reported tools manually and reruncoop setup. See backend prerequisites.
Install the latest release:
curl -fsSL https://raw.githubusercontent.com/trailofbits/coop/main/install.sh | bash
install.sh verifies the downloaded tarball's SHA-256 against the release's
SHA256SUMS and, when the GitHub CLI is installed,
also verifies its Sigstore build-provenance attestation. coop update runs the
same verification, except that it treats the checksum as mandatory and refuses
to install without it. To verify a tarball by hand, download
attestations.jsonl from the same release and pass --bundle (this needs no
GitHub credential — releases up to v0.5.4 predate the bundle asset and do
not publish it):
gh attestation verify coop-<version>-<triple>.tar.gz --repo trailofbits/coop \
--bundle attestations.jsonlDropping --bundle makes gh fetch the attestation from the GitHub API
instead, which it will only do when gh is logged in. install.sh and coop update use that API path themselves for releases published without a usable
bundle.
Rerun the installer above when upgrading from v0.5.4 to a release that includes
credential proxy support. The v0.5.4 updater replaces only coop; it does not
install the new coop-proxy companion. Proxy mode requires both binaries in
the same directory. If you installed into a custom directory, pass the same
INSTALL_DIR to the installer.
The published v0.5.4 Linux ARM64 binary reports
coop 0.5.4-dev (8e24729+dirty) and refuses coop update because it identifies
itself as a development build. Rerunning the installer also bypasses that old
updater.
Install Rust and CMake, then:
cargo build --workspace --release
The binaries land at target/release/coop and target/release/coop-proxy.
Keep them in the same directory when installing: proxy mode looks for its
companion next to coop.
The flake supports native builds on macOS arm64 and Linux x86_64/arm64.
It pins its inputs in flake.lock and reads the Rust version from
rust-toolchain.toml.
The commands below require Nix 2.30 or newer, with the nix-command and
flakes experimental features enabled. Check your version with nix --version.
(nix profile add was introduced in Nix 2.30.) From a checkout:
nix build
./result/bin/coop --version
nix run . -- --helpresult/bin contains both coop and coop-proxy. To install them in your
Nix profile:
nix profile add .#coopThe explicit #coop output names the profile entry coop, independently of
the checkout directory name. If you previously installed with nix profile add .,
check nix profile list for the existing entry name and use that name for
upgrade/removal, or remove that entry and reinstall with .#coop.
On macOS, the package includes Lima and the host command-line tools used by
coop, so nix run . -- setup does not require a separate Homebrew install.
The Apple Silicon and Rosetta prerequisites above still apply. On Linux,
install the host prerequisites separately and keep them on
PATH. VM images are created by coop setup.
Nix packages identify as development builds, which disables coop update
and background release notifications. To upgrade, update your checkout and
run nix profile upgrade coop (or nix build for a local build).
Use nix flake update when intentionally updating the pinned Nix inputs.
coop uninstall refuses Nix-store binaries before changing any data, even with
--yes or --purge. Do not use sudo coop uninstall or delete store paths.
For a profile installation, remove the package with:
nix profile remove coopThis leaves VM instances, images, configuration and update-check state in place. For a declarative NixOS or Home Manager installation, remove the package from your configuration and rebuild instead.
If you also want to delete coop's data, perform the following cleanup before
removing the package, while the coop command is still available. Back up any
VM-only work first; this destroys all instances and images for the selected
configuration. With the default configuration and data directory:
coop destroy --all && rm -rf -- "$HOME/.coop"destroy --all stops and destroys the VMs, cleans up backend resources and
removes coop's SSH config entries. The second command removes the remaining
data and configuration. With a custom configuration, use
coop --config /path/to/config.toml destroy --all, then remove the actual
data_dir configured there, rather than assuming it is ~/.coop. A config file
outside that directory must be removed separately if no longer needed. On Linux,
root-owned files may require elevated permissions for that data-directory
cleanup; never apply it to the Nix store.
Remove the update-check state separately, if present:
# Linux
rm -f -- "${XDG_STATE_HOME:-$HOME/.local/state}/coop/update-check.json"
# macOS
rm -f -- "$HOME/Library/Application Support/coop/update-check.json"Then remove the package as described above. Credentials stored separately in a keychain, password manager, or secret-store directory are not removed by these steps.
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]
auth = "api_key"
config_dir = "~/.codex"
[grok]
config_dir = "~/.grok"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, XAI_API_KEY, and, in the default Codex
API-key mode, OPENAI_API_KEY from your environment automatically. Setting
them explicitly under claude.api_key, codex.api_key, or grok.api_key
also works, but environment variables are preferred.
For Codex account or workspace access without OpenAI API billing, set
[codex] auth = "chatgpt" and rebuild any old image with coop setup --rebuild. An existing VM keeps its own guest disk across a restart, so also
run coop restore <vm> --image <image> --reprovision (see
Codex integration) to pick up the rebuilt image.
Reprovisioning replaces the guest disk, so save guest-only work first. Then
run coop codex -- login --device-auth once.
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 grok
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, Codex, and Grok Build 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
With [codex] auth = "chatgpt", first sign in from the guest:
coop codex -- login --device-auth
Pass extra arguments through to codex:
coop codex -- --model gpt-5
Launch Grok Build inside the VM:
coop grok
coop launches grok --always-approve --trust --cwd /workspace. For permission
prompts, pass --ask (coop passes --permission-mode default). A host
~/.grok/auth.json is copied into the guest on boot. If you have not signed
in on the host, use device-code auth (there is no browser in the guest):
coop grok -- login --device-auth
Pass extra arguments through to grok:
coop grok -- --model grok-4.6
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 into a new review directory on the host:
coop pull --dir ../my-project-from-vm
Both commands default to the workspace path recorded by coop up, but pull
refuses that normally nonempty directory unless --force is supplied. Prefer
pulling into a missing or empty directory, reviewing the untrusted guest files,
and only then copying back what you want. Override the push source or pull
destination with --dir:
coop push --dir ~/other-dir
coop pull --dir ~/new-review-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 editor |
Open VS Code or Zed 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 |