Releases: trailofbits/coop
Release list
v0.6.0
Upgrading from v0.5.4
- Rerun the installer to install both
coop
and the newcoop-proxycompanion in the same directory. The v0.5.4 updater
replaces onlycoop, so its first upgrade does not install the companion
required for proxy mode. Preserve yourINSTALL_DIRif you used a custom
installation directory. - The published v0.5.4 Linux ARM64 binary reports
coop 0.5.4-dev (8e24729+dirty)and refuses self-update as a development
build. Use the installer to upgrade it. - Restart all running Linux VMs after upgrading to apply guest-to-guest
network isolation. Every VM on the shared bridge needs the restart; a
pre-upgrade VM leaves peers reachable. macOS is unaffected. - Save guest-only work before
restore --reprovision: it replaces the guest
disk, including any guest keyring and cached account login. Rebuilding an
image alone does not update existing VM disks.
New features
-
Codex ChatGPT account auth — New
[codex] auth = "chatgpt"mode for
account/workspace access without OpenAI API billing. coop installs Linux
Secret Service support in the guest image, writes Codex's
cli_auth_credentials_store = "keyring"setting, launches Codex through a
D-Bus/GNOME Keyring wrapper (/usr/local/bin/codex-account, which the
in-guestcodex-yoloshortcut also uses and which execs Codex unchanged when
the mode is off), suppresses everyOPENAI_API_KEYforwarding path, and stops
copying hostauth.jsoninto the guest. Sign in withcoop codex -- login --device-auth;loginandlogoutnow run without the sandbox-bypass flag,
which is meaningless on subcommands that never start an agent session.
Existing images must be rebuilt withcoop setup --rebuildbefore using this
mode. A restart reuses the old guest disk, so an existing VM also needs
coop restore <vm> --image <image> --reprovision(or a destroy and
recreate) to pick up the new guest packages. -
coop restore --reprovision— start over without re-typing anything
(#432) — Replaces a clobbered or bloated guest filesystem with a fresh copy of
the instance's image, then provisions it as a first boot:/workspaceis
restored from the source the instance recorded — re-synced, re-cloned or
re-mounted — agents are re-bootstrapped, and plugins, marketplaces and MCP
servers are reinstalled. The instance is left running, so no follow-up
coop startis needed. It keeps its name, index, IP, image, disk size, port
forwards and guest env — including a devcontainer'scontainerEnvand
forwardPorts— so those flags do not have to be remembered. GitHub PATs and
provider credentials live in the host-side secret store and are untouched.
Extra--extra-mountdirectories,--exclude-gitand a devcontainer's
postStartCommandare not replayed, because coop does not persist them.This is what
coop restore+coop startcould not do: a restart skips the
workspace sync and the plugin install on the assumption that both survived on
the guest disk — which holds for acoop commitcheckpoint, but not after the
disk is replaced with a base image. Plainrestore+startremains the
checkpoint rollback;--reprovisionis the base-image case.--imagebecomes optional under--reprovision, defaulting to the image the
instance already records; it stays required otherwise.-yskips the
confirmation and is required off a TTY.-y,--no-agentsand--no-prompt
are rejected without--reprovision, since none of them mean anything to a
plain disk swap. Unlike a plainrestore, which requires a stopped instance,
--reprovisionalso accepts a running one and stops it itself.coop restore
without--reprovisionis unchanged. -
Credential-injecting proxy — keep the model API keys out of the guest
(#411) — New opt-in[proxy]config. When set, coop runs a small host-side
reverse proxy (coop-proxy, a new binary shipped in the same tarball) — one
process per (VM, provider) — for the lifetime of a remote-mode VM: the guest
is pointed at it and holds only a per-instance capability token, while the
real credential stays on the host and is injected onto requests upstream.- Claude Code (
[proxy.anthropic]) is pointed at the proxy via
ANTHROPIC_BASE_URL; supports an API key (x-api-key) or a Claude
setup-token(Authorization: Bearer). - Codex (
[proxy.openai]) is pointed at the proxy via a
[model_providers.coop_local]block (Responses API) with the capability
token as the provider bearer; OpenAI API keys inject as
Authorization: Bearer. In proxy mode Codex's~/.codex/auth.jsonis no
longer staged onto the guest disk; Codex subscription is out of scope — use
an API key.
The raw
ANTHROPIC_API_KEY/OPENAI_API_KEYis no longer forwarded into the
guest, so a prompt-injected or rogue agent cannot read a usable key.
Resolution fails closed — a bad credential aborts the boot rather than booting
without injection.coop model <vm> localtakes precedence and tears the
proxy down. The proxy binds host loopback and is reverse-tunnelled (ssh -R)
into the guest, so it works on both backends (Firecracker and Lima). GitHub
is a tracked follow-up.coop updatekeepscoopandcoop-proxyin
lockstep. - Claude Code (
-
The credential proxy is jailed (#411) — the host-side
coop-proxy
process runs confined so a proxy exploit cannot write files or execute
programs, and — on a new enough kernel — cannot reach any host beyond the
upstream:443and DNS:53. On Linux it self-applies Landlock before
serving, tiered by kernel capability: filesystem-write and program-exec are
always denied (kernel ≥5.13) and fail-closed — the VM start aborts rather
than running the proxy without that floor; TCP egress is port-scoped only on
kernel ≥6.7, and on kernels 5.13–6.6 (which have no Landlock network rules)
that scoping is dropped and egress is left open. On macOS the launcher wraps
it insandbox-execwith a Seatbelt profile. The jail is port-scoped, not
host-scoped (upstream identity is enforced by the proxy's TLS verification),
and does not restrict UDP on Linux; see
docs/trust-model.md. -
Per-VM credential overrides +
coop proxy status(#411) — A single VM can
use a different credential than the[proxy.<provider>]default — for
per-project billing, scope, or revocation — viacoop proxy setup [--openai] --vm <name>. The override is stored in that instance's state
(<inst.dir>/proxy.json), not a growing config table, and its secret is
namespaced separately. Resolution is override → default → off.coop proxy status [--vm <name>]shows what each VM resolves to, with credentials
redacted. -
coop proxy setup— store a provider credential like a GitHub PAT (#411)
— Mirrors thecoop githubPAT wizard: paste a credential, pick a secret
backend (macOS Keychain / Linux secret-service / 1Password / 0600 file), and
coop stores it and writes thecmd:reference into[proxy.<provider>]— the
credential is never plaintext in the config. Anthropic is the default;
--openaiconfigures Codex. The secret store is namespaced per service, so
proxy secrets live under their own directory rather than among the GitHub PATs. -
coop editor— VS Code or Zed —coop vscodeis nowcoop editor
(the old name remains as an alias).--editortakescodeorzed;
when omitted, coop tries VS Code first, then Zed. Zed connects with
zed ssh://coop-<name>/<path>, reusing the same~/.ssh/configalias
block that VS Code's Remote-SSH uses. -
coop up --new-instance— a second instance for the same project —up
normally reuses the instance recorded for a project directory or--git-repo
URL.--new-instanceskips that lookup and creates a sibling instance
instead, so two agents can work from one source tree. It requires--name,
since the project-derived name is already taken. Once a project has siblings,
coop upreports the ambiguity rather than picking one, so address them by
name (coop start <name>,coop shell <name>).
Fixes
-
Firecracker guests can no longer reach each other by IP — Instances on the
same host share thebr0bridge and a single subnet, so any guest could reach
any other guest's SSH and forwarded ports. coop now marks every TAP as an
isolated bridge port and inserts aFORWARD -i br0 -o br0 -j DROPrule; both are
needed, since the bridge flag alone leaves a guest able to route around it via
the host's bridge address. Guest→host and guest→internet are unaffected. A
host that cannot apply the flag fails the VM start instead of booting an
unisolated guest; this needs Linux ≥ 4.18 and iproute2 ≥ 4.19. Restart any running VMs after
upgrading — isolation applies per start, and because the kernel requires
both ports to be isolated, one pre-upgrade VM leaves the whole bridge
unisolated. This blocks reachability, not impersonation; see
docs/trust-model.mdfor the residuals. macOS is
unaffected — Lima gives each guest its own user-mode NAT. -
Guest transports fail instead of hanging when a VM stops responding — A
paused VM, a wedged sshd, or a lost TAP device leftssh,scp, andrsync
calls blocked on a dead socket with no deadline, so lifecycle commands,
coop exec, andcoop push/pullhung until interrupted. Every transport
now derives from one option list that setsBatchMode, a connect timeout,
and a liveness probe, so a guest whose sshd stops answering fails after ~90s
— the bound interactive sessions already had. -
The guest hostname resolves, so
sudostops warning — Instance creation
renamed the Firecracker guest toclaude-<name>in/etc/hostnamebut left
the image's127.0.1.1 claude-vmentry in/etc/hosts, so everysudo...
v0.5.4
New features
-
coop agent update— refresh in-guest Claude Code and Codex (#403) —
Agents are installed "latest at build time" duringcoop setupand are not
part of the image-staleness hash, so a plaincoop setupnever refreshes
them; they go stale in long-running VMs and in new VMs built from an old
image.coop agent update [NAME] [--claude] [--codex] [--check] [-y]updates
the agent binaries inside a running instance without rebuilding the golden
image. No agent flag updates both;--checkreports installed vs. latest and
changes nothing. Codex (root-owned/usr/local/bin/codex, no background
updater) is reinstalled from the current release via coop's own installer
over SSH; Claude Code (~/.local/bin, already self-updating) runs
claude updatesynchronously. Versions are parsed to semver so "update
available" is a comparison, not a string diff; an unparseable version
degrades toUnknownrather than erroring. -
Codex plugins and marketplaces (#407) — New
[codex] marketplaces/
[codex] pluginsconfig fields (with~expansion and local-path
validation), mirroring the existing Claude Code mechanism. On Lima the set is
baked into the golden image and staleness-checked; on Firecracker it installs
on first boot. coop preserves the guest's own[marketplaces.*]/[plugins.*]
tables across the per-boot~/.codex/config.tomlrewrite — so plugins
installed in-guest and manual/pluginstoggles survive stop/start — while
dropping any that came from the host config. Local marketplace directories
are copied into a per-tool guest subdir so same-basename marketplaces from
the two agents don't collide. Behavior change: marketplaces/plugins set
directly in the host~/.codex/config.toml(rather than in coop's[codex])
are now stripped from the guest; declarative[codex]is the source of
truth.
Fixes
-
Guest-memory floor enforced by construction (#404) —
coop up --mem 16,
coop setup --mem 16, aconfig.tomlwithmem_size_mib = 16, and a
cloned repo whosedevcontainer.jsonsetshostRequirements.memorybelow
the 128 MiB minimum are now all rejected up front, instead of booting an
unbootable VM that never comes up on SSH. The floor lives in a new
VmMemorytype whose constructor every entry point routes through (CLI,
config.toml,coop resize, the devcontainer translator), so no lifecycle
path can hold a sub-floor value. The staleValidatedwitness — which
vouched for a config that lifecycle commands mutated afterward, and which
the actual VM boot (create_and_start) never even consumed — is removed;
the environmental (path-existence) checks it stood in for now run at the
backend boot choke point on the freshest filesystem state, so a new
lifecycle path cannot skip them. The start-time mount set gains a
ValidatedMountsconstructor that enforces guest-path uniqueness on every
path, closing a gap wherecoop quickstartskipped the check. -
Firecracker CI artifact listing fetched over HTTPS (#401) — the S3
ListObjectsV2request that discovers kernel/rootfs versions during
setup used plain HTTP: the bucket name contains dots, which breaks TLS
certificate matching for the virtual-hosted URL. It now uses the same
path-style HTTPS endpoint as the downloads themselves, so a network
attacker can no longer steer version selection.
Documentation
- Docs describe the public repository (#401) — removed the
transitional "whiletrailofbits/coopis private" wording from the
README anddocs/commands.md.coop updateandinstall.shwork
anonymously and usegh/GITHUB_TOKENopportunistically when present.
Internal
repositorymetadata added to Cargo.toml (#401).
v0.5.3
New features
-
coop resizechanges memory and vCPUs in place (#397) —
coop resizenow accepts--mem <MiB>and--vcpus <N>alongside the
existing--size, so a stopped instance's RAM and vCPU count can be
changed without destroy-and-recreate. At least one of the three is
required;--startboots the instance after applying the change
instead of leaving it stopped. The backend config artifact is now the
source of truth for mem/vcpu (mirroring how disk already works): on
Firecracker the per-instance JSON is read back and only the infra
fields are regenerated on restart, so a change to the global[vm]
config no longer silently alters RAM for every existing instance; on
Lima thecpus/memorykeys inlima.yamlare rewritten atomically.
coop statusreads mem/vcpu from the artifact. A failed--start
boot rolls the values back so the instance stays bootable. -
--jsonoutput on read/query commands (#386) — An opt-in--json
flag on the highest-value read commands emits machine-readable output
for reliable parsing; the human-readable default is unchanged. Covered:
status,list/ls,images,devcontainer check,github status,
profiles list, andup/start --dry-run. Each command builds one
Serializeview model and--jsonswitches only the final render
step, so the human and JSON outputs cannot drift; absence is
null/[]rather than sentinel strings. Fordevcontainer checkand
up/start --dry-runthe JSON payload goes to stdout while the human
report stays on stderr, so… --json | jqstays clean. See
docs/json-output-design.md.
Fixes
- AppleDouble sidecars no longer copied on macOS hosts (#376) —
macOS creates._-prefixed AppleDouble sidecar files when archiving
through the systemtar. coop now setsCOPYFILE_DISABLE=1when
creating workspace archives on macOS so these sidecars are not written
into the guest transfer.
Internal
-
.editorconfig(#395) — declares the repo's indentation and
whitespace rules so editors match the project's formatting. -
License field in
Cargo.toml(#387). -
Release workflow sets the release title (#382).
Documentation
Dependencies
v0.5.2
New features
-
coop model— route a VM at a host-side local model (#352) —
Points Claude Code and Codex at a local model server (Ollama, LM
Studio, vLLM, llama.cpp) running on the host instead of the cloud,
configured per tool under[claude.local_model]/
[codex.local_model]inconfig.toml.coop model <vm>reports the
per-tool mode and resolved endpoint;coop model <vm> localroutes
the VM at the local model(s) andcoop model <vm> remoterestores
the cloud defaults. The selection is a persisted per-VM setting (not
a per-launch flag), so it applies tocoop shell,coop claude,
coop codex, and VS Code alike; switching writes guest config over
SSH with no rebuild and is re-applied on the next boot. A
localhost/loopbackhost_urlis rewritten to the backend's
guest-visible host automatically. -
coop codexbypasses Codex's sandbox by default (#353) —coop codex
now launches Codex with--dangerously-bypass-approvals-and-sandbox, so it
runs unrestricted likecoop claude. The VM is the isolation boundary, and
Codex's Linux sandbox does not work in the guest (no functioning bubblewrap),
which previously made every shell command Codex ran fail on sandbox setup.
Passcoop codex --askto keep Codex's sandbox and approval prompts. -
coop commit+coop restore— checkpoint and roll back an instance (#289) —
coop commit <name> --image <image>saves a stopped instance's
filesystem as a reusable image, adocker container commit-style
backup (files, not live memory). The committed image is an ordinary
coop image:coop imageslists it andcoop up --imagelaunches new
instances from it.coop restore <name> --image <image>rolls a
stopped instance back to an image's filesystem in place, keeping the
instance's name, index, IP, and workspace association. Both require a
stopped instance;commit --forceoverwrites an existing image name.
Fixes
-
OAuth-token users no longer land in Claude Code's onboarding wizard
(#87) — Runningcoop claudewith subscription auth
(CLAUDE_CODE_OAUTH_TOKENforwarded into the guest) dropped the user
into the first-run theme/login wizard, whose login step ignores the
token. Claude Code gates the wizard onhasCompletedOnboardingin
~/.claude.json, which coop never staged. coop now seeds that flag on
boot when an OAuth token is forwarded and the flag is not already set.
Idempotent and a no-op for API-key and no-credential users. Reverts
when anthropics/claude-code#8938 is fixed upstream. -
Installed Claude plugins survive a stop/start cycle (#350) —
write_managed_claude_settingsrewrote~/.claude/settings.jsonfrom
scratch on every boot with only apermissionsblock, deleting the
enabledPluginsandextraKnownMarketplaceskeys Claude Code uses to
track installed plugins and marketplaces. Plugins installed on first
boot then showed as uninstalled in/pluginsafter a restart. coop now
merges its managedpermissionsinto the existing file, preserving
those keys. -
~is expanded in everyconfig.tomlpath field (#349, #351) — A
leading~was only expanded forclaude.config_dir,
codex.config_dir, andclaude.marketplaces; other host-path fields
(data_dir,firecracker_bin,vm.kernel_path, per-profile
marketplaces) kept a literal~, so e.g.data_dir = "~/coop-data"
resolved to a literal./~/coop-datadirectory. Expansion now happens
at deserialization via aConfigPathnewtype, so every path field —
including ones added later — is expanded on every load path.
v0.5.1
New features
-
coop ssh-config— install acoop-<name>SSH alias (#294) —
Writes the same~/.ssh/configblockcoop vscodeproduces, but
without launching an editor, so plainssh,scp, andrsyncreach
the guest by alias.--cleanremoves the block. The alias now
survivescoop stopand is refreshed oncoop start(the Lima SSH
port changes per boot), so it stays valid across restarts;
coop destroyand--cleanremove it. -
coop setup --builder-timeout <duration>(#315) — bounds how long
setup waits for image-build commands before timing out. Accepts bare
seconds or ans/m/hsuffix (e.g.60m,2h). Applies across
both backends.
Removed
scripts/build-rootfs.sh(#293) — the standalonedebootstrap
rootfs builder is removed. Image creation runs throughcoop setup,
which downloads a Firecracker CI squashfs and provisions it with
scripts/guest/guest-config.sh; the standalone script duplicated that
provisioning, omitted the CI-kernel workarounds (iptables-legacy,
staticresolv.conf, Docker'sDOCKER_INSECURE_NO_IPTABLES_RAW), and
produced an image the current flow does not consume. The rootfs
discovery error no longer points at it.
v0.5.0
Breaking changes
-
coop buildremoved; creation flags moved offcoop start(#269,
#281) —coop startno longer accepts--profile,--git-repo,
--vcpus,--mem,--disk,--mount,--image, or--exclude-git;
those flags now live oncoop up. clap reports them as unexpected
arguments instead of accepting them and bailing at runtime. The
standalonecoop buildcommand and thescripts/fetch-kernel.sh
helper are removed —coop setupandcoop up --profilecover image
creation end-to-end.scripts/build-rootfs.shstays as a documented
manual fallback. Restart still works against existing instances; the
flags above only take effect at create time (viacoop up). -
--git-repomoved fromcoop starttocoop up(#264) —
coop up --git-repo <url>clones a remote repository into
/workspaceand is idempotent across re-runs, matching the rest of
up: an instance already associated with the URL is reused if
running, restarted if stopped, and created otherwise. The instance
name is derived from the repo basename when--nameis not given. -
tmux session wrapping removed (#183, #184) —
coop shell,
coop claude, andcoop codexno longer wrap the remote session in
tmux, and the--session <name>/--no-tmuxflags are gone. The
tmuxpackage is also dropped from the guest base image. Claude
Code'sclaude agentsdaemon (coop claude-agents) already
provides session persistence without a terminal multiplexer; users
who still want detachable terminals can install tmux themselves via
coop setup --extra-packages tmuxand start it manually inside
coop shell. -
Devcontainer auto-discovery prompts on
up/setup --workspace
(#129, #130, #242) — When the workspace contains
.devcontainer/devcontainer.jsonand no escape hatch flag is set,
coop prompts before applying it. Non-TTY callers must pass
--devcontainer <path>,--no-devcontainer, or--dry-run; the
prompt never silently chooses. Precedence is
CLI > devcontainer.json > defaults, and the report column makes
overrides explicit.
New features
-
coop up— project-oriented environment command (#230, #264) —
coop up [DIR]ensures an environment for a project directory
exists and is running, idempotently.DIRdefaults to the current
directory and is canonicalized for affinity; a matching instance is
reconnected if running, restarted if stopped, or created otherwise.
--git-repo <url>uses a remote repository as the workspace instead
of a local directory. Creation-time flags (--vcpus,--mem,
--disk,--image,--profile,--extra-mount,
--devcontainer,--exclude-git) only take effect when the
instance is created and are rejected against existing instances with
acoop destroyhint; runtime flags (--forward-port,
--post-start,--env) take effect on create or restart but are
ignored when reconnecting to a running VM. -
coop quickstart— one-shot setup + start + claude (#74, #213)
— Chainsensure-image → ensure-instance → launch-claude,
short-circuiting any step that's already done. Setup runs only when
thedefaulttemplate image is missing; workspace affinity
reconnects to a running instance for the current directory (or
restarts a stopped one) instead of allocating fresh.--no-workspace
opts out of the cwd mount; sensitive directories ($HOME,/)
prompt default-no on a TTY and bail non-interactively. -
devcontainer.jsonsupport (#129, #130, #131, #134) — A new
translator maps recognised keys to coop's existing primitives:
hostRequirements→--vcpus/--mem/--disk,containerEnv→
guest env,forwardPorts→--forward-port,postStartCommand→
post_start,features→ built-in profiles,mounts→
--mount,remoteUser→--guest-user. New flags on
setup/up:--devcontainer PATH(opt in to a specific file),
--no-devcontainer(opt out entirely), and--dry-run(print the
per-key report and exit before any side effects). JSONC (//,
/* */comments, trailing commas) parses cleanly. CLI--envand
devcontainercontainerEnvare persisted toguest_env.jsonso
coop shell/coop execsee the same values without re-parsing
config. -
OCI devcontainer features (#245) — Supported public
ghcr.io/devcontainers/features/*entries declared in
devcontainer.jsonare resolved atcoop setupand baked into the
image alongside the existing built-in profiles.coop up --profile
continues to compose the built-in profile set. -
coop devcontainersubcommands (#234, #247, #287) —
coop devcontainer check <path>translates adevcontainer.json
and prints the report without running any setup or VM work
(--stage setup|start|both).coop devcontainer ignore <project>
persistently opts a project out of discovery;... statuslists
current opt-outs;... clearremoves one. Symlinked project
prefixes are resolved when clearing, so a directory that was moved
or unlinked doesn't leave a stale opt-out behind. -
coop setup --guest-userandremoteUserhandling (#217, #218)
— Bake a configurable guest username (defaultubuntu, validated
against POSIX rules and notroot) into the image at setup time.
start/shell/execread the value from the image's
template_config.json. Adevcontainer.jsonthat declares a
differentremoteUseris honored when its value matches the
persisted setup user; mismatches surface an actionable diagnostic
instead of silently writing a wrong home path. Backward-compatible
for pre-existing images. -
--forward-port/forward_portsconfig (#125, #128) — Forward
guest TCP ports to the host for the lifetime of the VM.
coop up --forward-port 3000exposes guest 3000 on host 3000;
3000:18080remaps to a different host port. The flag is
repeatable, supported by a config-levelforward_ports = [...]
default, persisted acrossstop/start, and torn down cleanly on
coop stop. Collision with an already-bound host port fails fast
before the VM is created. -
post_starthook (#123, #126) —post_startinconfig.toml
(or--post-start <cmd>oncoop up/coop start) runs a shell
command in the guest after SSH is ready and before any interactive
shell or agent launch. Maps topostStartCommandin
devcontainer.json. Failure is logged at WARN and does not fail
the start, so a transient hook problem can't strand the VM. -
[guest_env]config block +--env KEY=VALUE(#127, #131,
#134) — Set literal env vars inside the guest without forwarding
from the host. CLI overrides win over config and devcontainer
values;--envis persisted toguest_env.jsonso
coop shell/coop execsee the same values without rerunning
start.coop start --envand--devcontainer-derived
containerEnvsurvivestop/startcycles. -
Build profile images on demand from
coop up(#235) —
coop up --profile <list>derives an image name from the sorted
profile list, runs the same stale-image check ascoop setup, and
builds or rebuilds that image if needed. Explicit named images
(--image) are unchanged. -
Submodule discovery in
coop github setup-pat(#219, #221,
#223) — The wizard pre-discovers submodule repositories via the
localghinstall (no network round-trip per submodule) and offers
to set up a PAT for each one. -
Guest PATH set via
/etc/environment(#248, #249) —
~/.local/binis now reliably onPATHfor non-interactive SSH
sessions, includingcoop claudeand Claude Code's Bash-tool
subshells. Previously these sessions skipped~/.profileand
~/.bashrc's PATH export, hidinguv/pipx/user tools and
triggeringclaude doctorwarnings.pam_envreads
/etc/environmentfor every PAM session regardless of shell mode,
so the prepend reaches remote commands, their subshells, and
VS Code remote sessions. -
Docker buildx in guest images (#288) —
docker buildxis
installed in every coop image so multi-arch andbuildkit-driven
builds work without manual setup. -
Secrets redacted from
Debugoutput (#79, #121, #262) — A new
Secret<T>newtype with redactingDebug, transparent serde, and
an explicitexpose()accessor wrapsClaudeConfig.api_key,
CodexConfig.api_key, andPatEntry.token. MCP header secrets
and forwarded env values render as<redacted>in error chains,
tracing events,dbg!, and panics. -
Hint at
--workspace/--mountwhenstartname looks like a
path (#226) — Runningcoop start ./my-projectnow surfaces a
hint to usecoop up(which interpretsDIRcorrectly) instead of
failing with an opaque "instance not found". -
Warn when
--mountsource is a live git repo (#102, #122) —
Live mounts share filesystem state with the host, so in-guest
changes affect the host checkout immediately. coop now warns when
the mount source looks like a working tree.
Fixes
-
Survive mid-session SSH exit 255 in interactive sessions (#224,
#227) — A network blip that produced SSH exit 255 mid-session no
longer kills the wrapping coop process; the session reattaches. -
Interactive SSH escape path hardened (#232) — Closes a regression
in escape-sequence handling on the interactive path. -
workspace.jsonparse errors surfaced (#225) — Three call sites
previously swallowed parse errors; all now report them with context. -
Bare
coop start --mountallocates a fresh instance (#214,
#215) — Earlier this collided with the auto-resolve path and
produced a confusing error. -
coop upworkspace sync no longer drops--extra-mounton
Firecracker (#264) — The workspace-sync block now always syncs
extra mounts; only mount-only instances record the workspace
identity. Also fixes a tar-pipe fallback that extracted to
`...
v0.4.4
Breaking changes
coop push/coop pull/coop exectake the instance name as a
positional argument (#90) — these three commands previously accepted
--name <name>while the other eleven subcommands tookname
positionally. The flag is removed; pass the name positionally instead.
Becausepushandpullalready had[DIR]as a positional, the
directory is now a--dirflag. BecauseexechadCOMMAND...as a
positional, the command must follow--. Examples:
coop push my-vm --dir ./src --force,
coop pull my-vm --dir ./out --force,
coop exec my-vm -- ls -la.
New features
-
coop ca/coop claude-agentsshortcut (#80, #82, #99, #100, #101) —
Runsclaude agentsinside the VM in one command. Defaults to no
tmux (Claude Code manages background-session lifetime itself);
--session <name>opts in. The guest is now bootstrapped with a
managed~/.claude/settings.jsonthat pre-accepts
bypassPermissions, so dispatched sessions no longer prompt for
tool permissions;coop claude --askexplicitly opts back into the
prompting default. -
coop github setup-patwizard (#85, #88) — Walks the user
through creating a fine-grained personal access token scoped to one
repo, stores it in the user's preferred secret store (Keychain,
Secret Service, 1Password, or file), and forwards it to the guest as
GITHUB_TOKENkeyed off the resolved repo slug. Adds a new
github = "pat"config mode. -
coop list/coop ls(#89, #94) — Local-only enumeration of
instance name + state. Reads on-disk metadata andbe.is_running
without SSH probes so it stays fast even when VMs are unreachable.
coop statuskeeps its richer per-instance and resource-usage
output. -
coop uninstall(#93, #96) — Reverses whatinstall.shdoes:
removes the running coop binary and, with confirmation, the data
directory (~/.coop) and XDG update-check state. Flags
--yes/--keep-data/--purge. Refuses to delete
target/{debug,release}/coopand surfaces EPERM with a
sudo coop uninstallhint. Bails on non-TTY stdin without--yes
so CI misuse fails loud. -
Shell completion (#92, #98) —
coop completions <shell>prints
a static completion script for bash, zsh, fish, powershell, and
elvish. Addingsource <(COMPLETE=<shell> coop)to a shell rc
additionally fills in live values via clap_complete's dynamic
engine — instance, image, and profile names are read from~/.coop
on each TAB. -
--git-repoclones authenticate against private GitHub repos
(#78, #119) — On the host, resolve a token in order: a configured
[github.pat."<slug>"]entry for the repo, thengh auth token,
thenGITHUB_TOKEN. Forward it to git in the guest via stdin and
a one-shotcredential.helper. The token never appears on argv,
stays out of/proc/<pid>/cmdlineand the ssh debug log, and is
not persisted in the cloned repo's.git/config. Opportunistic:
GitHub HTTPS URLs only; non-GitHub and SSH-style URLs pass through
untouched. A misconfigured PAT entry fails at start time rather
than silently substituting a broader identity. -
.git/included in workspace transfers by default (#95) —
coop start --workspace,coop push, andcoop pullpreviously
hardcoded.git/into the default exclusion list, breaking
in-guest git history and renderingcheck_guest_dirtya no-op.
Now transferred by default, with an--exclude-gitopt-out on
start/push/pullfor repos large enough that the transfer
cost dominates.check_guest_dirtyalso now detects unpushed
commits (@{u}..HEAD) so in-guest commits aren't silently
destroyed by a host push.
Fixes
-
integration-uninstall.shstate path on macOS (#106) —
dirs::state_dir()returnsNoneon macOS, sostate_path()in
src/update.rsfalls back to~/Library/Application Support/coop/.
The test seeded$XDG_STATE_HOME/coop/update-check.jsonbut the
binary never wrote there, so the--purgeassertion failed. The
test now uses the same platform branching the binary does. -
SIGPIPE flake in bash completion integration check (#105) —
echo "$HARNESS_OUT" | grep -q "coop,$sub"against the ~48 KB
completion script flaked underset -o pipefail: whengrep -q
matched early it closed the pipe, bash'sechobuiltin exited 141
(SIGPIPE), and the pipeline status maskedgrep's success (~60%
of trials on Linux 6.17 / bash 5.2.21). Replaced the pipe with a
here-string. -
destroy --allintegration phase gated behind
COOP_TEST_DESTRUCTIVE=1(#104) —coop destroy --allremoves
every coop-managed instance on the host, not just the ones the
test created. The phase is now skipped by default; remote mode
forwards the opt-in env var explicitly.
Internal
open_ssh_sessionhelper extracted (#81, #83) — The five-line
resolve_running+prepare_env_forwarding+SshSessionsetup
repeated across Claude, ClaudeAgents, Codex, pluscmd_shelland
cmd_exec, collapses to a singleopen_ssh_sessioncall.
SshSessionis now owned rather than borrowing target/env;
removing the lifetime parameter dropsSshSession<'_>from nine
downstream signatures. Incidental behavior change: with
--no-agents, a misconfiguredcmd:secret source no longer
fails the boot path.
Documentation
- Rust authoring + review guidance in CLAUDE.md (#84) —
Project-specific patterns for using the type system to eliminate
error states: parse-don't-validate, smart constructors, type-state
for the VM lifecycle, newtypes that earn their keep, and error
design. Includes prioritized review and authoring checklists.
Dependencies
cargo-bins/cargo-binstall1.18.1 → 1.19.1 (#86)
v0.4.3
Fixes
-
coop updateworks on the private/internaltrailofbits/cooprepo
(#70, #71) — Previously the in-binary update shelled out to bare
curl, which the GitHub API answers with 404 for unauthenticated
requests against private repos, surfacing ascurl exited with exit status: 22. The update path now authenticates the same way
install.shalready did: prefergh(when installed and authenticated
againstgithub.com), fall back toGITHUB_TOKEN, then bare curl
(which works once the repo is public). -
GitHub token no longer appears on argv (#71) — Both
src/update.rs
andinstall.shnow feed theAuthorizationheader to curl on stdin
(curl -H @-) instead of as a command-line argument, so
$GITHUB_TOKENis no longer visible in/proc/<pid>/cmdlineor in
coop -v update's tracing debug log.
Documentation
- README and
docs/commands.mdnote thatcoop updaterequiresghor
GITHUB_TOKENwhile the repository is private (#71).
v0.4.2
Fixes
-
SSH connections respect
IdentitiesOnly(#68) — Whenssh-agent
holds many keys, ssh offered all of them before the explicit-ikey,
hitting sshd's defaultMaxAuthTries=6and producing "SSH not ready"
oncoop start. SSH/SCP/rsync invocations and the generated
~/.ssh/configblock now setIdentitiesOnly=yes, matching Lima's
own probes. -
Workspace tar-pipe transfer (#66, #67) — Surface SSH stderr (with
acoop start --diskhint when the message mentions "no space left
on device") instead of a generic "tar archive truncated" error. Peak
guest disk usage during transfer is now the extracted tree, not 2× —
the temp-file/SHA-256 dance was redundant since SSH already MACs the
channel. Dedicated background threads drain remote and local tar
stderr to prevent deadlocks when warnings fill the 64K pipe buffer
during extraction. -
Integration test no longer pollutes user state (#63, #64) —
tests/integration-update.shredirects$HOMEand XDG vars to a
tempdir before invoking coop, so the syntheticv9.9.9release
served by the test fixture no longer lands in
~/.local/state/coop/update-check.jsonand surfaces as a bogus
update notification on later runs.
Dependencies
libc0.2.185 → 0.2.186,semver1.0.27 → 1.0.28 (#65)
v0.4.1
Re-release of v0.4.0. The v0.4.0 tag did not produce release artifacts
because tests/integration-update.sh Test 4 ("dev build refusal") fails
whenever CI runs on a commit tagged v{cargo_version} — build.rs
correctly bakes COOP_BUILD_KIND=release for that commit, so the test's
unset-override path produced a release binary instead of a dev one. Test 4
now sets COOP_FORCE_BUILD_KIND=dev explicitly, mirroring Test 1's
=release override. No functional changes to coop itself since v0.4.0.