Changelog

What is coming, then every release of the Cleat CLI from the first stable one onwards.

What's next

Not shipped yet #

Nothing below has shipped. Each item says whether it is planned for the next release or still in design, plus what is true today, so you can check the claim against the product you have. No dates, because a date we miss is worse than no date.

  1. Network egress control

    In the next release

    Name the hosts a box may reach. Everything else is refused at the boundary, not asked about in a prompt.

    Today Today a box reaches the whole internet. That is the gap this site has listed since the first release. It is the one an agent uses to send what it read in your repo.

    Check it: the gap, as it stands today

v1.5.4 #

latest

A security release. A box could steer Cleat into reading, writing or moving files on your host outside the cage. Every path we found is closed.

An internal audit followed the v1.5.3 image paste fix through the rest of the code. It found the same shape in many places: Cleat wrote or read a file inside a folder the box can write, so the box could swap that file for a link and send the operation somewhere on your host. Upgrade with cleat update, or brew upgrade cleatdev/tap/cleat for a Homebrew install. Every box made before this release is rebuilt once, the next time you start it with cleat, to move its input history to a folder the box cannot reach. Your sessions, logins, trust and history are kept. End any cleat session that was already open, since a running session keeps the old code until it exits.

Fixes #

  • A box could get the host to write or move a file outside the cage - to save a login, a renamed session, a deleted conversation, an edited .cleat or the settings it mirrors into a box, Cleat wrote inside a folder the box can write, then moved the result into place. A box could plant a link at the name Cleat was about to use and send the write to one of your own folders. Every one of these now builds the file in a folder the box cannot see and lands it with a single move that replaces a link instead of following it.
  • The input history file could be pointed anywhere on your host - each box mounts a per-project history file. Its source sat in a folder the box can write, so a box could replace it with a link and have the next start mount one of your files into the box, writable. The history now lives in a host-only folder. Closing this for an existing box needs a one-time rebuild, which Cleat does for you on the next start.
  • Deleted conversations were reachable by the box - the session trash sat inside the folder a box mounts, so a delete could be steered onto the host and a deleted transcript stayed readable by the box for thirty days. The trash moved to a host-only folder. An old trash is carried across the first time you run a session command.
  • The browser and clipboard bridges wrote logs and markers the box could redirect - the proxy log, the watcher log and the liveness markers moved out of the box's folder. The readiness signal a box does need to see is now written from inside the box, so a link there stays inside the box. When the private folder cannot be made, the bridge turns off for that session rather than falling back inside the cage.
  • A box could make the host follow a link to a slow or endless file - reads of any file a box can write (a .cleat, an env file, a [setup] script, a session log, a box .claude.json) are now bounded in time and size, so a pipe or a device the box swaps in costs a moment instead of hanging the command.
  • A capability could be granted from a .cleat you did not approve - the trust check now reads the project .cleat once and grants, prompts and records from that single read, so a file swapped in after approval cannot change what a box gets. An unknown capability name is dropped with a warning instead of counting toward trust.
  • A project env file could pull host variables into a box without consent - a bare KEY line in a project .cleat.env asks for a host variable by name. Those names now join the project's trust prompt and are passed only after you approve them. A project that uses them asks once after upgrading, including in CI, where CLEAT_TRUST_PROJECT=1 approves them. A KEY=VALUE line, which carries no host value, is unaffected. A symlinked env file is refused.
  • Box text could rewrite your terminal - a URL, a .cleat value, a filename or other text the box or the repo controls is stripped of control bytes before Cleat prints it, so it can no longer move your cursor or hide what a report says.

Changes #

  • A --fork copy now briefly pauses any running box that shares the project, so the copy is taken from a still tree.
  • 3470 (+215) behavioral tests (a regression for every fix). 1581 (+221) mutations caught, 0 missed, 0 skipped (1582 on a host with inotify-tools).

v1.5.3 #

A security release. A box could put files of its choosing into any folder you can write, through image paste. That path is closed.

Upgrade even if you never paste images. The watcher that answers ctrl+v runs in every session on a host with a clipboard tool, which is every Mac. A session that was open before the upgrade keeps the old watcher until it exits, so end those too. Until you upgrade, CLEAT_NO_CLIPBOARD_IMAGE=1 turns it off for sessions started after you set it.

Fixes #

  • Image paste could write into any folder you can write - to answer ctrl+v, the host claimed the box's paste request by renaming it to a fixed name beside itself, inside the folder the box can write. A box could plant that name first, as a link to one of your own folders. The rename then moved the request, a file or a whole folder the box built, into it. Requests are now claimed in a folder outside the box's reach, under a name the box cannot predict. They are removed unread.
  • The browser bridge's URL limit counted characters, not bytes - by default the bridge refuses a URL the box picks when it is over 2048 bytes. The check counted characters in your terminal's language setting, so under UTF-8, every Mac terminal's default, a URL of non-ASCII characters passed at up to four times that size. It now counts bytes.

Changes #

  • 3255 (+2) behavioral tests (a regression for each fix). 1360 (+2) mutations caught.

v1.5.2 #

Three account fixes. The note for an unchecked Claude Code version finally appears. A box running under Rosetta is judged like a native one. A command waiting for the account lock no longer gives up just as it comes free.

The handover is now verified end to end on Claude Code 2.1.280, the version the current image ships.

Fixes #

  • The version note on a live switch never appeared - v1.5.0 promised that the handover says so when a box runs a Claude Code version it has not been checked against. It never did. The box probe never sent the version and the host blanked it, so the line could not print. Cleat now reads the version Claude writes in its own session file and names it before it asks: cleat has checked this handoff with Claude Code 2.1.270, 2.1.274 and 2.1.280. This box runs <version>. The value comes from a file the box writes and lands on your terminal. It passes only as a dotted number, checked in the box and again on the host. A version Cleat cannot read gets no note rather than a wrong one.
  • A Claude that started during a switch on an amd64 box under Rosetta was missed - Docker Desktop runs an amd64 box through Rosetta on Apple Silicon, for example when DOCKER_DEFAULT_PLATFORM=linux/amd64 is set. A process that has just started there can show Rosetta and the binary in front of its real arguments. The switch's final scan read that as "not a Claude", so a Claude started in the switch window was staged over. The scan now looks past a known interpreter (Rosetta or qemu-user) when a path follows it. A native box is not affected.
  • A command waiting for the account lock could give up just as it came free - every account change takes one lock and a second command waits for it. The wait checked for a file planted at the lock path in two steps. A release that landed between them looked like one. The waiting command then said Another cleat command is changing accounts right now. and changed nothing. The live switch had the same check on Claude's own refresh lock. Each kind of file is now tested on its own, which a lock coming or going cannot fool. With several commands waiting, a second release in that window is no longer taken for an accounts directory that cannot be written.

Changes #

  • The live switch is verified end to end on Claude Code 2.1.280: an idle session moved from the shared login to a named account and back, reopening the same conversation each time with the identity confirmed. Before that, all 50 Claude Code behaviours the switch relies on were compared between 2.1.274 and 2.1.280. 49 were unchanged. The 50th is one more dialog that reads as waiting for an answer, which the switch already refuses to restart.
  • Test infrastructure: the test runner prints each failing test's whole diagnostic block, so a failure shows what an assertion actually saw. A failed account race test prints what its racing command printed. The guard that keeps a fake process list clear of real process ids now actually runs. CI splits the mutation registry and the WSL2 suite four ways like the macOS suite. A long step that is killed prints its log instead of nothing.
  • 3253 (+13) behavioral tests across 59 files. 1358 (+16) mutations caught, 0 missed, 0 skipped (1359 on a host with inotify-tools).

v1.5.1 #

Cleat now measures which uid your engine maps you to, instead of assuming it is your own. On rootless Docker and Docker Desktop for Linux it was not. Almost nothing worked.

Cleat hands the box your uid so the agent's files come back owned by you. A user-namespaced engine maps you to container uid 0 and your subordinate uids to container 1, 2, 3, so your own number picked a subuid that owned nothing on either side. The result was not one broken feature. The agent could not edit any file already in your project. Your own repo root passed to a subuid. Your real ~/.claude and its credential were taken from you. Named accounts, the clipboard, image paste, hooks, ssh, gh and the OAuth browser forward were all dead. The browser forward failed silently, so a login simply never opened.

Fixes #

  • The box gets the uid you actually are inside a container - on a Linux host Cleat measures the mapping once per engine. It mounts a directory you own into a throwaway container, reads back the uid that container sees and caches the answer in ~/.config/cleat/state/uidmap. It measures rather than matching an engine name on purpose: Docker Desktop for Linux remaps exactly like rootless without reporting itself as rootless. Colima, Lima or Rancher Desktop on a Linux host report the opposite way round: rootless, for a daemon inside a VM whose share layer remaps separately. On rootful Linux and WSL2 the measurement is your own uid and nothing changes. macOS never measures: every macOS engine runs the daemon in a VM whose file sharing already presents your files as yours, so Docker Desktop, OrbStack and Colima on a Mac behave exactly as in v1.5.0.
  • Sessions on a namespaced engine no longer die at launch - on such an engine the box user is uid 0. Claude Code refuses --dangerously-skip-permissions under uid 0. IS_SANDBOX=1 now rides a session whenever the box will run as uid 0, not only when the host user is root.
  • The check that should have caught all of this - cleat waited for the box to adopt your uid by comparing the box's number against your own. On a namespaced engine those match while the mapping is wrong, so the one check that could have reported any of this was a false green. It now compares the uid the box was actually told to use.
  • A box created under the old mapping says so - its uid is frozen at create and the recreate check deliberately ignores the env Cleat injects. No config change heals it. Such a box is now named at session start with the command that fixes it. It no longer spends five seconds polling for a match that can never happen.
  • cleat nuke clears the measured mapping, so a wrong measurement is not the one thing that survives the reset.

Changes #

  • The measurement binds a dedicated empty directory rather than the Cleat config directory, which holds the account credential store. Learning a number is no reason to put credentials inside a container.
  • A measurement records the engine it was taken against and the user it was taken for. Pointing at another daemon, switching context or running as a different user re-measures instead of reusing a number that was never about that engine. A corrupt or half-written entry is refused and re-measured rather than handed to a box.
  • 3240 (+17) behavioral tests across 59 files. 1342 (+12) mutations caught, 0 missed, 0 skipped (1343 on a host with inotify-tools).
  • The test harness pins the engine mapping, so a suite run no longer depends on what the Docker stub happens to answer.

v1.5.0 #

Accounts and sessions are the headline. Pin a box to a named Claude login and switch it mid-conversation, then rename, delete or restore conversations one at a time. Underneath, the host bridges now decide where a box may point your browser and what a host hook is handed.

It also adds the unsafe-rm capability for unattended cleanups and repairs cleat login and cleat resume. A hardening pass closes several ways a box could write to host files through the bridges.

Features #

  • cleat account - cleat account <name> [box] pins a box to a named Claude login, creating it if new. Start the box and run /login once (or cleat login) and that login is remembered under the name. cleat account default [box] unpins a box back to your shared ~/.claude login. Only the credential store moves, so conversations, project history and settings are the same on every account. Logins live in ~/.config/cleat/accounts, outside the ~/.claude every box mounts. Only the pinned login is staged into its box. Every attach stages it in and every detach takes the refreshed token back out. cleat rm, cleat clean and cleat nuke take it out too before they wipe a run directory. cleat account alone opens a picker to switch, rename or remove a login. cleat account list prints the same list without a terminal. cleat status and the launch summary name the account a box is really using.
  • Live account switch - running cleat account <name> on a box with a live Claude session hands the session over. Cleat probes the box and says what the restart costs first: anything typed but not sent, any background agent or monitor the session runs and a first reply on the new account that may read the whole conversation again without a prompt cache. After Hand over? it stops the session, moves the login and the other terminal reopens the same conversation on the new account, straight into the chat with no resume question in the way. Type continue there. --yes skips the question. --now also restarts a session that is mid-turn or waiting at a prompt, where its reply so far is lost. Even with --now it refuses when background shell commands run outside the turn it would stop, when cleat did not start that Claude session, when the target account has no working login or when a conversation larger than a standard context window shows no sign it ran with 1M context. The handoff is checked against Claude Code 2.1.270 and 2.1.274 and says so when a box runs another version.
  • Identity-checked logins - a refreshed login is written back to its account only when Cleat can tie it to that account, by the same refresh token or by what Anthropic's profile endpoint says about it. Anything it cannot prove, such as a /login as someone else inside a pinned box or a login left in a box you removed, is held instead of written over an account's only credential. cleat account held lists held logins with where each came from. cleat account adopt <id> <name> saves one as an account. One host lock serializes every account change, so two terminals cannot mix two logins.
  • Account trash and usage - cleat account rm moves a login to a trash kept 30 days and cleat account restore brings it back. Held logins are kept 30 days too. The picker reaches the trash with the right arrow. The list shows each account's 5h and 7d usage while its own access token is alive, a timestamped last-known figure when it is not and "window reset" once the reset time has passed. A parked login is never refreshed to draw a figure. Usage needs jq and curl on the host. Saving a new login into an account needs curl too, because Cleat first asks Anthropic whose login it is.
  • cleat session - cleat session [box] lists a box's Claude conversations, newest first, with the disk each one really costs: the transcript plus its directory of subagent transcripts and tool results. On a terminal it is a picker where Enter opens rename or delete and the right arrow opens the trash. cleat session rename <id> (with --title to skip the prompt), cleat session rm <id>, cleat session trash and cleat session restore <id> work without a terminal. An id needs at least 8 characters. A delete takes both parts to a trash kept 30 days, asks first and needs --yes without a terminal. It refuses while the box has a live Claude session and when Docker cannot say whether it does. A rename keeps the conversation's place in cleat resume order. Only real session ids are ever touched, so the project's Claude memory and prompt history are out of reach. Listing works with Docker down.
  • unsafe-rm capability - a new guard capability that answers Claude Code's dangerous-rm prompt, which stays on even under --dangerously-skip-permissions. Cleat adds a PermissionRequest hook to the box's settings overlay that allows a command when it invokes rm or rmdir at the top level, so chained cleanups run unattended. A command that removes nothing is never answered. The answer covers the whole command, so any other safety prompt that same command raises is approved with it. It disarms a real guard over read-write host mounts, so it is accepted only from your global config or --cap, never a project .cleat. It shows in red on the launch summary. It needs jq on the host and says so when that is missing.
  • Browser destination check - the host opens a URL a box asks for only when its host is on the allowlist: 19 login origins that ship with Cleat plus any you add. The shipped ones include claude.ai, claude.com, platform.claude.com, github.com, accounts.google.com and login.microsoftonline.com. A login at any other origin is refused and printed in full when the session, cleat shell or cleat login ends, with the cleat browser allow <host> line that permits it next time. No list entry can admit a loopback, link-local or private address, whatever form the IP is written in. By default the bridge never opens a plain link or a device-flow page, because your terminal opens a clicked link itself. That now holds with no terminal attached too, so an unattended run never opens a plain link nobody is watching. CLEAT_BROWSER_BRIDGE=always still opens any origin. The launch summary shows a Browser: row whenever the mode is always or off.
  • cleat browser - cleat browser origins (or bare cleat browser) lists what a box may open, including origins you added and any entry that was ignored. cleat browser allow <host> adds one for every box under [browser] in the global config. CLEAT_BROWSER_ORIGINS adds more for one shell. Both append to the shipped list and neither replaces it. A [browser] section in a project .cleat is ignored with a warning, because the caged agent edits that file.
  • Browser rate cap - a box can make the host open at most 6 URLs a minute and 30 a session, in every mode including always. Anything held back is reported when the session ends, with up to three of the URLs to open by hand.
  • Hook events are validated and translated - before a host hook runs, the event is checked and its path fields are rewritten from /workspace/... to your real project path (the fork's copy for a fork box). Each path has to land inside the project on disk, symlinks included. The hook runs in the event's translated working directory. An event that fails a check is dropped and logged to ~/.config/cleat/state/hook-drops.log. The session end says how many were dropped. Every event handed to your hooks gets a row in hook-runs.log beside it. Timeouts are now per event (15s for PreToolUse and PostToolUse, 120s for Stop and SubagentStop, 30s for every other event) and are enforced through timeout, gtimeout or perl, so a stock macOS no longer runs hooks unbounded. A tool name with a line break in it no longer satisfies an anchored matcher. A session that will run a host hook says so at launch.
  • More of ~/.claude is read-only in a box - 15 more paths at the root of ~/.claude that a later session or your own host Claude Code reads as configuration are mounted read-only: rules/, workflows/, output-styles/, themes/, keybindings.json, loop.md, settings.local.json, daemon.json, the npm-local local/ install and six others. Six of them show a read-only copy of your own content, refreshed on create, start and resume. The rest are empty. session-env, daemon and seed-admin join the per-box private directories. A box can still create ~/.claude/.config.json, which no mount can cover, so Cleat warns at launch when that file exists and at session end when one appeared.

Fixes #

  • cleat login signs you in - Claude Code has no top-level login subcommand, so cleat login opened a session whose first prompt was the word login and signed nobody in. It now runs claude auth login. On a box pinned to an account the new login is saved to that account.
  • cleat resume reopens the right conversation - cleat resume passed --continue, which picks the newest transcript even when another terminal has that conversation open, so two processes appended to one file. It now names the newest conversation that is not open in another terminal and starts a new one when every conversation is open elsewhere. A conversation that ran with 1M context also reopens with it, unless your settings or env name a model.
  • Host hooks come only from ~/.claude/settings.json - with the hooks capability on, the host bridge read hook commands from the project's .claude/settings.json and .claude/settings.local.json on every event. Both sit inside the read-write /workspace mount, so a box could rewrite a hook command between two events and have your host run it. Project hooks no longer run on the host. A project that defines some gets a note at session start saying so. Copy the ones you want into ~/.claude/settings.json.
  • A box can no longer aim your browser or a host port - any URL containing redirect_uri= counted as a login and opened, even with a terminal attached. The OAuth callback proxy then made the host bind whatever port the redirect named, in every mode including off. The proxy now runs only for an authorize URL at a listed origin whose callback is exactly localhost or 127.0.0.1 on a port from 1024 to 65535. It does not start at all when something on the host already holds that port.
  • Bridge files cannot redirect host writes - the run directory the bridges share with a box is mounted read-write. Host-side appends followed whatever the box put there. A symlinked .proxy-log let a box append a line of its choosing to any file you can write, a shell rc file included. A symlinked .watcher-log emptied its target once that file was over 1 MB. Anything at a box-writable bridge path that is not a regular file is now removed unread before the host touches it. Logged URLs are stripped of control characters. On macOS the Keychain seed wrote its temp file at a guessable name in ~/.claude. It now uses mktemp.
  • The OAuth callback forward works where socat is installed - socat's EXEC: address never runs a shell, so on any host with socat the forward that brings a hands-free login back into the box returned an empty reply. Hosts without socat used the python3 fallback, which worked. The socat forward now uses SYSTEM: with its separators escaped. An abandoned login no longer leaves a host port bound. The python3 fallback also stopped setting SO_REUSEPORT, which let a box-named port share a port with a live host service.
  • Login expiry is read from the login - the credential reader took the last expiresAt anywhere in ~/.claude/.credentials.json, so an MCP server's OAuth entry beside the login could stand in for the login's own expiry. Every reader now takes the claudeAiOauth entry only. On macOS that stops the Keychain re-seed overwriting a login a box had refreshed. An unreadable credentials file no longer aborts the launch either.
  • cleat status reads the real login - cleat status said "logged in" whenever any file named like credentials sat in ~/.claude, a months-old dead one included. It now checks the store the box actually uses.
  • cleat clean with Docker stopped - with the daemon down cleat clean printed "Stopping all Cleat containers..." and then exited with status 1 without saying why. It now says Docker is not running and removes nothing.
  • The idle sweep no longer mistakes Node for Claude - live-session detection matched any process line containing claude or node, so a box left running a Node dev server or an MCP server was never stopped by the idle sweep. Each process is now judged by its executable, so only a real Claude Code process keeps a box running.
  • Project hooks on a host without jq - with no jq on the host a project's .claude/settings.json and .claude/settings.local.json were mounted into the box unchanged, so their hooks ran inside the container where host-only commands do not exist. A file that defines hooks now reads as {} inside the box on such a host and the launch says so. A file without hooks still passes through unchanged. When the hooks capability is on, the launch also warns that no hooks are forwarded until jq is installed.
  • Clipboard copies survive a planted directory - on a host without inotify-tools or fswatch, which includes a stock macOS, the polling clipboard watcher ignored a directory named clipboard in the bridge directory, so every later copy was moved inside it and lost. The watcher now clears anything at that path. The documented 100KB clipboard cap is now enforced on the host too.
  • IPv6 loopback in DOCKER_HOST - an expanded, zone-scoped or IPv4-mapped IPv6 loopback address was treated as remote, so the docker capability forwarded it into a box where it names the box itself. It is recognised as local now.
  • cleat upgrade-claude keeps the image labels - the upgraded image is committed with its sh.cleat.image-spec and sh.cleat.version labels carried across, so a later image change can still prompt that machine to rebuild.
  • A busy package manager is named - on Linux, when Docker's installer fails because another package manager holds the lock (a fresh cloud image updating itself on first boot), cleat names the running process and says to wait and run it again. Any other install failure now prints the command to install Docker yourself.

Changes #

  • No image rebuild this release. docker/ is unchanged, so upgrading rebuilds nothing and recreates nothing. The new ~/.claude masks and the account credential mount are create-time mounts, so a box created before this release prints a recreate note with the command for that box on every start until it is recreated. An account pin on such a box is reported as not in effect until then. Conversations, trust and env live on the host and survive the recreate. Anything installed inside the box does not, so move it into [setup] first.
  • Creating a box now seeds any of the new mask targets that are missing from your host ~/.claude, since macOS VirtioFS cannot mount over a missing path: empty directories, an empty loop.md, {"bindings":[]} for keybindings.json and {} for the other JSON files. A path that already exists is never touched.
  • The per-project session directory name is now derived under the C locale, so it no longer changes with your locale. A project whose folder name has non-ASCII characters keeps the directory it already has.
  • The plan-big-execute-small kit now names Fable 5.1 as the planner, the model Claude Code's /model picker offers as Fable.
  • The on-start note introduces accounts, sessions and the live switch. cleat help lists session, account and browser.
  • The repo gained a security policy with a private reporting route and a written scope. The README documents accounts, sessions, the browser origin check and host hooks.
  • Test infrastructure: new real-Docker integration files cover the account credential mount and the live handoff inside a real container. The docker stub can script docker exec and docker top. CI installs socat on the Linux suite legs and the mutation job so the OAuth forward tests drive the real binary.
  • 3223 (+1033) behavioral tests across 59 files. 1330 (+748) mutations caught, 0 missed, 0 skipped (1331 on a host with inotify-tools).

v1.4.3 #

The compatibility release. The docker capability now binds the daemon socket where your engine actually runs, so rootless Linux and Colima work instead of pointing at a socket that is not there.

Features #

  • Engine-aware docker capability - the socket bind follows the daemon. A VM-backed engine (Docker Desktop, OrbStack, Colima, Rancher Desktop and Lima, on macOS or Linux) gets the in-VM /var/run/docker.sock. A host-local daemon gets its real socket, rootless included (/run/user/<uid>/docker.sock). A remote endpoint gets DOCKER_HOST forwarded. DOCKER_HOST always wins over the active docker context.
  • Loopback tcp counts as local - a daemon reached over tcp://127.0.0.1 (any 127/8 address), localhost in any letter case or [::1] runs on your machine, so the cap binds the host socket instead of handing the box an address that would point at the box itself.
  • TLS remotes warn instead of failing silently - client certificates are host credentials and never enter the cage. When DOCKER_HOST points at a TLS-secured daemon, cleat says the handshake will fail and why, instead of leaving the box to a quiet dead end.

Fixes #

  • A missing socket is never bound - the docker cap used to mount /var/run/docker.sock unconditionally. On engines whose socket lives elsewhere, Docker would materialise a phantom directory at that path on the host. The bind is now guarded on a live socket, closing that hazard for good.
  • DOCKER_HOST trailing-slash tolerance - a unix:// endpoint ending in a slash (common from an env file) now resolves to the real socket instead of missing the liveness guard on a running daemon.
  • bash 3.2 strict-mode hardening - the install-shadow scan and cleat status guard BASH_SOURCE under set -u on macOS bash 3.2, with lint-clean quoting in bin/cleat and install.sh.

Changes #

  • CI now exercises the whole host matrix - 16 jobs per push: macOS on Apple Silicon and Intel, Ubuntu 22.04, 24.04 and arm64 (the arm64 image is built and booted), Windows via WSL2, real Colima and rootless Docker engines, GNU coreutils on macOS. The macOS suite runs sharded across four parallel runners.
  • The README gained a compatibility matrix and nav badges linking the site.
  • Test infrastructure hardening: the runner trusts bats exit codes so a killed run cannot pass, supports sharding, roots its temp dirs safely on macOS and closes a lock-reclaim race.
  • 2190 (+21) behavioral tests across 56 files. 582 (+20) mutations caught, 0 missed, 0 skipped (583 on a host with inotify-tools).

v1.4.2 #

The on-start "what's new" note went missing in 1.4.1. This restores it and guards against a repeat.

Fixes #

  • The release highlight shows again - the on-start "what's new" note is shown only for the version it was written for. 1.4.1 shipped with that gate left a version behind, so the note stayed silent for the whole release. The gate now tracks the release, so the flagship-feature note (forks, image paste and Homebrew) surfaces again on upgrade.

Changes #

  • A source guard now fails the test suite the moment that gate drifts from the version, so a release can never silently disable the highlight again.
  • 2169 (+1) behavioral tests across 56 files. 562 (+1) mutations caught, 0 missed, 0 skipped (563 on a host with inotify-tools).

v1.4.1 #

A Homebrew keg installed next to a curl install used to sit there invisibly. Cleat now warns about the split on every start.

Fixes #

  • A second install is surfaced on start - the one-install guard only covers cleat's own installers, so brew install could add a keg over a curl install with nothing flagging it, the split behind a stale cleat --version or an update that seemed to do nothing. When more than one cleat is on the machine, every interactive start now warns, names which one is running and which is shadowed, then points at the one-command fix: brew uninstall cleatdev/tap/cleat for a keg, or an rm of the shadow's path for a script install. It only warns, never blocks, since Homebrew installs its keg without cleat's guard and cleat can surface the split but not prevent it. cleat status shows the same list on demand. Silence the startup warning with CLEAT_NO_INSTALL_CHECK=1.

Changes #

  • The cleat status install list and the new startup notice share one renderer, so their output can never drift.
  • 2168 (+7) behavioral tests across 56 files. 561 (+3) mutations caught, 0 missed, 0 skipped (562 on a host with inotify-tools).

v1.4.0 #

Forked workspaces are the headline: several agents work one project at once, each in its own copy of the tree, plus native ctrl+v image paste and a hardening pass that keeps every other project out of a box.

The largest release in months. It ships forked workspaces for real parallel work, native ctrl+v image paste inside a box, per-box config in a single .cleat and a ~/.claude containment pass, on top of a 32-defect security audit of the whole CLI.

Features #

  • Forked workspaces - cleat start <box> --fork (or cleat run <box> --fork) gives a box its own copy of the project at /workspace instead of the live tree, so several agents can work one project without touching each other or your working directory. It is a copy rather than a git clone, so it carries uncommitted work, untracked sibling repos and even a project with no git at all. On APFS cp -c clones blocks copy-on-write, so the copy is close to free. Symlinks are copied as symlinks, so a keys -> ~/.ssh in the tree never materializes real key bytes into the cage. The launch summary prints the copy's age because a fork is a point-in-time snapshot. [fork] exclude in .cleat prunes paths from the copy and [fork] dir in the global config relocates where copies live.
  • cleat fork verbs - cleat fork start and cleat fork run are discoverable aliases for --fork, alongside cleat fork to list every copy with its size and age, cleat fork path to print a copy's path, cleat fork refresh to re-copy from the live tree, cleat fork rm to delete one copy and cleat fork prune to reclaim copies whose box is gone and clear any stale marker. cleat rm keeps a box's copy on purpose, because for a project with no git it can hold the only version of hours of work.
  • Native ctrl+v image paste - copy a screenshot on the host, click into a box's Claude prompt and press ctrl+v. About a second later Claude shows [Image #1], exactly as it does outside Docker. It is images only and enforced host-side, because clipboard text is where passwords and tokens live. There is no Dockerfile change, so no image spec bump and no box is recreated: every existing box gets it on its next session. Use ctrl+v rather than cmd+v on a Mac, since the terminal intercepts cmd+v. CLEAT_NO_CLIPBOARD_IMAGE=1 turns it off.
  • Per-box config sections - a box's own capabilities, resources, [setup] and fork settings now live in the project's one .cleat as [box.<name>.<kind>] blocks, replacing the old one-file-per-box .cleat.<box>. A declared section replaces the project default, an absent one inherits it and a declared-but-empty section is a real value, so a box can be locked to zero capabilities. cleat config <box> --list marks each value declared or inherited and names the box. Trust is per box and the box name is required on every edit, so no config change can silently apply to main.
  • ~/.claude containment - a box now sees only the project you gave it plus your Claude login and that project's history. Other projects' transcripts and edited-file history are no longer reachable from inside a box, where before a caged agent could read the full history of every repo on the machine. ~/.claude/skills and ~/.claude/plugins are masked read-only so a caged agent cannot plant an auto-loaded skill or write your host plugins. ~/.claude/hooks becomes a per-box empty directory so the cage cannot overwrite a script the host later runs. These are all run-argument changes, so no box is recreated. Installing a plugin from inside a box now fails by design: install it on the host.

Fixes #

  • 32 verified defects fixed in a post-fork hardening audit - an adversarial audit of the CLI after forks landed confirmed and closed 32 bugs, each with a test and a mutation that proves the test cannot be quietly disarmed. The worst were data loss: cleat fork prune deleted every workspace copy on the machine when Docker was stopped, an unvalidated box name walked rm -rf out of the fork root through x/../../../Documents and cleat nuke wiped fork markers while keeping the copies so the next --fork deleted the surviving work. Also closed: config writers that truncated in place now write to a temp file and rename, an indented [caps] or [setup] that was silently duplicated or deleted, a session key that lost every snake_case project's history under the docker cap, a caps trust hash computed after the prompt so a .cleat rewritten mid-prompt was recorded as approved, a hook event name interpolated into a jq program and a UTF-8 BOM that voided the first config section.
  • Clipboard and browser bridges no longer read through a symlinked payload - the box's clip directory is a read-write mount, so the box could drop a symlink where a payload belongs and have the host read any file the host user can read, then pipe it into the host clipboard or hand it to the URL opener. Both bridges now drop a link unread at the gate and again after the rename. They claim the file in a directory outside the mount so it cannot be swapped between the rename and the read.
  • A crashed session no longer latches the clipboard bridge on - a watcher marker left by a session that died without running its trap held the .host-ready sentinel on forever, so a later plain copy wrote to a bridge nobody was reading and the OSC 52 fallback that would have delivered it never fired. Dead watcher markers are now swept by their stamped pid at watcher start and at cleanup.
  • A Homebrew install is left to brew - a brew keg has no .git, so cleat update used to fall through to the generic "not a git installation" advice and tell the user to re-install with the curl script. On an Intel Mac that overwrites Homebrew's own cleat binary and leaves brew tracking a stale keg. cleat update now detects a keg and runs brew upgrade cleatdev/tap/cleat for you instead, replacing its own process so nothing below the guard can touch a git checkout that isn't there. cleat uninstall offers brew uninstall, asking first: on a git install it drops a PATH symlink and leaves the clone alone, while brew uninstall deletes the whole keg. It is terminal-only and default No, so no script or wrapper removes an install unattended. Doing that removal by hand was the older hazard, since rm -f /usr/local/bin/cleat deletes Homebrew's own symlink on an Intel Mac and leaves brew believing cleat is installed while the command is gone from PATH. cleat install refuses, because brew already linked the keg. The on-start upgrade offer now checks too: it was gated only on finding a .git, which is not enough where readlink -f is missing (macOS before 12.3). The install root resolves to the Homebrew prefix. On Apple Silicon /opt/homebrew is Homebrew's own git repository, so the offer fired and would check out a Cleat tag inside brew's checkout. Detection reads where the binary physically lives, never the directory it was invoked through and never by shelling out to brew, so where brew is off PATH the command is printed rather than run.
  • One cleat per machine, from cleat's own installers - the curl script and cleat install now refuse to create a second install (a brew install is Homebrew's own path and does not run this check). Two cleats on one machine is a state you cannot see from the inside: a bare cleat resolves to whichever comes first on your PATH. The loser is invisible until it bites, as a stale version that "fixes itself" after a shell restart or an update that appears to do nothing. curl | bash, install.sh --local and cleat install all scan the machine first and stop if a cleat already exists at a path they are not about to write. Re-installing over the path you already own is untouched, since that is an upgrade rather than a second install. A conflict is replaced with --force (curl -fsSL https://cleat.sh/install | bash -s -- --force), except two cases that are never replaced. A Homebrew keg, because that would leave brew tracking an install it no longer owns. A regular file, because that is your own copy rather than a link Cleat created. The scan covers every PATH entry plus /usr/local/bin, ~/.local/bin, /opt/homebrew/bin and /home/linuxbrew/.linuxbrew/bin even when those are off PATH, which is what catches an Apple Silicon Homebrew prefix from a shell that never ran brew shellenv. cleat status grew an Install section that names every cleat it finds and marks which one is running, shown only when there is more than one.
  • Cleat's own state moved out of the install directory - the update throttle, the release-highlight memory and the Docker, disk and Claude check stamps now live in ~/.config/cleat/state/. On a Homebrew install they used to sit inside the keg, which brew upgrade deletes, so every throttle reset and every "don't ask me about this version again" was forgotten on each upgrade. On older macOS they landed at the root of the Homebrew prefix, where nothing ever removed them. Anything left in the old place is carried over on first run, including a script install's ~/.cleat when you move to Homebrew, so a switch needs no action from you at all. This also means switching between the install script and Homebrew loses nothing: config, trust, boxes, fork workspaces and Claude history were always kept outside the install. Boxes are identified by project rather than by where Cleat lives.
  • A failed image pull no longer ends in a confusing build error - when the prebuilt image cannot be pulled, Cleat falls back to building locally, which needs the docker/ directory that ships beside the script. A packaged install may not have one. Instead of a raw failure about a build context that was never there, Cleat now says so and points at retrying the pull.

Changes #

  • The old .cleat.<box> per-box config files are no longer read (the .cleat.<box>.env env sidecar is unaffected). Move their settings into .cleat as [box.<name>.<kind>] sections. Cleat names the file and the sections to move at launch. A Cleat older than this release cannot see [box.*] sections and falls back to [caps], so a per-box reduction reads as the permissive project set on a teammate's older CLI. That fails open, which the first per-box section writes into the file as a comment.
  • No image spec bump this release. Forks, native paste and the ~/.claude containment pass are all run-argument changes, so no existing box is recreated. Every box picks up the new behavior on its next session.
  • Test infrastructure only, no user-facing change: an automated host verifier now drives the manual release scenarios end to end, the integration harness runs on macOS and the mutation harness lost a GNU-only sed address plus a mutation the host could satisfy.
  • 2161 (+388) behavioral tests across 55 files, covering forks, the fork verbs, per-box config sections, ~/.claude containment, native ctrl+v paste, the symlink read-through fixes and the Homebrew guard on all four of its call sites.
  • 558 (+174) mutations caught, 0 missed, 0 skipped (559 on a host with inotify-tools).

v1.3.1 #

Project provisioning can put a toolchain on PATH again. On macOS it could not. The failure took the whole [setup] payload down with it.

A patch release for one image bug found while writing the provisioning docs. Cleat will offer to refresh your box image on the next start, because the fix lives in the image rather than the CLI.

Fixes #

  • [setup] provisioning failed on macOS whenever a step touched a shell rc file - the box remaps its user to your host account, then takes ownership of the paths that need it. The shell rc files and ~/.config were not in that set, so on any host whose user id is not the image's own (which is every Mac) a [setup] step that appended to ~/.bashrc or ~/.profile, or created a directory under ~/.config, was refused. A payload runs under bash -e, so that refusal aborted the entire provisioning run. This is what rustup, the .NET install script and the uv installer all do by default. The box now takes ownership of those paths too. ~/.config is claimed non-recursively on purpose, so the gh capability's mount of your real ~/.config/gh keeps host ownership.

Changes #

  • The bundled image spec moved to 4, so existing boxes are offered a refreshed image on the next start. Accepting it recreates the box: your project files and Claude state are untouched, ad-hoc installs inside the box are not. Any approved [setup] re-runs automatically on the new box.
  • The examples/setup starters no longer depend on that fix landing, so they also work on a box created before it. rustup runs with --no-modify-path and uv installs unmanaged into ~/.local/bin. The .NET and Rust examples expose their binaries by symlinking into ~/.local/bin instead of editing a shell rc file. The box already puts ~/.local/bin first on the session PATH. A provisioning payload never sources an rc file anyway.
  • The Rust example installs build-essential and pkg-config. The .NET example installs libicu72 on arm64. The slim base ships neither, so rustc could not link and dotnet --info aborted with exit 134.
  • 1773 (+3) behavioral tests across 52 files, including one that fails if the ~/.config ownership fix ever becomes recursive and starts rewriting your real ~/.config/gh.
  • 384 (+2) mutations caught, 0 missed, 0 skipped.

v1.3.0 #

See your Docker disk, reclaim it safely and never get blindsided by a full store, plus durable project setup, a config editor and a launch that stops when Docker is misconfigured.

A stability and maintenance release that bundles several unreleased improvements. The headline is a disk-aware cleanup system that answers a real field report (a fresh box reading 100% right after cleat rm), alongside trust-gated project provisioning, an interactive .cleat editor and a launch gate for a misconfigured Docker.

Features #

  • cleat storage - a read-only, cross-OS breakdown of the shared Docker store: a cap-relative fill bar, per-type totals (images, containers, volumes, build cache with reclaimable), then your attributable share (cleat image and box counts plus reclaimable images), with the shared build cache and everything else honestly labeled other projects.
  • Disk-fill advisory and gate - keyed on the store's fill percentage read from the just-started box, so a 60 GB Docker Desktop disk and a 1.8 TB one trip at the same fullness. At 85% full with little free space a daily read-only advisory names the fill and the levers. At 95% with under 10 GB free it holds the interactive launch on a keypress, the way the Docker memory gate does. CLEAT_NO_DISK_GATE=1 skips the hold. An ENOSPC backstop covers a store too full for a box to start at all.
  • cleat prune --cache - clears the shared, regenerable Docker build cache that bare cleat prune never touched (the heavy-rebuild hog). It is a typed flag behind a loud default-No confirm and runs reclaimable-only, never a cold wipe. --yes skips the prompt for scripts.
  • Cross-OS reclaim guidance - the disk advisory, gate and cleat storage write their fix steps for your actual engine: the Docker Desktop disk slider, OrbStack auto-reclaim, Colima disk resize and fstrim, a native-Linux prune, a WSL disk compaction or .wslconfig for a WSL2 memory fix.
  • Project provisioning with [setup] - a trust-gated [setup] section in .cleat (plain shell lines plus a script <path> directive) that runs once per container, as coder, before the session starts, so a project's real dependency stack survives every recreate. Consent is a second, separate class from capability trust (CLEAT_TRUST_SETUP / --trust-setup, deliberately not granted by project trust). Box-aware cleat trust and cleat untrust. A new cleat setup [box] verb runs the provisioning on demand against a running box. cleat setup [box] --show previews the payload, the trust state and the run-once marker without executing anything.
  • .cleat editor - cleat config is now an arrow-key editor for capabilities and [resources] (memory, cpus) in one screen, with a row to generate a project .cleat. New --memory and --cpus flags. --list now prints the resources block alongside capabilities. Both rings are built from your actual machine: cpus from the core count Docker reports (never more cores than you have), memory in real stops up to the size of your Docker VM, so a 24 GB VM can be asked for all 24 GB. Past 8 GB the editor annotates rather than blocks, with a note that escalates as the number climbs and a full explanation at the whole-VM value, where a box that grows into everything leaves nothing for the daemon and the VM's own OOM killer starts firing. A project .cleat still stops at 8 GB, matching the clamp the runtime applies to repo-shipped values.
  • Docker-config launch gate - when the Docker VM memory is undersized or swap is at the default, the interactive launch holds on a prominent banner until you press Enter, so a misconfiguration cannot scroll past unread. Interactive-only, CLEAT_NO_DOCKER_GATE=1 skips it.

Fixes #

  • Prune offered a cleanup that removed nothing - the on-start prune offer counted images a stopped box still pins, which docker rmi refuses to remove, so a "N stale images" prompt was followed by "Nothing to prune". The offer now counts only images a prune can actually reclaim.
  • A full host process table corrupted the Claude TUI - during a heavy many-agent run, host fork-exhaustion errors from Cleat's background watchers leaked into the terminal and scrambled Claude Code's interface. Watcher output now goes to a capped per-box log. A one-line diagnostic at session end explains the host limit and the fix.
  • WSL2 got a launch hold it could not act on - on the Docker Desktop WSL2 backend the memory advisory named the Resources slider (absent there) and the new gate held the launch on it. Cleat now names .wslconfig on WSL and downgrades that hold to a non-blocking advisory, since WSL memory is elastic.

Changes #

  • cleat nuke no longer runs a global dangling-image prune that would delete other projects' dangling images. It scopes the sweep to cleat-labeled images and discloses that it clears the shared build cache. The false "and volumes" wording was dropped (Cleat creates no volumes).
  • cleat rm now prints a one-line note that the box's disk layer is freed but its image and the shared build cache remain, pointing at cleat storage.
  • One hardened Docker size parser now backs every reclaimable figure the CLI reports, fixing a latent size misparse on mawk and under a comma-decimal locale.
  • 1770 (+318) behavioral tests across 52 files: the disk parser, the two-tier gate, the engine classifier, cleat storage, cleat prune --cache, the nuke narrowing, [setup], the config editor, the machine-sized resource rings and the Docker gate each pinned by tests.
  • 382 (+78) mutations caught, 0 missed, 0 skipped.

v1.2.5 #

Your clipboard stops getting clobbered: a box never replays an old copy onto the host again.

Starting cleat on an existing box could instantly overwrite the host clipboard with whatever was copied inside that box last session. This release fixes the replay at its root and hardens the whole bridge teardown path.

Fixes #

  • Stale clipboard redelivery - the bridge's payload file survived between sessions and every new session's watcher treated it as a fresh copy on its first poll tick, pushing a previous session's clipboard over whatever you had on the host. The watcher now consumes a payload the moment it delivers it (an atomic claim, so it can never be replayed in any watch mode) and sweeps any leftover older than 5 seconds at startup before the bridge comes up.
  • Same-second copies no longer swallowed - the old polling dedup compared mtimes with one-second resolution, so a second copy within the same wall-clock second was silently dropped. Consume-on-read retires the dedup entirely.
  • Backward clock steps can't resurrect the bug - a leftover whose mtime sits ahead of the clock (an NTP correction, a VM clock catching up after sleep) now reads as stale and is swept instead of redelivered.
  • Concurrent sessions can't eat each other's copies - in-flight claims live outside the namespace a sibling session's exit cleanup sweeps, so closing one terminal never deletes a copy another session is mid-delivering.
  • Watcher orphans on inotify hosts - on Linux hosts with inotify-tools installed, session exit left the clipboard watcher and its inotifywait blocked forever as orphan processes (a TERM never lands while the watch tool blocks). Cleanup now kills the blocking child directly.

Changes #

  • The Linux CI leg now installs inotify-tools so the event-driven watcher branch runs for real on every push, while the macOS leg keeps covering the polling fallback. That coverage is what surfaced the orphan leak above.
  • 1452 (+5) behavioral tests across 47 files: stale redelivery, future-dated leftovers, claim consumption, claim namespace and the orphan reap each pinned by a test
  • 304 (+6) mutations caught, 0 missed, 0 skipped

v1.2.4 #

The kit learns from a real job: sequential dispatch, honest worker reports and a built-in way to verify it is working.

A deep re-audit against Anthropic's coordinator-pattern cookbook, plus one big real-world build, turned into a second generation of the flagship kit's delegation policy.

Features #

  • Kit delegation policy v2 - the plan-big-execute-small fragment now teaches dispatch craft: a dispatch names the files, the exact change, the constraints and the test to run, so the worker executes instead of re-deriving context. It prefers fewer, larger chunks because every dispatch pays a fixed overhead. A dispatch that dies with an error is re-dispatched unchanged instead of absorbed into the expensive main loop. Plans build on scout-verified findings, not on memory. Each rule ports a caveat from Anthropic's cookbook.
  • Worker reports open with a status - done as dispatched, done with deviations, partial or blocked. Large changes report per-file line ranges instead of pasting whole diffs, so the planner's context stays lean and review happens in git diff.
  • cleat kit show tells you how to verify routing - after a heavy session /usage should show the bulk of tokens on the worker model. A heavy session showing only your session model means delegation is not happening.

Fixes #

  • Parallel worker dispatch is out - launching several workers in one message scrambled briefs, spawned duplicates and clobbered files on a real build. The failure classes match open upstream Claude Code bugs (duplicated parallel fan-out, concurrent edit clobbering), unfixed as of this release. The fragment now dispatches one worker at a time: dispatch, review, then dispatch the next. The ban lifts once upstream ships deduplication and edit protection.
  • The confirm screen stops overstating scout - scout structurally lacks Edit and Write but its Bash stays inspection-only by instruction, so the screen now says read-only by contract instead of stating it flat. When your own agent shadows a kit agent by name, the collision warning now says the kit's policy will steer your agent with its model and its tools.

Changes #

  • 1447 (+8) behavioral tests across 47 files: every new fragment rule, the worker report contract, the sequential-dispatch ban, the confirm-screen wording, the collision note and the kit show verification note each pinned by a test
  • 298 (+8) mutations caught, 0 missed, 0 skipped
  • README copy polish

v1.2.3 #

The kit picker stops eating arrow keys and starts explaining itself. Every box finally learns the clipboard rules.

Features #

  • Kit picker: detail pane, chevrons, arrow-key models - the kit list now shows the highlighted kit's full description in a detail pane, so nobody has to enable one to find out what it does. The model rows show the active value in ‹ › chevrons and ←/→ (or space) cycles it, ⏎ or → selects and the hint lines name the keys. The non-TTY text picker lists each kit's description too.
  • The kit carries its provenance - the enable screen credits the pattern to Anthropic's cookbook by its own title (big models for planning, small models for execution), cleat kit show prints the notebook URL and the description was rewritten around it: worker and scout each run in their own context window so the main session stays lean, the mechanical bulk bills at the worker model's rate and heavy work burns your rate limit far slower.

Fixes #

  • Arrow keys no longer close the pickers - every unrecognized escape sequence (left and right arrows, PgUp, function keys) decoded as Escape and the picker loops cancel on Escape, so pressing the right arrow on a model row (the most natural key there) closed the picker instead of changing the model. The caps picker shared the decoder and the bug. Only q or a bare Escape cancels now. Unknown keys are ignored.
  • The clipboard rules reach the box at last - since v0.1.0 the image baked in-box guidance (copying with xclip/pbcopy/clip reaches your host clipboard, paste-back is not supported, exit 0 means the copy worked) at ~/.claude/CLAUDE.md and since v0.1.0 the host ~/.claude mount shadowed it, so no box ever saw it. The generated CLAUDE.md every box mounts now composes three marked layers: your global content first, byte for byte, then the box notes, then the kit section when one is enabled. Existing boxes pick it up on their next start with no recreate.

Changes #

  • 1439 (+15) behavioral tests across 47 files: the keypress decoder pinned key by key, the picker driven through arrows, chevrons, pane and cancel paths on a real pty, the three-layer CLAUDE.md compose and its ordering, a byte-identity guard between the box notes and the image bake and a pane-fit guard so no future kit description silently truncates
  • 290 (+6) mutations caught, 0 missed, 0 skipped

v1.2.2 #

The kit's scout subagent loads again. Every mode of working now routes to the kit's cheap models.

A live probe of the plan-big-execute-small kit (dispatch each agent, ask for its model) showed the worker answering on Sonnet 5 and the scout never registered at all. The follow-up audit found that even a healthy kit let exploration and multi-agent fan-outs ride expensive models.

Fixes #

  • Kit scout agent registers again - since v1.2.0 the generated kit-scout.md carried a colon-space inside its unquoted YAML description ("Use for all exploration: finding"), which is invalid YAML and Claude Code drops an agent whose frontmatter fails to parse without a word. Every scout dispatch errored with "agent not found" and the planner quietly fell back to searching in the main loop, so the kit ran at half strength with nothing visibly broken. The description is reworded, the no-colon constraint is documented at the source and a regression test now validates every generated kit agent frontmatter as strict plain-scalar YAML. Existing kit boxes heal on their next cleat start with no recreate (the overlay regenerates in place). The scout appears in the first session started after that.
  • Kit routing covers every mode of working - the kit's delegation policy only governed the planner's own loop, so exploration could ride the built-in Explore agent, multi-agent workflow fan-outs defaulted to the expensive session model and sizable single-file work never triggered the worker. The policy now names worker and scout as the only subagents the planner dispatches (built-ins are banned by name), routes workflow stages through them explicitly, widens the dispatch trigger to sizable single-file changes, keeps the locating step of judgment reads on the scout and requires the session to report a missing worker or scout as a broken kit instead of quietly substituting a built-in. The worker also gains a tools pin (Read, Edit, Write, Grep, Glob, Bash) mirroring the scout's.

Changes #

  • 1424 (+6) behavioral tests across 47 files: a frontmatter class check that generates the real overlay and fails on any unparseable kit agent, plus five policy-content tests (workflow routing, built-in ban, single-file trigger, scout-locates carve-out, worker tools pin), each mutation-verified
  • 284 (+7) mutations caught, 0 missed, 0 skipped

v1.2.1 #

Sessions on a root-only host launch again. The memory advisories stop calling a native Linux engine a VM.

A first run on a stock Ubuntu VPS surfaced both: the box came up green and Claude immediately exited and the readiness nod above it claimed a 4 GB "VM" with "room for many parallel sessions" on a host that has neither.

Fixes #

  • Sessions on a root-only host no longer die at launch - Cleat maps the box user to your host UID so workspace files come back owned by you and on a root-only host (a stock VPS image, or sudo cleat) that makes the box user root, which Claude Code itself refuses to run with permissions skipped (its root/sudo guard, upstream issue 9184). Every session now rides IS_SANDBOX=1, Claude Code's own escape hatch for sandboxed containers, on root hosts only: ordinary boxes keep an unchanged environment and existing root boxes heal on their next session with no recreate.
  • Memory advisories name the real pool on a native Linux engine - the readiness nod on a 4 GB VPS read "Docker tuned for Cleat (4 GB VM, room for many parallel sessions)": on a native engine no VM exists, nothing was tuned and four 1 GB ceilings is not room for many. Where the daemon does run in a VM (Docker Desktop anywhere and every macOS backend) the wording is unchanged. A native engine now reads "Docker ready for Cleat (N GB RAM, ...)" and drops the parallel-headroom claim below 8 GB and the overload warning and the cleat status overcommit line say the host has only N GB of RAM instead of naming a VM you don't have.

Changes #

  • 1418 (+7) behavioral tests across 47 files: the root-host IS_SANDBOX injection pinned on both branches at source time, the engine pinned in every readiness, overload and status wording test with refutes covering both readings and a strict-mode smoke pass over the native small-host path
  • 277 (+5) mutations caught, 0 missed, 0 skipped

v1.2.0 #

Kits: one command enables a tuned Claude Code setup inside the box. Cleat now starts Docker for you when the daemon is down.

The release that makes a Cleat box the nicest place to run Claude, not just the safest: maintainer-curated kits bring the plan-big-execute-small orchestration pattern to any box with one command. The Docker tax shrinks with an autopilot that launches your engine, waits and continues, plus a consent-first install offer on machines that have no Docker at all.

Features #

  • Kits: curated Claude Code pre-configurations, per box - cleat kit plan-big-execute-small enables the flagship kit, Anthropic's coordinator pattern (big models plan, small models execute) adapted into one command: run your session on Fable 5 and it holds the plan and every review while worker and scout subagents (Sonnet 5 by default) carry the mechanical bulk in their own context windows, so multi-file work burns your rate limit far slower. The kit merges on top of your own config instead of replacing it: your global CLAUDE.md comes first with the kit's delegation policy appended under a marked header, your agents win name collisions and everything applies inside that box only. Enabling, switching and turning a kit off never recreates the container and takes effect next session (boxes created before kits existed lack the mount points, so enabling there offers an explicit rebuild first). Bare cleat kit opens an interactive picker (kit, then per-agent models). cleat kit show prints the kit's full contents before you trust it. cleat kit off returns to vanilla. Pin or swap the agent models under a [kits] section in the global config (worker_model = haiku). The planner is always your session's model and a hint after enabling names the knob (/model inside the session). Adapted from the pattern in Anthropic's cookbook, with its guardrails encoded as behavior: workers report distilled results instead of raw logs, scouts never guess when a search comes up empty and the planner reads subtle code itself instead of trusting a summary.
  • Read-only masks close the host config write channel - a caged agent could previously write to the user-level ~/.claude/CLAUDE.md, agents and commands that your native host Claude then obeys. Those three surfaces are now read-only masks inside every new box (kit or not): the box sees your real content merged with the kit's, but cannot write a byte of it back. Project-level .claude files in your repo keep working through /workspace. Boxes created before the masks show a one-line recreate note on every start until rebuilt.
  • Docker autopilot: a down daemon is started for you - cleat on a stopped engine used to die with a raw "Cannot connect to the Docker daemon". Session commands now detect the engine you actually use and start it: Docker Desktop, OrbStack, or Colima (named profiles included) on macOS, Docker Desktop or a rootless engine on Linux and Docker Desktop from inside WSL2, then wait with a spinner and continue. Root-owned Linux engines get the exact sudo command printed instead of a privileged attempt, remote daemons (tcp://, ssh://) are never started for you and a daemon that is up but unreachable because your user lacks the docker group gets the real diagnosis and the usermod remedy instead of a false "Docker is down". Wait is bounded by CLEAT_AUTOSTART_TIMEOUT_SECS (default 90). Opt out entirely with CLEAT_NO_AUTOSTART=1. cleat status now says "Docker isn't running" with the exact start command instead of pretending no boxes exist.
  • No Docker at all? Cleat offers to install it - when the docker CLI itself is missing, interactive sessions print the exact command and ask before running anything: a three-way Homebrew menu on macOS (Desktop, OrbStack, or Colima, with licensing stated), the official convenience script on Linux (downloaded to a private directory and shown, never piped blind into a shell) and winget under WSL2. Default is No, non-interactive runs never prompt and CLEAT_NO_AUTOSTART=1 suppresses the offer.

Fixes #

  • Login opens your browser in boxes created before v1.1.1 - the v1.1.1 BROWSER fix landed at container create, but docker exec inherits the container's environment frozen at create time, so existing boxes never saw it and /login stayed on the manual code-paste flow with nothing ever suggesting a recreate. Every session now injects BROWSER at exec time (cleat, cleat shell, cleat login), healing every existing box on its next session with no recreate. A BROWSER= in your .cleat still wins.
  • A second session can no longer swallow a login link - starting cleat login or a shell next to a running session wiped the browser bridge file unconditionally, so a login URL written moments earlier could vanish before any watcher opened it. The startup sweep is now age-gated: fresh URLs survive and get opened, only stale leftovers from dead sessions are cleared. Every open-or-defer decision in the bridge log now records the URL itself, so a link that "did nothing" can be recovered by hand, including with the bridge set to off.
  • A fresh machine without ~/.claude/settings.json can create a box - on macOS, creating the very first box on a host where Claude had never written its settings file failed with an opaque Docker error (a nested file mount whose target was missing in the parent bind source). Cleat now pre-creates the file and refuses a broken symlink at any masked path with a clear remedy instead of a raw trace.

Changes #

  • cleat describe sees new companions - per-box kit selections live next to box descriptions, are removed with cleat rm and are wiped by cleat nuke.
  • Total-memory detection fails safe - a garbled /proc/meminfo line could kill a session under strict mode instead of skipping the memory advisory.
  • The README's top fold now carries a real recorded demo of an agent probing for credentials and wiping the box, not a mockup
  • 1411 (+157) behavioral tests across 47 files: the kit merge and mask semantics (user content first, collisions by agent name, inode-stable regeneration on a running box, the :ro masks, broken-symlink refusal, the pre-mask recreate note), the full autopilot launch matrix (macOS Desktop, OrbStack, Colima profiles, Linux rootless, WSL2 interop gating, remote refusal, bounded timeout, kill switch) and the install offer's consent gates, the exec-time BROWSER injection pinned per exec site, the age-gated bridge sweep from both sides and the picker TUI driven on a real pty
  • 272 (+31) mutations caught, 0 missed, 0 skipped
  • Note on upgrade: existing boxes keep working untouched. Recreating a box (cleat rm && cleat) is only needed to gain the new read-only masks and each start says so until you do

v1.1.1 #

Logging in to Claude opens your browser again. A second terminal no longer asks you to log in after you have already authenticated.

Two login regressions, both surfacing after a recent Claude Code update: the login link stopped opening the browser and always fell back to pasting a code and a box you had logged out of kept demanding login on every session even after you signed back in elsewhere. Both were traced to changes in how recent Claude builds handle login, then fixed and hardened.

Fixes #

  • Logging in to Claude opens your browser again - a recent Claude Code update (2.1.191 and later) stopped opening any URL from inside a display-less Linux container unless the BROWSER environment variable is set. In that state it only ever hands its opener the hands-free loopback login URL (the manual code-paste URL is printed, never opened). A Cleat box is display-less Linux and set no BROWSER, so Claude never invoked the container's open shim, the host-side bridge had nothing to open and every login dropped to "paste this code". Cleat now points BROWSER at its bridge shim inside the box, so cleat login (and the first-run login) opens the real login page on your host and completes automatically through the callback proxy, the way it did before. The variable is baked into the image and also passed at container start, so a box created from an older image is fixed without a rebuild and a BROWSER you set in .cleat still wins.
  • A second terminal no longer re-prompts for login after you have authenticated - logging out inside one box writes hasCompletedOnboarding: false into that box's per-project Claude config and deletes the shared credential file. Recent Claude builds gate the startup login screen on that one flag, so the box you logged out of kept asking you to log in on every session even after you signed back in from another terminal and the shared credential was restored. Cleat only re-asserted that flag when it first created a container, never afterward. It now heals the per-project config whenever you start a stopped box, resume one, or attach to a box that is already running (in place and only when nothing is working inside it) and it carries an identity you established in any box across to boxes that were created earlier. Log in once and every box is logged in.

Changes #

  • The browser bridge now covers cleat shell and sweeps its own stale markers - cleat shell runs the same host-side browser watcher a session does, so a login started from a raw shell completes instead of waiting forever on a callback nobody was proxying and the watcher clears expired single-open debounce markers when it starts so a long-lived clip directory cannot accumulate them.
  • Adopting this release refreshes the image once - the BROWSER shim is baked into the image, so the image content advances to spec 3 and the first start after upgrading offers the usual one-time "refresh the image?" prompt. The fix already works without it (the variable is also passed at container start), so the refresh is optional and version-only releases after it will not prompt again.
  • 1254 (+24) behavioral tests across 45 files: the auth-URL classification truth table and its strict-mode probes, the BROWSER shim present at create and ordered before user [env] so a .cleat override wins, the cross-box identity fallback and the start, resume and running-box attach heal paths (with a live-agent skip and a never-truncate guard), the cleat shell watcher and the stale-marker sweep. Two adversarial-review rounds caught and fixed a real regression in the first cut (a rebuild that could drop a box-only top-level config key for API-key users) before release.
  • 241 (+11) mutations caught, 0 missed, 0 skipped

v1.1.0 #

Closed-terminal sessions stop reserving Docker VM memory on their own. A brand-new project no longer asks you to log in to Claude when you are already logged in on the host.

The first two issues after 1.0, both from a single screenshot: a Mac left running for hours where closed-terminal boxes had over-committed the Docker VM and a fresh project that re-prompted for login even though Claude was authenticated on the host and older projects still worked.

Features #

  • Idle sessions are stopped automatically to free Docker VM memory - closing a terminal ends the Claude session but leaves its box running, still holding its memory ceiling, so a day of closed terminals can over-commit the Docker VM and make every session swap. On each interactive start, Cleat now stops other boxes that are safe to stop and tells you what it freed, in one line. A box is only stopped when nothing is running inside it (so a session working unattended, with its terminal left open, is never touched), it is not the box you are launching and it has been idle past a grace window (30 minutes by default). A stopped box is preserved: run cleat in its project to bring it right back. Disable the sweep with CLEAT_NO_IDLE_SWEEP=1, or change the grace with CLEAT_IDLE_GRACE_MINS.

Fixes #

  • A brand-new project no longer re-prompts you to log in to Claude - on macOS, logging in again on the host rotated the Keychain token and invalidated the older copy in the shared credential file. Sessions already running kept working on a live token, but a freshly created box read the stale one and dropped to a login screen. Cleat now refreshes that shared credential file from the Keychain when its token has expired and the Keychain has a newer, valid one and never overwrites a still-valid in-box token, so a fresh box (and any box on its next resume) starts authenticated. A new project is also pre-trusted inside the cage so a newer bundled Claude does not re-run its first-run onboarding there.
  • The cleat status memory line can no longer contradict itself - its over-commit warning compared running limits against the kernel's reported VM memory but printed the configured slider size, so a total landing between the two could read as "reserves 23 GB on a 24 GB VM". It now compares and prints in the same whole-GB unit, the same way the on-start advisory already did.

Changes #

  • The over-commit notice reads clearly - it now names how many sessions are still running and reminds you that closing a terminal does not stop its box, instead of a bare GB number that looked like an off-by-one against your VM size. "promised X GB" is now "reserve X GB of memory ceilings" and the stale "~4 GB ceiling" wording was corrected (the per-box default is a quarter of the VM, 4 to 8 GB).
  • 1230 (+36) behavioral tests across 45 files: the idle-session sweep (the liveness gate across a live, detached and unreadable process list, the grace window against both mocked and real run-dir timestamps, the unknown-age skip, self-exclusion, multi-box stop with the freed-memory summary and the malformed-argument, terminal and env-var gates), the expiry-aware credential re-seed paths with a pinned clock, the per-workspace onboarding seed, the session-count copy and the status-line contradiction guard and a strict-mode regression for the singular-session notice
  • 230 (+12) mutations caught, 0 missed, 0 skipped

v1.0.0 #

The first stable release. A CLI version bump no longer re-downloads the image and recreates your container when nothing in the image actually changed.

Cleat is 1.0. The headline is that upgrades stop being disruptive: the on-start image-refresh prompt is keyed to the image's real content, not the CLI version, so a routine release leaves your running container and its writable layer untouched. This release also pins the base image by digest, fixes two config-parsing bugs and restyles the workspace-trust prompt.

Features #

  • The image refreshes only when the image actually changed - the on-start "refresh the image?" prompt was tied to the CLI version, so every release asked you to re-pull the image and recreate this project's container, discarding its writable layer (anything you installed in the box, caches) even when the image was byte-identical. The decision is now keyed to the image's content, tracked by a spec number baked into the image as a label, so a version-only release leaves your container alone. It still prompts when something that lands in the image genuinely changes: the entrypoint, the clipboard or browser bridge, the Dockerfile, or the base. Images built before this scheme are classified by the version they were built at, so upgrading to 1.0.0 is a single expected one-time refresh to adopt the pinned base, after which version-only releases never recreate again.
  • The base image is pinned by digest - the container base is pinned to an exact multi-arch image digest instead of a floating tag, so builds are reproducible and a base or security update becomes a deliberate, reviewed change that ships through the same content-aware refresh path instead of drifting in silently.

Fixes #

  • A hand-edited .cleat no longer drops its last line - a .cleat (or its [resources] block) whose final line had no trailing newline silently dropped that line: a capability listed last was never requested, so the trust prompt never fired and the cap never applied and a final memory = 8g fell back to the VM-derived default instead of your configured ceiling. Both the capability and resource readers now read that final line, matching the env-file parser.

Changes #

  • The workspace-trust prompt matches the other startup prompts - approving a project's .cleat capabilities now uses the same single aligned line and [y/N] question as the image-refresh, Claude-update and recreate prompts, with docker flagged amber because it breaks the sandbox, instead of a bordered box. It still defaults to deny: only an explicit yes applies the caps.
  • 1194 (+37) behavioral tests across 44 files: the image content-spec comparison and its legacy version inference (per-field numeric, the equal and just-below boundaries, suffix stripping, leading-zero safety), the no-trailing-newline reads for the capability and resource parsers with mutation-verified regressions, duplicate-key first-wins for resources and the restyled trust prompt (approve, deny, empty-and-EOF default-deny, the no-box style, re-prompt wording and the docker amber rendering)
  • 218 (+6) mutations caught, 0 missed, 0 skipped