Skip to content

v1.5.0: accounts with live switching, session management and bridge hardening

Choose a tag to compare

@coder-pm coder-pm released this 21 Sep 22:45
· 47 commits to main since this release

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).