Codeman logocodeman GitHub →

Changelog

Every Codeman release, newest first. Codeman follows semantic versioning; each release is also on GitHub Releases and npm as aicodeman.

1.33.1 2026-09-24 release →

Patch Changes

  • Thanks

    • @irisitymichaelgrundberg for closing sessions whose agent exited cleanly (#486), built carefully around every way a pane exit can lie (a SIGKILL with no status, a single misread), with the .claude-images guard split into its own commit as asked.
    • @opticon454 for the live-refreshing case picker and Manage search (#483), and for the uv/uvx, libsecret and pnpm additions to the Docker images (#487, #485).

    Finished sessions close themselves (#486). A session whose agent you ended with /exit is now closed the same way the X button closes it, so finished sessions stop piling up on the board; the conversation stays resumable from the Resume list and the lifecycle log records "agent exited cleanly (status 0)". Only an explicit exit status 0 with no signal, confirmed by two pane reads, qualifies: a crashed or OOM-killed agent keeps its row with the exit code on the tab. The phone overview and desktop home rail now say exited instead of idle, reboot restore no longer offers to rebuild a session whose agent had exited, and closing one session no longer deletes the .claude-images directory that a sibling session in the same case still uses. Thanks @irisitymichaelgrundberg.

    Search in the phone Select Case sheet (#488). The bottom sheet gains a "Search cases" field that filters by name (every word must match, any order, ignoring case), Enter picks the case when exactly one row is left, and Escape clears then closes. Also fixes a dead band under Create New Case and a list shorter than the sheet could show.

    An oversized paste no longer jams a session's input (#484). A single input over the 64 KiB frame limit used to be refused by both transports, retried every 2 s forever, block every later input for that session and come back from localStorage on each reload. Pastes over the limit are now split into in-limit frames delivered in order (up to 1 MiB; larger ones are refused with a toast and never queued), a refused frame is dropped instead of retried, frames persisted by an older build are pruned on load, and the WebSocket answers an oversized frame with an explicit too_large error instead of silence.

  • Add a search box to the Manage tab of the Add Case dialog. It filters the case list by name or path, and the reorder arrows are disabled while a filter is active so a swap cannot involve a hidden case.

  • The case picker now refreshes its list from /api/cases when it opens and every 5 seconds while it stays open, so folders deleted or created on disk appear without a page reload. If the selected case has been removed, the picker falls back to another case without saving it as the last-used one.

    Thanks @opticon454.

  • Install uv and uvx in the Compose server image and the agent image, so MCP servers launched with uvx (such as the Nginx Proxy Manager MCP) can be enabled by Codex instead of failing with uvx not found. Both images also install libsecret-1-0, the native library the keytar dependency of the Azure DevOps MCP (@azure-devops/mcp) needs; without it the server crashes before answering the MCP initialize handshake.

    The Compose server image now also carries pnpm: dsh plugin spawns a literal pnpm with no npm fallback, so the Run menu's "DeepSeek - add a terminal profile" button failed with dsh: pnpm not found on PATH there. Because this release changes server.Dockerfile, the in-app updater asks Compose deployments to rebuild the image (Update-Codeman.sh) rather than applying it in place.

    Thanks @opticon454 (#487, #485).

1.33.0 2026-09-24 release →

Minor Changes

  • CLI management from Settings (#476, finishing the CLI registry work from #343). ~/.codeman/clis.json used to be hand-edit only; with the new opt-in cliManagementEnabled switch (synced, default OFF) App Settings → Agents & CLIs can enable or disable any CLI, install a missing stock CLI with its vetted install command, and add, edit or remove custom CLIs. Six new endpoints back it (GET/POST /api/clis, PUT /api/clis/:id, POST /api/clis/:id/install, PUT /api/clis/custom/:id, DELETE /api/clis/:id), documented in docs/api-reference.md. Every write is refused while the switch is off, is admin-only in multi-user mode, is serialized on one queue, and refuses to overwrite a clis.json that does not parse or has group/world permission bits. A custom entry is re-validated through the same schema as the stock ones and its install text is never executed. shell cannot be disabled. The Run menu and the welcome screen are now built from the enabled catalogue, so the welcome screen also offers Codex, Shell and any custom CLI, and the stock Claude entry is labelled "Claude Code".

    Models: Opus 5.5 (claude-opus-5-5, 1M context capable) is offered in App Settings → Models and in task routing (#480).

    Self-update: on a macOS launchd-daemon install, a Homebrew node upgrade could leave update-status.json stuck at queued, which made every later update fail with "An update is already in progress." The updater now falls back to node on PATH when the server's own node binary is gone, and an in-flight status that has not been written for 15 minutes is failed on the next read. A graceful shutdown that hangs is now force-exited after 10 s (and the launchd updater SIGKILLs a server that has not exited after 30 s), so launchd can start the new build instead of leaving the service down (#478). Both fixes protect updates that start FROM this release.

    Session Manager (Cmd+K): rows keep their mode, claudeSessionId and resumeId, so the ⋯ menu's Resume session relaunches a Codex row as Codex on its own conversation, and the mode badge shows as it does on the home list (#477).

    Maintainer fixes applied while landing #457: renaming a tab to the name it already has (the Session Options field saves on blur) is now a no-op, so it no longer pins the placeholder as the /resume title again; Docker sessions skip the transcript title sync, since their transcript lives in the container; and the agent skill's messaging examples no longer use a w<N>- name as the peer name.

    Tests: the suite strips every inherited CODEMAN_* variable, so running it inside a Docker Compose deployment no longer writes into the deployment's real case root (#479).

    Thanks

    • @opticon454 for CLI management (#476), the last piece of the CLI registry, with every review item answered in one round, and for splitting the test isolation fix out into #479.
    • @shenlvkang-collab for the /resume title fix (#457) and the careful diagnosis behind it.
    • @julian3xl for the Session Manager row fix (#477), their first contribution.

Patch Changes

  • fix(sessions): stop pinning the w1-myapp placeholder as Claude's session title. Local Claude spawns passed the tab name as --name, which is also the /resume picker entry and the terminal title, and a pinned title stops Claude generating its own, so every conversation of a case showed up in /resume as the same w1-myapp and none got a generated title. Only a name the user chose is pinned now; placeholder and auto-named tabs let Claude title the conversation again. Renaming a Claude tab also reaches /resume: the new name is appended to the conversation's transcript as the custom-title row /rename writes (a tab that was spawned with --name keeps re-appending its own title until its next respawn, so the rename wins from then on). Orchestrators that rely on a fixed peer name should give workers a descriptive sessionName rather than a w<N>- one.

1.32.1 2026-09-23 release →

Patch Changes

  • Terminal copy: copying text out of a Claude Code or Codex pane no longer puts the pane's two-column transcript gutter on the clipboard, so pasted lines arrive flush instead of indented (#469). The width comes from the CLI registry (capabilities.transcriptGutter, 2 for claude and codex, measured on live panes) and is only a ceiling: a selection only ever shifts as a block, so its own indentation survives. Other CLIs and shells are untouched. It works in split panes and detached session windows too, and can be turned off per device in App Settings under Selection & clipboard.

  • Sessions: recovering a Claude session whose tmux pane had died relaunched claude --session-id <id>, which Claude refuses once that id has a transcript, so the pane died again straight away and the conversation was stranded. The relaunch now resumes the conversation (--resume <id> || --session-id <id>), including when tmux lost the whole session (#467).

  • Terminal: when a burst of output overflows the render queue and a frame has to be dropped, the repaint that repairs it is now retried until it actually happens, instead of being scheduled once and silently skipped when another load was in flight (#470).

  • Mobile: a long press on blank terminal space on Android Chrome no longer opens the keyboard and blanks the terminal (#471, fixes #360). The long-press guards are now armed before the press is checked for selectable text, so a press on empty space is swallowed the same way a press on a word already was.

  • Sessions: a tab whose agent has exited (the CLI quit, but tmux kept the pane) now says so with a muted dot and an exited (137) badge, instead of looking like an idle session (#466, part 1 of #446). The state is published as paneExit on the session and survives a restart. Nothing closes such sessions yet; that is part 2.

  • Docker: optional GitHub CLI and Azure CLI for private repositories (#472). Both are off by default. With CODEMAN_INSTALL_GH=1 / CODEMAN_INSTALL_AZ=1 as build args in docker-compose.override.yml, the server image gets gh and/or az (with the azure-devops extension) wired in as git credential helpers, so after one gh auth login or az login from a shell session, Add Case → Clone Repo can clone private GitHub and Azure DevOps repositories. CODEMAN_AGENT_IMAGE_INSTALL_GH / _AZ do the same for the Docker-case agent image, and only then are the sign-ins copied into new case containers. In multi-user mode a non-admin's clone runs with the credential helpers cleared. This changes server.Dockerfile, so Compose deployments need a Start-Codeman.sh rebuild rather than an in-app update.

  • Run menu: the Gemini, Antigravity and OMP run buttons now show their own colours on every skin; they rendered in Claude blue on all skins except OG (#463). The CLI registry's accent values were also corrected to the colours the UI really paints, and a test now guards the stylesheet trap that caused it.

  • Terminal: five ways the browser terminal could silently stop being correct are fixed (#431, #464). The browser terminal and the PTY can no longer disagree about their width, which is what produced doubled lines and half-overwritten text ("text gets muffled sometimes"): there is now one function that sizes the terminal, and every resize is answered with the geometry the PTY really holds. A replay clear goes through the terminal's own queue, so bytes written just before it no longer fuse into the next snapshot. A renderer that stops painting after an iOS PWA is backgrounded heals itself instead of needing a reload. Every terminal capture has a deadline that also covers the response body, and a capture that runs out of time during a tab switch falls back to the bounded tail instead of leaving a blank pane. Output lost to a half-open WebSocket is repainted on the next successful open. The service worker's precache list is now generated by the build and its cache is rotated per build, so old releases' assets no longer pile up.

  • Docker: new docker/Update-Codeman.sh for the major-update path the docs used to describe by hand (#465). It rebuilds the image with --no-cache before taking the stack down, clears the build-artefact volumes, refuses to run when another checkout's Compose project already owns the same name, and then hands over to Start-Codeman.sh.

  • Approvals: a session that is idle only because it is waiting on its own background work (Claude Code's 1 monitor footer chip, or a Codex background terminal) no longer raises the yellow NEEDS YOU alert or a push (#473, fixes #468). Its idle item is opened already acknowledged, and the tab, the home screens and the rail show a small watching badge next to the state instead. The item still exists in the Approvals Inbox, and the TUI's pending count now leaves acknowledged items out.

  • Maintainer fixes applied while landing this batch:

    • Terminal (#431): while another device holds the pane's width, a resize retry no longer re-fits xterm to the container and re-wraps the whole buffer every 30 s, and no longer clears scrollback for a redraw that never comes. The PTY's spawn geometry is now recorded at attach, so ptyGeometry never reports a size the PTY never held.
    • Terminal (#470): the TERMINAL DROP crash-trail line is logged once per recovery window instead of once per dropped frame (which wiped the rest of the trail within a second), and a refresh that died at its fetch deadline is no longer retried.
    • Sessions (#467): the resume pin also covers the branch where tmux lost the whole session, the conversation id Codeman reports follows what the relaunch actually resumed, and the test setup strips CLAUDE_CONFIG_DIR so the suite stays green for anyone running a separate Claude config dir.
    • Sessions (#466): detailed sidebar and rail rows show an exited pill instead of idle, the exit is announced to screen readers, and the user manual's tab-appearance table lists the new state.
    • Approvals (#473): a failed pane capture clears the watching badge rather than keeping a stale one (a failure now falls toward an alert, not toward silence), and the header bell's count leaves acknowledged items out, matching the TUI.
    • Run menu (#463): the Gemini and Antigravity run buttons no longer render two-tone on phones, Gemini's registry accent matches its tab badge, and a test now guards the stylesheet trap for every run mode.
    • Docker (#465): Update-Codeman.sh removes exactly the two build-artefact volumes it names instead of every named volume in the project, reports a failing docker compose instead of exiting silently, and its docs and comments were corrected. (#472): the multi-user notes say that a non-admin's seeded Docker case also receives the gh/az sign-in when those switches are on.

    Thanks

    • @irisitymichaelgrundberg for four PRs in this release: the watching badge that stops background work from raising false alerts (#473, from their own report #468), the exited-agent badge (#466) and the dead-pane resume fix (#467), both from their report #446, and the transcript-gutter strip for copied text (#469), a follow-up to their #451.
    • @rounakdatta for the terminal resilience work (#431) and the dropped-frame recovery (#470), both from their report #464, and for answering four rounds of review in full.
    • @opticon454 for private-repository support in the Docker images (#472), the Update-Codeman.sh script (#465) and the run-button colour fix (#463).
    • @DodgyBadger for the Android long-press fix (#471), from their own report #360.

1.32.0 2026-09-21 release →

Minor Changes

  • feat(custom-model): the model picker puts the ready model first

    When a custom endpoint has more than one model, the Run menu's picker now promotes one row to the top instead of showing raw discovery order: the model llama-swap reports loaded and ready right now (tagged "Currently loaded", the one a launch attaches to with zero wait), else the model you last launched on that harness and endpoint (tagged "Last used", remembered per device). The endpoint's default keeps its own pill, nothing is ever auto-chosen, and a plain OpenAI-compatible server or an endpoint that does not answer within a second simply keeps the old order. The probe is bounded on the client too, so a GPU box that is off no longer holds the picker closed for five seconds.

  • feat(split-pane): view two live sessions side by side

    A new Split button in the header (opt-in in App Settings, off by default, desktop only at 1180px and wider) opens a picker and shows a second live session beside the active one: its own terminal, its own WebSocket, and a divider you can drag. When either session ends the view collapses back to one pane, with Pane B promoted to the primary when it is Pane A that ended. Nothing is persisted on purpose in this first cut, so a page reload always returns to a single pane. Pane B is deliberately plainer than the primary pane (no local-echo overlay, CJK input, touch handling or keyboard accessory bar); the design and the v2 boundaries are in discussion #452.

  • Installer v2. curl -fsSL https://getcodeman.com/install | bash now looks at the machine first, asks at most three questions up front (how the dashboard is reached, optionally what to call the machine on your tailnet, whether to run Codeman as a background service), does the install unattended behind progress spinners with the output in ~/.codeman/install.log, and ends on the URL with a QR code to scan. One consent covers every missing package and sudo asks for your password once. Flags pipe through bash -s -- (--tailscale | --lan | --local, --name <n> | --no-rename, --service | --run | --no-start, --yes, --password, --port), install.sh status prints the URL and the QR code again, and the cloudflared question moved out of the main flow into install.sh cloudflared. On the Tailscale route, a :443 that already belongs to another app gets Codeman under https://<node>/codeman (or on a second port) instead of a dead end, the node can be renamed opt-in (--name, install.sh name, undone by uninstall), and the HTTPS-certificates toggle is polled with the admin page opened for you. Also fixed on the way: the installer's own npm install no longer lets the postinstall start a stray server on port 3000 (the service crash-looped on EADDRINUSE while the done screen said "running"), the LAN address comes from the default route rather than the first interface, a hand-written LaunchDaemon on a headless Mac is left alone, a flag re-run keeps an existing dashboard password, and the done screen's start command carries the sub-path and port it was installed with.

  • feat(mobile): a Compose key for writing prompts on a phone

    The agent keyboard bars on phones replace their Paste key with Compose: a real multiline editor with autocorrect and spellcheck, per-session drafts kept in memory only, image attach that never writes into the terminal early, and a Send that delivers the text as one paste followed by Enter, so a long prompt no longer has to be typed blind into the terminal composer. Anything you had already typed into the terminal is picked up into the editor. Shell sessions keep the direct Paste key. This is the manual first slice from #359; the auto-open setting and terminal tap routing are a separate follow-up.

Patch Changes

  • refactor(run-menu): one table-driven launcher for every external CLI

    The eight near-identical per-CLI launch functions in the Run menu collapsed into one launcher driven by a table that a CI test keeps in step with the CLI registry, and a second no-id-branching guard now covers the frontend the way the backend guard covers the server. No behaviour change: the refactor was verified byte-identical across 288 launch permutations against the previous code.

  • Maintainer fixes applied while landing the above. The model picker's promoted row keeps its Default pill (the promotion tag and the default marker are two pills now, and they render as pills in the picker rather than as plain text). The phone composer keeps its bottom gutter on folding devices (the generic fold rule used to erase it), a whitespace-only draft is no longer sent, and its dialog is translated on a zh-CN UI. A split that collapses mid-drag no longer leaves the page stuck in resize-cursor mode, Pane B refuses a session that has no live process, and a burst of refresh frames replays once instead of twice. The </head> script injections on the page render use replacer functions, so a CLI label containing $' can no longer splice the document into the inline script, and the frontend no-id-branching guard now catches comparisons on any variable name.

  • Thanks

    • @timkjr for split-pane sessions (#453): five review rounds turned around in two days, and the pointer-capture edge case measured in a real browser rather than reasoned about.
    • @DodgyBadger for the mobile prompt composer (#444), a first contribution that took the scope back down to one slice when asked, and that verified the delivery path against a live tmux pane and a live Claude Code composer instead of trusting the diff.
    • @opticon454 for putting the ready model first in the picker (#459) and for collapsing the eight Run-menu launch functions into one (#458), proven byte-identical across 288 launch permutations instead of argued.

1.31.0 2026-09-19 release →

Minor Changes

  • feat(remote): wake a sleeping remote host from Codeman

    A remote SSH case pointing at a machine that suspends used to fail the same way every time: the session was there, the host was not, and typing into it went nowhere. A host can now carry a wake target, either a MAC address for Wake-on-LAN (Codeman builds the magic packet itself, so nothing reaches a shell) or a wake command of your own, and Codeman uses it when you ask for the host: when you type into a sleeping session, when you press the wake button on the banner, or when you start or attach a session on that host. Input you type while it wakes is buffered and flushed once it is back, up to 4 KB, and a chunk over that is refused outright rather than delivered as a fragment.

    Waking only ever happens because you asked. No watcher, dropped-session handler or boot-recovery path can reach it, since a machine woken by a reconnect watcher would come back seconds after every suspend.

  • feat(custom-model): pick a custom endpoint straight from the Run menu

    #393 landed the backend for custom model endpoints and left it reachable only over the HTTP API. This is the rest of it. Turn on Custom model endpoints in App Settings, save an endpoint, and the Run dropdown grows a Custom Endpoints section built live off the CLI registry, one entry per harness that can actually redirect plus each endpoint you saved. Pick one and it launches that harness pointed at your server, asking which model first when the endpoint has more than one. Endpoints re-discover themselves every five minutes, and one unreachable endpoint never blocks the others. App Settings gains full add, edit and delete for endpoints.

    Seven of the harnesses (opencode, Codex, Gemini, Pi, Grok, DeepSeek and OMP) now launch directly onto the endpoint with no restart at all, where before you watched a native boot followed immediately by a second one. Claude still launches and then restarts in place, which its own resume makes far less jarring.

    Most of this release's work went into things that only show up against a real server, and each was found that way rather than in tests: a freshly launched CLI reporting itself busy for its own startup and getting refused; Claude Code assuming a large context window for a model it does not recognise and silently overflowing a small one; a model whose real context is below what Claude Code's own system prompt costs, which no setting can fix and which now warns before launching into a certain failure; and the big one, llama.cpp running exactly one model at a time, so applying a selection can unload the model another session is using. That last case now asks first, tells you which session it affects, and keeps a "loading model" notice on screen for the whole swap window, so a prompt sent mid-swap reads as loading rather than as an answer from whatever was loaded a moment ago. A background sweep also catches the reverse: your session's model being evicted later by somebody else's ordinary use.

    Two things worth knowing if you drive this over the HTTP API or run multi-user. The two questions an apply can ask (the model's context window is too small, and loading it will unload the model another session is using) are now answered by separate confirmedContext and confirmedSwap fields rather than one confirmed. They shared a flag until now, and since the context check runs first, confirming that one silently agreed to evict another session's model as well. The old confirmed still means both. And CLAUDE_CONFIG_DIR is now admin-only in multi-user mode: it joined claude's privileged env keys, so a non-granted owner can no longer set it through envOverrides, and an already-persisted one is dropped on reboot-restore, which returns that session to the default Claude account rather than the per-client one it was pointed at. Single-user installs are unaffected.

    Remote SSH and Docker sessions are refused for now, since their restart reattaches a durable tmux rather than relaunching the agent.

Patch Changes

  • fix(terminal): keep the output a pane capture could not contain. Opening a session, a backpressure refresh, a clear-terminal reload and a full-history re-pull all load the screen from a tmux pane capture, and anything the CLI printed between that capture and the end of the load used to be dropped, so its next partial redraw landed on a frame the terminal had never seen: missing or garbled output right after a tab switch or a refresh, plainest in a shell session. Each load now replays exactly the output that arrived after the capture, through one shared rule for all four paths, and a refresh that restores your scroll position no longer snaps back to the bottom afterwards.

  • fix(terminal): replay a pane capture at the geometry it was taken at

    Opening a session could draw a frame built for a pane bigger than your terminal. A taller pane wrote its overflow rows onto the last line and lost the rows underneath (against a 50-row pane, a 30-row terminal rendered 28 of a 45-line command and drew the survivors twice), and a wider one wrapped every row and scrolled the whole frame up by one. The terminal response now reports the geometry the capture was really taken at, so the browser can see the mismatch and replay once at the size that stuck. A pane that cannot be sized to fit is diagnosed once per session instead of on every tab switch.

  • Thanks

    • @irisitymichaelgrundberg for three terminal fixes in one release: keeping the output a pane capture could not contain (#436), replaying a capture at the geometry it was taken at (#435, five rounds and a Playwright suite that fails against the merge base), and trimming the padding out of a copied selection (#451), where the scan-instead-of-regex call avoided a 2.9s freeze nobody would have traced back to a copy.
    • @timkjr for a first contribution that found a real silent failure: the Instance count stepper next to the Run button had only ever applied to Claude, so on the other eight run modes it launched one session and said nothing (#454).
    • @Randalix for Wake-on-LAN on remote hosts (#439), built and live-tested against a real sleeping machine, and for reading the whole diff again between rounds rather than only the parts that were asked about.
    • @opticon454 for turning #393's backend-only custom model endpoints into the whole feature (#430), and for validating it against a real llama-swap box rather than against the tests: the /props versus /running context discrepancy and the DeepSeek /v1 root cause were both tracked down to the SDK source instead of guessed at.
  • fix(run): make the Instance count stepper work for every non-Claude mode

    The Instance count stepper next to the Run button only ever applied to Claude. Setting it to 3 and launching OpenCode, Codex, Gemini, Antigravity, Pi, OMP, Grok or DeepSeek started exactly one session, with no error and no hint that the control had done nothing. All eight now launch the count you asked for, and the opening banner says how many are starting. The one exception is a launch started from the Custom Endpoints section of the Run menu, which always starts a single session.

  • fix(input): make sure a prompt sent through the API actually leaves the composer. Claude Code 2.1.277 started ignoring Enter for the first 30 to 50 seconds after the composer paints while still accepting the typed text, so a prompt sent right after a session came up sat unsent in the pane and every waiter (send-and-wait, the agent skill, cron, the maintainer bot) burned its whole timeout on a turn that never started. The server now reads the pane after every programmatic write that carried Enter and presses Enter again, on a 2 to 60 second schedule, only while the composer verifiably still holds the text it sent; an empty composer, other text, or a pane with no composer at all ends it. The agent skill's sendwait gets the same loop for servers that predate this, and its preamble version moves to 1.30.1 so an already-seeded agent picks up the fresh copy.

  • fix(terminal): trim the padding out of a copied selection

    Copying out of a pane put a wall of spaces on the clipboard. xterm hands back whole screen rows and trims only the cells that were never written to, so the real spaces a full-screen program paints across the unused part of a row count as content: measured against Claude Code in a 282-column pane, single lines arrived carrying 138 trailing spaces. Pasting that into a chat client or an editor meant deleting the whitespace by hand, while Windows Terminal, iTerm2 and GNOME Terminal all trim it for you. A copy now drops the trailing run from every line, on all four paths (the Ctrl+C chord, right-click, the phone selection button and Auto Copy), while leading indentation is left exactly as it is. An Alt+drag rectangular selection is copied verbatim, because its columns lining up is the point of that gesture. A selection holding nothing but padding is refused rather than copied as bare line breaks.

1.30.0 2026-09-18 release →

Minor Changes

  • Offer to rebuild the sessions a host reboot destroyed. A reboot takes the tmux server down with it, so every pane dies and the board comes up empty. Codeman now works out what was running, and the board offers to restore it behind a click. The conversations come back; the terminal scrollback does not, and the banner says so.

Patch Changes

  • Stop a phone keyboard losing the last character of every message it sends. Android soft keyboards commit the last typed character and send the Enter key in one InputConnection transaction, so the input event and the Enter keydown are both processed before any zero-delay timer runs. The orphaned-input recovery from #388 only resolved its candidate on such a timer, and lost it both ways: xterm emits \r synchronously from the Enter keydown, so the local-echo composer submitted the prompt before the recovered character existed, and that \r bumped the "did xterm speak for this keystroke" counter, so the candidate then stood itself down and dropped the character outright. Pending candidates are now drained synchronously at the next keydown, from xterm's custom key handler, which runs before xterm processes that key, so the counter still holds the value it had while the candidate's own keystroke was current, and the recovered byte reaches the composer ahead of the Enter. Typing on a physical keyboard is unaffected: there, the timer has already resolved the candidate before the next key arrives.

  • The installer's hint for a launcher-only CLI (DeepSeek today) now says why it is a docs link rather than a command you can run, and points at the thing that resolves it: the package installs a launcher that still needs a terminal profile, and Codeman's Run menu can add one in a click. Driven by a generated CLI_LAUNCHER_ONLY flag rather than an id check, so it covers any future entry of that shape. Also removes three dead lookup helpers and two never-read generated arrays from install.sh, skips a disabled entry's probe instead of filtering it afterwards, and corrects a comment that claimed the non-interactive default is always Claude Code (on a wget-only host its curl one-liner is filtered out first).

  • Maintainer fixes applied while landing the above. A session restored after a reboot keeps the name you gave it (the rebuild dropped the field that records who named a session, so a hand-renamed session came back looking auto-named and the next prompt overwrote it), and no longer types continue into itself on its own: a pending auto-resume stamp from before the reboot is dropped rather than re-armed, since the pane is new and one click could otherwise arm several unattended prompts at once. Auto-resume itself stays on and re-arms on the next real usage-limit message. The restore offer is also hidden in a detached single-session window, which has no tab strip to put restored sessions in, and a conversation that goes live while an earlier session in the same batch is starting is no longer restored a second time.

  • Thanks

    • @irisitymichaelgrundberg for the reboot-restore banner (#442), and for the three real reboots behind it rather than a mocked one.
    • @shenlvkang-collab for tracking down why Android keyboards lost the last character of every message (#441), including the half where the character was not late but gone.
    • @opticon454 for going back and closing out the loose ends left as "worth knowing rather than fixing" after #380 (#429).
  • Keep the terminal anchored where you are reading while an agent streams (#358). Scrolling up during a Codex response could still be dragged back to the live bottom by the next redraw: the flush captured the viewport before writing and restored it immediately after, but xterm parses asynchronously, so at that moment the buffer had not moved yet, the restore compared the anchor against itself and did nothing, and the redraw landed a tick later with nothing left to pull the view back. The restore now runs inside xterm's own write callback, which is the first point at which the redraw's effect exists, and it holds across consecutive and chunked redraws. It is dropped if you switch sessions or a history replay starts before the write parses, since the anchor indexes the buffer it was captured from.

1.29.1 2026-09-15 release →

Patch Changes

  • Auto-name sessions from the first prompt (#376, opt-in). With the new synced Auto-name Sessions setting on (App Settings → Appearance → Tabs, default off), a tab that still carries its generated name takes a title from the first real prompt you submit, keeping the case prefix: w3-myapp becomes w3-myapp: fix the login redirect. The strip shows the title with the prefix in the tooltip, and the next session in that case still counts up. It happens once per session, only for prompts you type or send through the input API (never a Ralph, respawn, cron or approval answer), never for shells, and a name you set yourself is never touched. Slash commands such as /clear do not become titles. The title is derived locally from the prompt's first sentence; no text leaves the machine. nameSource (placeholder / auto / manual) is a new additive field on session state.

    Landed with the fixes the review of #376 asked for: first prompt only (not every prompt), a user-input gate so Ralph, respawn, cron and approval writes cannot name a tab, the prefix form so the case identity and w<n> counter survive, and a keystroke tracker that handles a bare Esc, bracketed pastes, wheel reports, Tab and history recall instead of mis-titling the tab.

    Thanks

    • @shenlvkang-collab for #376, the auto-naming idea and the ownership plumbing (nameSource, the listener wiring, the restore path) it shipped with.

1.29.0 2026-09-15 release →

Minor Changes

  • Custom model endpoints, HTTP API first (#393). Any run mode that has a mechanism for it can be pointed at a custom OpenAI-compatible endpoint (a local llama.cpp, llama-swap, Ollama or vLLM, or a cloud gateway) instead of its native backend, per session. Endpoints are stored in ~/.codeman/custom-model-hosts.json (GET/POST/PUT/DELETE /api/model-endpoints, admin-only in multi-user mode), their model lists are discovered from the endpoint's own /v1/models, and POST /api/sessions/:id/custom-model applies one to a session by restarting its CLI in place. The mechanism is per-CLI registry data (capabilities.customModelInjection): env vars for Claude, Gemini, Grok and DeepSeek, OPENCODE_CONFIG_CONTENT for opencode, an isolated config dir for Codex, Pi and OMP, unsupported for Antigravity. Verified live against a llama-swap server for claude, opencode, pi, grok and omp; gemini and deepseek reach the server and fail for reasons not yet understood, and codex only speaks the Responses API, so a plain chat-completions server cannot serve it. Those three are documented as gaps rather than shipped as working. The toolbar picker is a follow-up; until it lands the feature is HTTP-API only (docs/custom-model-endpoints.md), and the customModelEndpointsEnabled setting is declared but read by nothing yet. Merged with maintainer follow-ups: clearing a selection now actually clears it (the injected vars are delivered by tmux setenv, which respawn-pane inherits, so the relaunched CLI came back still pointed at the endpoint; retired keys are now setenv -u'd before the respawn), applying a model to a local claude session no longer kills the pane (the relaunch pins --resume <id> with the --session-id fallback, since Claude Code refuses a session id that already has a transcript), pi, omp and grok now select the generated model through a registry-declared launchModel (custom/<id>, -m codeman-custom) instead of writing a config the CLI then ignored, remote and Docker sessions are refused with a clear 400 until those paths are plumbed, the selection survives a Codeman restart, discovery goes through the egress-guarded webviewFetch(), key-bearing files are written 0600 and the per-session config dir is removed with the session, and the design plan moved from the repo root to docs/custom-model-endpoints-plan.md. Along the way the multi-user clamp learned about GOOGLE_GEMINI_BASE_URL, GROK_BASE_URL, CODEX_HOME, PI_CONFIG_DIR and OPENCODE_CONFIG_CONTENT, which were already reachable through envOverrides and now count as privileged keys.

    Single-page apps work as web tabs, and a frame that reloads comes back (#402). A history-routed dashboard (React Router, Vue Router, a Vite dev server) read /webview/<cap>/ as its location.pathname and rendered its own "page not found" the moment its script ran. The proxy's runtime shim now masks the prefix off the document URL before any page script runs, while every URL the page emits still goes through the rewrite layers (now including Worker, SharedWorker, sendBeacon and window.open). A navigation the page starts itself afterwards (a dev server's full reload, a root-absolute location.href) used to land on Codeman's root with no capability; it is now recognised by shape, answered with a static recovery page that posts the lost path to the owning tab, and the frame is remounted inside the prefix at that path, bounded to five recoveries a minute per frame. Merged with maintainer follow-ups: the recovery path is sanitised properly (a leading backslash, or a tab/newline the URL parser deletes before parsing, resolved /\evil.com to a foreign origin in a direct-mode tab); a reload on the dashboard's landing page is recovered too, on password-protected and passwordless installs alike (it used to render Codeman's own shell inside the web tab); and the recovery page is written down as the third unauthenticated 200 in the security table and docs/security-architecture.md, with the route-enumeration property it implies stated rather than left to be discovered.

    Shift arrows for Codex on the phone keyboard bar (#408). Two keys, ⇧← and ⇧→, send the Shift-modified arrows Codex binds to editing the last queued message and walking the prompt stack (verified against Codex 0.154.0's /keymap). Merged with a maintainer follow-up: the keys are shown only on Codex sessions (a codex-enabled class on the bar, the same shape as the Read My Mind key), because tapping one in any other session did nothing except hand that session to plain PTY echo for the rest of the prompt.

    Remote (SSH) cases can finally show you their files (#421, fixes #415). File previews, downloads, text reads and the out-of-workspace attachment path resolved every path against the Codeman host's own filesystem, so in a remote case every click ended in "File not found" while the file plainly existed on the other machine. A single new ssh read layer (src/remote-files.ts, built on the same buildSshConnectionArgs() the launch uses) probes realpath and stat for the file and the workspace root in one round trip, then streams the body with cat (or a tail/head slice for a Range), so the 200/206/416 contract holds and nothing is buffered on the server. Symlinks are resolved on the host that can resolve them, containment is checked against the resolved remote root, the size cap applies to the remote size before a byte is requested, an unreachable host is a 502 rather than a 404, and there is deliberately no local fallback: a same-named file on the Codeman host is never served under a remote name. Writes, Office previews and generated thumbnails answer 400 for a remote case instead of a misleading 404. Merged with maintainer follow-ups: the readlink -f fallback resolved only the directory chain, so on a host without it a symlink's final component was returned unresolved and ws/notes.txt -> ~/.ssh/id_rsa passed containment while cat served the key; it now follows the last component with plain readlink for a bounded number of hops and fails closed (404) on a loop or the cap; PUT /api/sessions/:id/file-content answers 400 for a remote case as the PR already claimed (it still validated against the local filesystem, so a same-named local directory took the write); ssh children are bounded by a small semaphore (CODEMAN_MAX_REMOTE_FILE_SSH, default 4) covering the attachment-history fan-out, which now probes the whole history in one batched call, and the fire-and-forget magic-link registrations an injected agent could use to fork hundreds of ssh processes; probe records are NUL-delimited and index-keyed so a newline in a filename cannot shift one path's result onto the next; and a 502 body never carries the ssh command line.

    Docker Compose: bind-mount ownership, override files, a codeman runtime account, and no more stale volumes (#377). A missing bind source (first run, cleared appdata, restored backup) is created root-owned by the daemon, and the unprivileged server crash-looped on EACCES when Compose was run directly; the image now starts through an entrypoint that corrects a root-owned bind mount and drops to PUID:PGID with setpriv, and the compose file adds back only the capabilities that needs. Start-Codeman.sh honours docker-compose.override.yml (naming a Compose file with -f silently disables Compose's own discovery of it), pre-creates the cases directory like it already did for appdata, and detects when the checkout's HEAD or lockfile moved under the codeman-node-modules/codeman-dist volumes and refreshes them, which used to leave a docker compose build serving stale compiled routes. The default runtime account is named codeman (it was opencode), the four global agent CLIs live in their own /opt/codeman-cli prefix so the runtime account can update them in place without owning /usr/local/bin, and CODEMAN_ALLOWED_HOSTS is documented and forwarded. Merged with maintainer follow-ups: cap_add gains KILL (with init: true tini runs as root while the server runs as PUID, and without CAP_KILL its SIGTERM forward failed and the server was SIGKILLed on every compose down/restart); the CLI prefix is appended to PATH rather than prepended and the root entrypoint pins its own PATH, since a PUID-writable directory ahead of /usr/bin let the runtime account plant a setpriv that ran as root on the next start; the entrypoint decides with a real writability probe as the runtime identity instead of an owner comparison, so ACLs, group-writable trees and NFS/CIFS mounts work and only a genuinely unwritable directory is refused, by name; the cases directory is created with the runtime owner after PUID/PGID are known; the build-source marker is written only when a refresh actually happened, an empty Compose project name falls back to down --volumes, the build runs before the down so the stack is offline only for the recreate, docker-compose.override.* stays out of the image, and test/docker-entrypoint.test.ts pins cap_add against what the entrypoint needs. ⚠️ Compose users: run Start-Codeman.sh once for this release rather than a plain docker compose up, so the rebuilt image, the refreshed volumes and the new entrypoint arrive together.

    Selected text is visible again on the light skins (#423, part of #360). Every skin palette named its selection layer selection, the key xterm renamed to selectionBackground in v5, so all seven skins had been painting xterm's default white at 30% instead of the colour next to it in the palette. Dark skins hid it; on the four light skins a selection was white on near-white. The key is renamed and test/skin-themes.test.ts pins it. CI additionally exercises install.sh's dsh identity probe with timeout missing under bash 3.2 (#422), the guard #382's fix shipped without.

    Eight fixes salvaged from #375 (dignfei; landed with the author's commits preserved, the rest of that PR is covered below). Shift+drag starts a text selection in a pane whose mouse reports go to the CLI, and right-click copies the selection. Ctrl- and Alt-modified navigation keys typed through the CJK composer reach the CLI as the modified sequences instead of plain arrows. A browser whose reliable-input sequence counter fell behind the server's watermark (a restored tab, a cleared localStorage) now recovers: the duplicate ACK carries dup: true plus the watermark, the client lifts its counter and re-sends, so a session that had silently stopped accepting typed prompts accepts them again. An SSE reconnect that lands on the session you are already looking at keeps its terminal buffer and resyncs instead of resetting the whole terminal. The hidden offline overlay and the file-preview overlay only apply backdrop-filter while shown, which removes a stale compositing layer that swallowed clicks. One adopted Docker container can back several cases at different in-container directories, and the adopt panel gains a "copy an existing case" picker. Of the PR's 27 commits, 14 had already shipped through #357, the selection theme key rename shipped as #423, and foreign tmux adoption plus SSH password auth stay with the author.

    Thanks

    • @opticon454 for custom model endpoints (#393), including the part nobody enjoys: working out each CLI's real endpoint mechanism against real binaries and writing down which ones do not work yet instead of claiming they do; and for the Docker Compose deployment fixes (#377), rebased and reworked through three review rounds.
    • @shenlvkang-collab for making single-page apps route inside web tabs and recovering a frame that reloads (#402), the best-engineered PR of this batch, and for the Codex Shift arrows on the phone keyboard bar (#408), verified against Codex's own keymap.
    • @dignfei for the eight fixes salvaged from #375 (terminal selection and copy, CJK navigation keys, input recovery, SSE reconnect, overlay compositing, multi-case adopted containers), landed under their own name.
    • @Randalix for reporting #415 and then fixing it themselves with the whole missing ssh read side for remote cases (#421), with a real-shell test for the probe script and a full route suite.

Patch Changes

  • fix(webview): let a proxied single-page app route on its own path, and recover a frame that reloads

    A dashboard served through a web tab saw /webview/<cap>/ as its location.pathname, and no app has a route for that: a React Router, Vue Router or Vite dev-server page painted its HTML and CSS and then replaced them with its own "page not found" the moment its script ran. The proxy's runtime shim now rewrites the history entry to the path the page would see on its own origin before any page script runs, while every URL the page emits still goes through the existing rewrite layers (plus Worker, sendBeacon and window.open, which the masked Referer can no longer rescue). A navigation the page starts itself afterwards — a dev server's full-reload HMR, a root-absolute location.href — lands on Codeman's root with no capability; it is recognised by shape (an iframe navigation asking for HTML for a path Codeman does not serve), answered with a static page that tells the owning tab which path was lost, and the tab remounts the frame inside the prefix at that path. That answer is served before the credential checks, so it never counts as a failed login.

  • File previews, downloads and text reads now work in a remote (SSH) case.

    A remote case's working directory is an absolute path on the remote host, but the file routes resolved it with local fs — so a clicked path (or the File Viewer) always failed as "File not found" even though the file existed and the session was clearly working in that directory. GET /api/sessions/:id/file-raw, file-content, file-preview and file-thumbnail now resolve and read through the same buildSshConnectionArgs() connection the launch uses (src/remote-files.ts, one realpath+stat probe per request returning both the file and the workspace root).

    Clicked paths that point OUTSIDE the case directory (a remote /tmp scratchpad capture, a screenshot elsewhere in the remote home) go through the attachment routes, which had the same local-fs assumption: registration, the by-id raw stream, the metadata poll and the attachment history list now resolve over ssh as well, so the click-path works whether the file sits inside or outside the case. Which host a record is read from follows the SESSION, never the path string — the same absolute path means a different file on each host, and a remote session never falls back to a local file.

    The guards are unchanged in strength: the workspace boundary is still enforced (now resolved on the host that can actually resolve it), the sensitive-path blocklist and the size cap (CODEMAN_MAX_DOWNLOAD_BYTES) still apply before any bytes are read, and Range requests keep working, so remote <video>/<audio> seeking behaves like a local file. An unreachable host is reported as 502 with the remote reason instead of a misleading 404. Nothing is ever copied to the Codeman host.

    Still not available for remote cases, and now said explicitly instead of 404-ing: editing a file (edit=1 / PUT answer 400, the viewer hides its Edit affordance), office-document previews and generated thumbnails (both need the bytes on the server's disk), the file tree / path picker, and tail-file. Docker cases are unaffected (their workspace is bind-mounted at the same absolute path).

  • Add Shift+Left and Shift+Right buttons to the default and extended mobile agent keyboard bars, shown only on Codex sessions, enabling Codex queued-message editing and prompt-stack navigation. Flush locally buffered drafts before navigation and keep terminal focus after taps.

  • Fix an invisible terminal text selection on the light skins (#360). Every xterm palette declared its selection colour under the key selection, which xterm.js renamed to selectionBackground in v5. An ITheme is a plain object, so the unknown key was dropped without an error and every skin fell back to xterm's own default of rgba(255,255,255,0.3): unnoticeable on the dark skins, which wanted roughly that anyway, and effectively invisible on Paper Gray, Solarized Light, Catppuccin Latte and Rosé Pine Dawn, where white at 30% over a near-white background moves a channel by about 3/255. Selecting text on those skins now highlights it, with desktop drag-select and the mobile long-press both fixed by the same rename.

1.28.2 2026-09-14 release →

Patch Changes

  • Terminal font weight (#417, from discussion #403). App Settings → Terminal → Font gains two per-device rows, Normal font weight and Bold font weight, each a select from Default plus 100 to 900. Claude Code marks bold with a bare ESC[1m and no colour change, so with a family that ships only a regular and a bold face a bold heading reads as body text; setting normal to 300 turns that one small step into an obvious one. Both slots resolve against their own xterm default (an unset bold never inherits normal), apply live to the terminal, both echo overlays and open Agent Teams panes, and the bundled JetBrains Mono @font-face is declared over the font's real 100 to 800 axis instead of 400 to 700, without which every weight below 400 rendered identically to 400 on a stock install.

    Phones up to 599px get the phone layout (#390, fixes #389). The phone tier's cutoff moves from 430px to 600px in the JS classifier, mobile.css and every test and doc that pins it, so the iPhone Plus and Pro Max sizes, the Pixel Pro and the Z Fold cover display (430 to 460px) get the phone header, the Enter key and the accessory bar instead of the tablet layout. Verified on a real iPhone 17 Pro Max; a Safari page zoom below 100% widens the reported viewport, which is why the cutoff is 600 rather than 480.

    The plan-usage statusline exporter no longer touches your settings files (#361, diagnosed in #405). Codeman used to write its exporter into a workspace's .claude/settings.local.json, which Claude Code ranks above ~/.claude/settings.json, so it replaced your own statusline for ANY claude run in that directory, including outside Codeman, and rendered the bare word codeman when run by hand. The exporter is now passed to claude as an ephemeral --settings flag when Codeman spawns it and is never written to disk; your own statusline (project-local, project, then ~/.claude/settings.json) is wrapped and printed through inside Codeman sessions, and a hand-run claude sees nothing of Codeman. Workspaces an older Codeman wrote to self-heal the first time a session starts there. Telemetry collection follows the Plan Usage chip setting, read fresh at every Claude session create and respawn; an absent setting means on, and a device writes the switch only when it flips the chip, so a phone (chip off by default) saving its font size can no longer switch collection off for the desktop. The exporter prints nothing when it cannot reach Codeman, the telemetry route answers an unknown session with an empty body, and the footer is empty rather than a brand word. Known limit: sessions inside a Docker case do not feed the chip yet (the flag rides local spawns only; the chip is account-wide, so any local Claude session covers it).

    install.sh and the Docker agent image read the CLI catalogue (#380). Adding a CLI to src/config/cli-registry/stock.ts and running npm run generate:cli-catalog wires it into the installer's detection, install menu and closing reminder, and into the agent image's npm layer; each of those was a separate hand-kept list before, and OMP had been missing from the installer's detection entirely. The install menu offers every enabled CLI that can drive a pane (eight, rather than the fixed two), DeepSeek is deliberately withheld because npm install -g [@deepseek-ai](https://github.com/deepseek-ai)/dsh installs only a launcher with no runnable profile, a wget-only host keeps the entries that never needed curl, and the agent image respects enabled. The script stays bash 3.2 compatible and CI now executes it inside a real bash:3.2 container. Choosing "s" (Skip) in the menu continues to the clone and build instead of aborting.

    iPhone Duo support (#407). A visual-viewport resize that changes the WIDTH is the device changing shape and is never read as the virtual keyboard: closing an iPhone Duo (626 to 466pt wide) or rotating any phone used to latch the keyboard layout with no keyboard on screen, sticky until the device was opened again. The seven centred overlays keep their dialogs out of the hinge through the CSS Viewport Segments variables (inert on devices that do not fold), the phone path picker and preview stay flush under 600px, and a shape change with the keyboard up baselines to the layout viewport so the settle event after a rotation no longer closes the keyboard layout. Two Duo device profiles join the test matrix.

    Codeman is its own Claude Code plugin marketplace. /plugin marketplace add Ark0N/Codeman followed by /plugin install codeman@codeman installs the codeman agent skill as a plugin, from plugins/codeman/ (a mirror of skills/codeman/ kept byte-identical by a test), which is a small separate directory on purpose: a plugin root carrying a package.json gets an npm install on every installer's machine. A Claude Code holding both the plugin and a user-level or per-case copy lists the skill twice; pick one route.

    Housekeeping: the maintainer's Telegram PR bot moved out of this repository (it is a client of the HTTP API like any other), the COM flow gained a Discussions announcement step, and the changelog's Thanks sections were backfilled for 1.22.0 to 1.28.1.

    Thanks

1.28.1 2026-09-14 release →

Patch Changes

  • fix(tabs): let a wrapped desktop tab strip grow the header instead of clipping itself

    The wrapped tab strip carried fixed height caps (120px for the manual two-row layout, 96px for measured auto-wrap) that were row counts in disguise. A third row of tabs was clipped into a roughly 4px scroller, so the tab being looked for sat off-screen inside a container nothing invites you to scroll, while the header had the whole page below it to grow into. The header is min-height plus flex-shrink: 0, and terminal-ui's ResizeObserver refits the terminal on its own, so growing it costs nothing.

    Both wrapped layouts now share one rule capped at var(--tab-strip-max-height, 40vh). That cap is a safety net for an absurd session count rather than a row limit: past it the scroller comes back, which still beats a header that swallows the terminal. Nothing sets --tab-strip-max-height yet, so today it is the 40vh fallback plus a hook for a future control.

    Desktop only in effect. tabs-auto-wrap is applied by updateTabOverflowMode(), which returns early for anything that is not a desktop viewport, and below 1024px mobile.css pins the header to max-height: 48px so it cannot grow at all. The two rules are comma-grouped rather than wrapped in :is(), so each arm keeps its own (0,2,0) specificity and mobile.css's matching overrides still win on source order.

    Thanks

    1.28.1 is a same-day follow-on to 1.28.0, so the thanks for this pair belong here too:

    • @shenlvkang-collab for the path picker's typed-path jump and name/date sort (#399), and for the care in the edges: the retry is bounded to one parent level, a typo keeps the listing you had instead of resetting to the root, and a full file path lands in its folder with the entry already selected.
    • @irisitymichaelgrundberg for Claude truecolor in panes (#409), and above all for flagging the one reading they could not prove: that suppressing truecolor may have made Claude's block collapse into the background rather than fixing anything. That paragraph is why this got measured instead of taken on trust, and the measurement changed the changelog.
    • @timkjr for trapping Ctrl+Z in agent sessions (#404), for finding that Caps Lock flips ev.key to 'Z' without setting shiftKey so a plain === 'z' check misses exactly the keystroke the guard exists for, and for stating up front that an agent CLI already holds its tty with ISIG off rather than overselling the fix.

1.28.0 2026-09-14 release →

Minor Changes

  • feat(files): let the path picker jump to a typed path and sort by name or date

    The picker's current-folder line was read-only, so reaching a deep folder meant tapping through every level, and its listing was fixed to name order, so the file an agent had just written was somewhere in a 500-entry list. The current folder is now an editable field (Enter or Go jumps there, a full file path lands in its folder with the file selected, and a typo keeps the listing you had instead of resetting to the root), the listing can be sorted by name or modified time in either direction with folders always first (the choice is remembered per device), and each entry shows a compact modified time. GET /api/filesystem/browse entries carry mtimeMs to make that possible, with one stat per entry.

Patch Changes

  • fix(terminal): swallow Ctrl+Z in agent sessions so it cannot suspend a running CLI

    Ctrl+Z raises SIGTSTP on the pane's tty. In a shell session that is ordinary job control and is left alone, but in an agent session suspending the CLI stops an unattended loop dead with no visible output, the same failure shape as an XOFF freeze. The key is now swallowed in attachCustomKeyEventHandler for every non-shell mode, and unconditionally in the subagent/teammate terminals, which always run an agent CLI. The match is case-insensitive, because Caps Lock flips ev.key to 'Z' without setting shiftKey and a plain === 'z' check would let exactly the keystroke this exists to catch through.

    This is defence in depth rather than a fix for the steady state: an agent CLI holds its tty in raw mode with ISIG off, where ^Z is already inert. It covers the moments that are not the steady state: the window before the CLI takes the tty at startup, and any point where it hands the tty back. Two input paths are deliberately not covered and still reach the PTY: the mobile keyboard accessory bar's one-shot Ctrl, and the CJK composition textarea when cjkInputEnabled is on. Both are separate choke points to the PTY, and both are worth covering if this ever turns out to matter in practice.

  • fix(terminal): let Claude use truecolor so its themed backgrounds render

    Claude draws the user's own messages as a block of background color, and it renders as an approximation of the theme color at best. Claude's registry entry deleted COLORTERM, which left it the only agent CLI here besides opencode not asking for 24-bit color, so every RGB color its theme asks for was quantized down to whatever palette TERM alone implies. Claude now exports COLORTERM=truecolor like codex, gemini, antigravity, pi, grok, deepseek and omp already do, and the block renders in the color the theme actually names.

    How bad the quantization was depends on TERM, which is why this looks different on different machines. On tmux 3.2 and newer, whose default-terminal defaults to tmux-256color, supports-color reports 256 colors and rgb(55, 55, 55) lands on ESC[48;5;237m: visible, but not the color the theme asked for. Where TERM resolves to a 16-color entry instead (tmux older than 3.2, or a ~/.tmux.conf setting default-terminal screen, which Codeman's tmux server does read), every dark background collapses to ESC[40m, the terminal's own black, and the block disappears entirely. That is the case this was reported from, and a custom Claude theme could change the color there with nothing on screen moving.

    Those seven CLIs also unset NO_COLOR; Claude does not, so a user who exports NO_COLOR globally keeps the monochrome panes they asked for. CLAUDECODE stays unset, because Claude reads it as a signal that it is running nested inside itself.

    buildClaudeEnv(), the direct-PTY fallback used when tmux is unavailable, now reads the same registry entry as the tmux pane and its attach client instead of deleting COLORTERM from a hand-maintained list of its own. It applies that entry before assigning Codeman's own variables, mirroring buildEnvExports(), so a clis.json override naming one of them cannot strip it on this path while the tmux pane keeps it. A remote pane still exports nothing, because buildRemoteLaunchCommand() never carried these declarations, so an SSH-remote Claude session keeps the old rendering.

    PR #3 introduced the unset COLORTERM in February, citing xterm.js#484 for the claim that xterm.js mishandles truecolor, and aiming to fall back to 256-color mode. xterm.js closed that issue in April 2019, Codeman now depends on @xterm/xterm 6, and TmuxManager sets terminal-overrides ",*:Tc" on its own tmux server, so 24-bit color already reaches the browser for the CLIs that ask for it.

    Thanks

    • @shenlvkang-collab for the path picker's typed-path jump and name/date sort (#399), and for the care in the edges: the retry is bounded to one parent level, a typo keeps the listing you had instead of resetting to the root, and a full file path lands in its folder with the entry already selected.
    • @irisitymichaelgrundberg for Claude truecolor in panes (#409), and above all for flagging the one reading they could not prove: that suppressing truecolor may have made Claude's block collapse into the background rather than fixing anything. That paragraph is why this got measured instead of taken on trust, and the measurement changed the changelog.
    • @timkjr for trapping Ctrl+Z in agent sessions (#404), for finding that Caps Lock flips ev.key to 'Z' without setting shiftKey so a plain === 'z' check misses exactly the keystroke the guard exists for, and for stating up front that an agent CLI already holds its tty with ISIG off rather than overselling the fix.

1.27.0 2026-09-12 release →

Minor Changes

  • Session lists that answer "which of these wants me next?", loopback links that work from a phone, and a batch of input and remote-session fixes.

    The vertical tab rail sorts by activity and wears the home screen's cards. A new per-device setting (App Settings → Appearance → Tabs → Vertical Rail Order, default By activity) orders rail rows with the same comparator both home screens use: whatever is blocked on you first, then whatever has been running longest, then the most recently quiet. Detailed rail rows become cards, with the state dot keeping its working ring and gaining the home rail's green halo. ⚠️ Existing vertical-rail users get sorting on upgrade, and a self-sorting list cannot also be drag-reorderable: choose Manual to get your own order and drag-reordering back. The lineage bracket also moves 4px further from the rail's left edge, where its glow was being clipped by the window frame.

    The Claude Response Viewer's brief view shows the whole last turn. It used to render one row, so the eye button often showed the "Done." tail of an answer whose substance was in the rows above it. A multi-row turn now also opens at its newest text instead of its first narration line.

    A localhost link in agent output opens as a proxied web tab. An agent prints http://localhost:5173/ and you tap it on a phone: that address only exists on the Codeman box, so the link was a guaranteed connection error from any other device. It now opens through the proxy, reusing a saved dashboard for the same dev server (one tab per server, not per host spelling) or saving one under its host:port. LAN and tailnet addresses still open directly, and on the box itself every link opens directly. *.localhost is deliberately not auto-routed: it is the only spelling that is a DNS name rather than an address literal, and these links come from agent output; add such a dashboard by hand instead. Trusted (non-sandboxed) dashboards are likewise never auto-reused by a tapped link.

    Remote omp and remote claude sessions continue their conversation across a respawn or reattach. Remote claude now launches an idempotent --session-id || --resume pair and remote omp respawns with --continue, instead of starting a fresh conversation each time. An omp session id is never resolved from the local ~/.omp for a remote session, which would have pinned an unrelated local conversation.

    Android and IME keyboards no longer drop committed characters. Chrome on Android delivers a composed: true input event preceded by a keydown, which is exactly the shape xterm refuses to forward, so the character vanished. A recovery controller forwards it when, and only when, xterm produced nothing for that keystroke, so dictation and soft-keyboard input cannot be delivered twice either.

    Thanks

    • @shenlvkang-collab for the Response Viewer last-turn fix (#400) and for loopback links as web tabs (#401), both carefully measured, #400 against 285 real transcripts.
    • @timkjr for remote-omp resume/continue through respawn and reattach (#362), including dropping a half that had already landed and verifying the merge kept none of it.
    • @aakhter for the Android/IME input recovery (#388), and in particular for finding that an earlier version of their own browser test was passing vacuously, and saying so.

1.26.2 2026-09-10 release →

Patch Changes

  • Terminal rendering fixes, a Ctrl+V paste fix, an iOS Safari toolbar fix, a 2GB download cap, and a Blur entrance animation.

    Terminal rendering

    Three independent causes behind #398, where opening a session rendered a frame with characters spliced into each other and left the caret on the composer's border instead of its input line, until the CLI next wrote anything:

    • The full-history replay now keeps row alignment (#395). The linear capture path never restored the cursor, so every cursor-relative update the CLI sent afterwards was measured from the status line instead of the pane's real position, and four transforms that each can delete a line (trailing-blank stripping, redraw-bloat stripping, the pre-banner trim, leading-whitespace removal) shifted the frame out from under it. The full-history path now appends the pane's own cursor position and keeps every row, so row N of the reply is row N of the pane. The visible-frame and tail paths are untouched.
    • The first fit waits for the terminal font (#396). A cell measured against a fallback font gives the wrong column and row count, so the pane was sized twice and the CLI repainted for a shape that no longer matched the frame on screen. selectSession now holds for the font before measuring, bounded at 2s so a font that never arrives cannot strand a session, and it ends by re-measuring explicitly — FitAddon.proposeDimensions() divides by a cached cell size and nothing in it listens for font loading, so waiting alone would still divide by the fallback cell.
    • A detached session's own window owns its pane size (#397). Popping a session out left both windows sizing one PTY, and the dashboard's terminal is narrower than the popup because the session rail takes width the popup does not have, so the CLI drew frames that fit neither. The dashboard now withholds the resize send (never the local reflow) for a session showing in its own window, and takes sizing back on redock.

    Other fixes

    • Ctrl+V no longer pastes twice (#394). One keypress delivered two paste events to the clipboard trap: Firefox dispatches a trusted event for document.execCommand('paste') and then returns false, and the key's own default action fires another, because xterm's custom key handler returns false without cancelling the keydown. Right-click → Paste has no keydown, which is why only the keyboard duplicated. The trap now consumes exactly one event per keypress.
    • iOS Safari: the phone toolbar sits on Safari's bottom bar (#391, #392). The toolbar was lifted by 100vh - --app-height, which on iPhone Safari measures the bar's collapsible height rather than an overlap — fixed elements there already stop above the bar — leaving an empty ~40px band and padding the terminal by the same amount. The lift is now --chrome-overlap (innerHeight minus the visual viewport height), which is 0 on iPhone Safari and equals the real overlap anywhere fixed elements do land behind the chrome.

    Downloads

    file-raw, the attachment /raw route and GET /api/download now cap at 2GB instead of 50MB, configurable via CODEMAN_MAX_DOWNLOAD_BYTES (0 = unlimited). The old cap was memory protection for a readFile() that no longer exists: those bodies stream and answer Range requests, so size costs a read stream rather than RSS (measured: a 600MB download moved peak RSS by ~37MB), and all the cap still did was refuse legitimate downloads of build artifacts, videos and archives. /api/download was the last route that really did buffer the whole file; it now streams, advertises Accept-Ranges and is resumable. Refusals move from 400 to 413, the correct status for the case.

    Blur entrance animation

    A new opt-in Blur style on all four entrance surfaces (tabs, agent windows, the terminal pane, connection lines), plus a Soft focus theme that sets all four: an iOS-style focus pull where the thing arrives out of focus and the blur fades off it as the opacity comes up. App Settings → Appearance → Entrance Animations, or mix per surface at ?animlab=1. Entrance animations stay off by default, so an untouched install is unchanged.

    Maintainer tooling

    The PR bot now fails fast when the review model's budget is spent, instead of hanging a review for the full 40-minute timeout and burning its retry cap.

    Thanks

1.26.1 2026-09-07 release →

Patch Changes

  • Codex sessions no longer report idle for their entire life, and Codex conversations now appear in Past Sessions and can be resumed.

    Per-CLI work detection (#385, irisitymichaelgrundberg). The composer glyph and the working status line are now registry data (capabilities.workDetect) rather than Claude constants. Claude keeps its exact current pair, Codex declares › plus its esc to interrupt footer, and any CLI that declares neither falls back to Claude's, which is what every session used before. Work detection had been gated Claude-mode-only on the reasoning that an external CLI has no ❯, which was true and still left every Codex session reporting idle from the moment it started. workingLine is config-supplied and its compiled pattern runs on the PTY hot path, so it goes through compileVersionRegex() in both the schema refine and the runtime compile: a nested quantifier there would backtrack on the event loop for the whole server. The Codex footer is matched case-insensitively on the E, so a future version capitalising it cannot make the fix silently inert.

    Codex conversations in Past Sessions (#386, irisitymichaelgrundberg). A bounded scanner reads codex's ~/.codex/sessions rollout store, so the unified session list now merges three transcript stores rather than one (Claude's ~/.claude/projects, omp's ~/.omp/agent/sessions, codex's ~/.codex/sessions). A scanned row carries a resumeId, the rollout's own thread id, which lets it resume through codexConfig.resumeSessionId; a live session never carries one, so a row without it stays a genuinely fresh session. Live and resumed Codex sessions fold into their rollout row through the existing alias map, including a session_meta.originator match for fresh panes, so a conversation never shows up twice. The phone overview carries resumeId through its own row projection, without which a tapped Codex past row started a fresh session on a thread already on disk.

    Thanks

1.26.0 2026-09-07 release →

Minor Changes

  • Tag the case directories agent workers create, and clean up what they leave behind.

    A long agent orchestration creates one case directory per worker, and deleting the sessions never removed them, so ~/codeman-cases filled with scratch folders that looked exactly like real projects.

    • A case directory POST /api/quick-start creates for an agent-driven spawn now carries a .codeman-agent-case.json marker recording when it was made, by whom, from which session, and in which mode. Only the branch that creates the directory writes it, so a linked case, a cloned repo or any pre-existing path is never labelled, and deleting the marker file adopts a scratch case as a real one.
    • The label comes from the new X-Codeman-Agent-Origin header that the packaged agent skill sets on its shared curl invocation (preamble 1.22.0), or an agentOrigin body field, falling back to a resolved parentSessionId so workers spawned by an older skill copy are still labelled.
    • GET /api/cases publishes it as agentCreated, and the new read-only GET /api/cases/agent-created lists the scratch cases with inUse (a live session is still working in it) and modifiedAt.
    • Add Case -> Manage badges every agent-created case and adds a sticky Clean up entry point that names each directory in its confirmation and skips any case a running session is using. Removal still goes through DELETE /api/cases/:name.
    • The agent skill's per-session preamble cache (~/.cache/codeman-agent-<id>.sh) is now removed with the session and swept at boot. One was written per Claude session and nothing ever deleted them (236 orphans on a working machine); the sweep keeps every live session's file and only takes orphans older than seven days.

    Thanks

    1.26.0 carries no contributor PRs of its own. It lands the day after 1.25.0, so the thanks for that pair belong here too:

1.25.0 2026-09-07 release →

Minor Changes

  • Codeman can be mounted under a sub-path behind a reverse proxy (#381, @mtiller). --base-url /codeman (or CODEMAN_BASE_URL) makes the server strip the prefix on the way in, rebase redirects on the way out, inject <base> and window.__CODEMAN_BASE__ into the shell, and route web-tab proxying and WebSocket upgrades under the mount, so one TLS name can front several apps. A root install is byte-identical to before. Applied on top: the crash-diag beacon stays under the mount (sendBeacon is not fetch, so the base-aware wrapper never saw it), the test suite strips CODEMAN_BASE_URL, and a wiring test boots a real server under a prefix.

    A case can attach to a container that is already running (#357, @dignfei). DockerCase.owned:false mirrors the remote-SSH attach contract: Codeman only execs into such a container, never creates, starts, stops, removes, pauses or commits it, with the refusal enforced at string-construction time so no caller bug can reach docker stop. The Add Case dialog gets an attach panel with a container picker, the run menu takes its mode availability from the CLIs actually present in the container, and adoption is admin-only in multi-user mode. Three gaps closed after review: export no longer pauses or commits an adopted container, a freshly linked owned case no longer hides every agent mode behind a probe of a container that does not exist yet, and multi-user gating is explicit.

    The Claude response viewer renders one message per model message (#369, @shenlvkang-collab). The reader used to fuse every assistant row between two human prompts into one card and never read the attachment rows that hold a prompt typed mid-turn; measured over 57 real transcripts it now shows 1,806 messages instead of 356 and recovers 162 absorbed user prompts, with the assistant text unchanged row for row.

    A Claude pane learns its live conversation from the CLI's own UserPromptSubmit hook (#367, @shenlvkang-collab). The conversation id used to be re-derived by correlating ~/.claude/history.jsonl against a stamp only Codeman's own input path set, so a pane driven straight from tmux stayed pinned to its launch conversation forever. The hook reports the id first-hand, addressed by the pane's own $CODEMAN_SESSION_ID, and the chain of conversations is persisted so a restart re-pins the right one. The new hook:prompt_submitted SSE event is registered (158 = 158), and it lands in the run summary only when the conversation actually moved.

    The Add Case modal can be submitted from a phone again (#368, @shenlvkang-collab). Since 1.16.4 the layout below 860px hid the modal footer, which held the only Create/Clone/Link button. A header submit button now sits beside the close button, dims while a submit is pending, and a static test pins the contract so it cannot silently disappear again.

    The Link Existing case picker opens in the Codeman Cases directory instead of Home (#383, @opticon454). Under Docker the two are unrelated trees and Home holds nothing but dot directories, so the picker showed no cases at all. The fallback chain is now Current Folder, then Codeman Cases, then /mnt/d, then the first root.

    A PR review bot for the maintainer (scripts/pr-bot/, guide in docs/pr-bot.md). It reviews every open pull request in its own Codeman session inside a private clone and reports the verdict, ranked findings and a recommendation to Telegram with action buttons; merge, close, post-comment and approve-CI happen only from a confirmed tap. Maintainer tooling, not part of the server or the CLI.

    Thanks

1.24.7 2026-09-04 release →

Patch Changes

  • The web-tab proxy refuses link-local and cloud-metadata targets. Its Test probe, the proxy itself and the WebSocket relay accepted any http(s) host, so a saved dashboard URL could reach 169.254.169.254 (in decimal, hex, IPv6-mapped or DNS-name form) through a capability and no cookie. Loopback and RFC1918 addresses stay allowed on purpose, since a localhost Grafana is the feature; only link-local and the fixed cloud-metadata addresses are refused, at the schema, at every connect site, and through a DNS lookup hook that judges the resolved addresses, which is what closes DNS rebinding. Adds undici so the proxy runs its fetch through its own agent.

    Proxy capabilities are revoked on logout. revokeOwner() had shipped with no caller, so a leaked proxy URL stayed valid for as long as anything kept polling it. POST /api/logout, the admin forced logout and user deletion now revoke the capabilities they should, and proxied responses carry Referrer-Policy: same-origin with the upstream's own policy dropped, so a dashboard on a loose referrer policy cannot hand the capability to a third-party host it links to.

    The Docker Compose deployment updates itself from App Settings again (#373, @opticon454). The checkout Compose builds from is bind-mounted at /opt/codeman, so an update's git checkout and rebuild land on the host and survive container recreation; build artefacts live in named volumes so container-compiled native modules never enter the host checkout; the image keeps devDependencies and a build toolchain; and the restart is the server exiting under restart: unless-stopped. An in-place update applies code only, so the updater refuses a release that changes server.Dockerfile or docker-compose.yaml, or that adds keys to .env.example the user's .env has no value for (Compose interpolates an unset variable to the empty string and starts anyway), and points at docker/Start-Codeman.sh on the host instead. The four global agent CLIs in the image are pinned. A follow-up makes the final step fail safe: the server exits only when the Compose file declares CODEMAN_RESTART_BY_EXIT=1 or the daemon confirms an auto-restart policy, and otherwise the build is staged for a manual restart, so a container nothing would restart is never taken down. Details in docs/docker-self-update.md.

    The test suite strips CODEMAN_INSTANCE, CODEMAN_DATA_DIR and CODEMAN_TMUX_SOCKET before any application module loads (#371, @opticon454), with a two-half test whose static half reads test/setup.ts so a dropped line fails everywhere. This replaces the throwaway data dir #356 had set for the same variable.

    Thanks

1.24.6 2026-09-04 release →

Patch Changes

  • CLI backends are now a data-driven registry (#347, @opticon454). Every run mode (Claude Code, Terminal/Shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek Harness and OMP) is a CliEntry in src/config/cli-registry/: binary discovery (search dirs, version and identity probes), the launch argv template, environment handling, the multi-user privileged-parameter and privileged-env-key clamps, the remote and Docker pane commands, and the capability flags the rest of the app reads instead of branching on a CLI's name. ~/.codeman/clis.json can override any stock entry or add a custom CLI; it is read-only in this release, must be mode 0600, and every reason it was ignored is now logged once on first load (docs/cli-registry.md). Config never contains shell text: entries declare typed argv tokens, literals are validated at load time, and values resolve through patterns named in code. Registry data resolves at call time rather than at module import, so a CLI enabled while the server runs moves every surface at once, and a guard test fails the build if per-CLI-id branching reappears outside the stock catalog.

    This is an internal refactor. The spawn command every CLI receives is byte-identical to the previous hand-written builders, verified by pinned golden strings in the test suite and by diffing both implementations across 11,602 option combinations for all ten modes. Five small deliberate changes ride along: the in-container version probe derives the binary from the registry (antigravity runs agy), the remote version probe now covers Grok and DeepSeek, codeman doctor's CLI rows are generated from the registry (Claude's install hint is the install command, five CLIs gain hints, the row order follows the catalog), OMP now requires tmux like its siblings instead of silently falling back to a direct PTY, and an OMP session's attach client now receives COLORTERM=truecolor like the other truecolor CLIs.

    Remote sessions are no longer auto-revived after a clean agent exit (#355, @timkjr). The reconnect watcher could not tell a transport drop from a Ctrl-C, Ctrl-D or exit inside the remote CLI, so a clean exit relaunched a fresh agent (OpenCode and OMP started a new conversation every time; Claude only looked fine because its --resume fallback masked it). The watcher now revives a dead pane only when the durable remote tmux session is verifiably still alive, via a has-session probe over ssh, and an unreachable host means do not revive. A follow-up classifies that probe by exit status, since tmux has-session prints nothing on success and reading its stdout had marked every live session as gone, forgets the cached answer whenever the pane is seen alive again so a stale result cannot revive a later clean exit, and caps the probe at one in flight per session.

    The test suite can no longer reach the production ~/.codeman data dir (#356, @timkjr). test/setup.ts now points CODEMAN_DATA_DIR at a throwaway directory, which is the absolute override that bypasses the suite's temporary HOME when inherited from the shell, and every test that deletes a case tree goes through a containment gate that refuses paths outside the temporary HOME. A bare suite run had overwritten a real remote-hosts.json with a route test's fixture. The comments around it and CLAUDE.md's testing section now name that variable as the cause; os.homedir() itself does follow $HOME.

    Thanks

    • @opticon454 for the CLI registry (#347), the phased resubmission of #343, and the review rounds that hardened it.
    • @timkjr for the remote auto-revive fix (#355) and the test-isolation sweep (#356).

1.24.5 2026-09-02 release →

Patch Changes

  • Fable 5.1 is selectable in App Settings.

    claude-fable-5-1 is in Claude Code's model catalog (display name "Fable 5.1", June 2026 knowledge cutoff), but the model picker only went up to Fable 5, so pinning it meant hand-editing a case's .claude/settings.local.json. It now appears as a card under App Settings -> Models -> New Claude sessions, and as an option in Task routing (Default for tasks, plus the Explore / Implement / Test / Review overrides).

    It is offered exactly the way Fable 5 already is: the "1M capable" badge, the 1M context window switch stays live for it, and base + switch compose into claude-fable-5-1[1m]. Both strings are accepted by the CLI.

    Deliberately not claimed: that a 1M window is what sets Fable 5.1 apart. The CLI's model catalog marks both fable entries as natively 1M with the same window, so an always-on window for 5.1 next to a switchable one for 5 would encode a difference the models do not have.

    Thanks

1.24.4 2026-09-01 release →

Patch Changes

  • The Compose deployment image ships the Docker CLI instead of the whole Docker engine.

    docker/server.Dockerfile installed Debian's docker.io to get a client for the mounted host socket. That package is the full engine: even with --no-install-recommends it pulls 15 packages including containerd, runc, dmsetup and iptables, none of which a container that only talks to a socket can use. It also ships Docker 20.10.24, from 2023.

    The CLI and the buildx plugin are now copied from the official docker:29-cli image instead. Measured on the same node:22-bookworm-slim base: 266 MB → 108 MB, a 158 MB saving, with the current CLI (29.7.2) in place of a two-year-old one.

    Verified by building the real image and running it: the binaries are static Go builds, so they work on this glibc image even though they come from an Alpine one, and `docker

--version, docker psanddocker buildall succeed against a mounted host socket as the unprivileged runtime user. buildx is copied deliberately —scripts/build-agent-image.mjs shells out todocker buildand Codeman auto-builds the agent image on the first Docker case, which without the plugin falls back to the classic builder Docker has deprecated. docker-compose` is not copied; Codeman never shells out to it.

Thanks

1.24.4 is a same-day follow-on to 1.24.3, so the thanks for that pair belong here too:

  • @opticon454 for #349, and for a write-up that made an infrastructure PR quick to review

1.24.3 2026-09-01 release →

Patch Changes

  • Docker Compose deployment, and the plan-usage chip stops losing its 5-hour window.

    Run Codeman itself in a container (#349, @opticon454). docker/ now carries a local-image Compose deployment: copy docker/.env.example to docker/.env, set CODEMAN_PASSWORD, run bash docker/Start-Codeman.sh. Docker cases then start as sibling containers through the mounted host socket rather than nested ones, which inverts an assumption the bare-host path takes for granted: the daemon no longer shares Codeman's filesystem, so a bind source that is valid inside Codeman means nothing to it. CODEMAN_DOCKER_HOST_HOME translates sources under HOME into the daemon's namespace and CODEMAN_CASES_PATH points the cases dir at a host-absolute bind mount, so a workspace resolves to the same absolute path on both sides. CODEMAN_DOCKER_DISABLE_SWAP_LIMIT=1 drops --memory-swap for hosts without swap accounting (--memory still applies) and filters only that one kernel warning. Guides: docs/docker-compose.md, docker/README.md.

    Three things were fixed while landing it:

    • docker/.env was being baked into the image. A .dockerignore pattern matches the whole context-relative path, so the bare .env line excluded only the root file while COPY . . picked up docker/.env — the file the deployment's own README tells you to fill with CODEMAN_PASSWORD and provider API keys — and left it at /opt/codeman/docker/.env. Now excluded via **/.env, verified in both directions against a real build context with a canary secret.
    • codeman skill install --case <name> could not find a case under Compose. CODEMAN_CASES_PATH moved the server's cases dir but not the CLI's, which still hardcoded ~/codeman-cases. Both now resolve through one place.
    • A Docker case handed its Claude conversation id to every other CLI. resumeOnStart seeded dockerResumeId from lastClaudeSessionId regardless of mode, and appendResumeFlag() maps a resume id onto codex/gemini/pi/grok/deepseek/omp/antigravity. This one is a plain master bug, unrelated to Compose.

    The plan-usage chip keeps its 5-hour slot. It silently shrank from 5h 4% · 7d 52% to a lone 7d 52%, which reads as half the feature breaking. Nothing was broken: Claude Code ships rate_limits.five_hour "only while the API reports it and its resets_at has not passed", so between 5-hour session windows the key simply leaves the statusline payload. The slot now stays with a dimmed em dash and the tooltip says "no active session window". Claude only — a missing Codex bucket means that plan has no such limit, so those stay omitted.

    Thanks

    • @opticon454 for #349, and for a write-up that made an infrastructure PR quick to review

1.24.2 2026-09-01 release →

Patch Changes

  • Fix every new claude session dying on Claude Code 2.1.252's rewritten folder-trust dialog.

    That dialog used to offer ❯ 1. Yes, I trust this folder / 2. No, exit, so Codeman answered it by pressing Enter on the highlighted default. 2.1.252 dropped the numbers, reversed the options and highlights No, exit, so the same Enter now answers exit: a session in any directory claude had not seen before died (Pane is dead (status 1)) about six seconds after it started, before the agent ever drew a composer.

    • trustDialogNextKey() (src/session-trust-dialog.ts) now reads the ❯ marker off the rendered pane and returns ONE keystroke at a time: an arrow while the cursor is on the wrong option, Enter only once the screen shows it on the trust option. A frame it cannot read presses nothing. Both the 2.1.252 and the older numbered layout are handled, and the direction is derived from the frame rather than assumed, so a further reordering costs a repaint instead of a session.
    • The scan schedules its own follow-up read. It had only ever run from the PTY data handler, which was enough while one Enter answered the dialog; the arrow that moves the cursor is the last output the pane produces, so a two-keystroke answer would otherwise stall with the cursor sitting on the right option forever. The keystroke cap goes from 3 to 6 for the same reason.
    • The bundled codeman agent skill gets the same treatment (preamble 1.21.0): its _accept_trust fallback reads terminal?full=1, steers onto the trust option and confirms only after re-reading, instead of posting a blind \r. It sends those keystrokes under its own clientId, because input sequence numbers are monotonic per client and spending prompt numbers on dialog keys would make the next send-and-wait look like a stale duplicate and vanish silently.
    • Readiness recipes in docs/extending-codeman.md, docs/api-reference.md and the skill's own reference carry the corrected answer and a new symptom-table entry for a worker whose pane is dead seconds after the spawn.

    Also included: a CLAUDE.md audit against the tree, correcting counted drift (route modules, handler counts, frontend module count and app.js size, install.sh size) and documenting several subsystems that had no entry.

    Thanks

    1.24.2 is a hotfix on top of 1.24.1, so the thanks for that pair belong here too:

    • @opticon454 for #350, with a reproduction that made this a confirmation rather than a hunt
    • @timkjr for reporting #352, and for finding it while verifying Docker support for someone else's PR

1.24.1 2026-08-31 release →

Patch Changes

  • The Docker agent base image builds again.

    docker/agent.Dockerfile could not be built from a fresh checkout (#352, fix in #350): the DeepSeek Harness step died with dsh: pnpm not found on PATH and exit 127, which took the whole image with it and, because Codeman auto-builds this image on the first Docker case, left Docker mode unusable on a clean host. dsh plugin does not bundle a package manager; it spawns a literal pnpm with no npm fallback, so pnpm is now installed alongside dsh and the layer proves it with pnpm --version.

    The profile install also passes --config.dangerouslyAllowAllBuilds=true, because pnpm, unlike npm, refuses dependency lifecycle scripts by default and fails the install over it (ERR_PNPM_IGNORED_BUILDS, exit 1). Which packages that hits moves between rebuilds, since the terminal profile is resolved by dist-tag rather than pinned: the tree that broke the build in August pulled @google/genai, today's does not. An allowlist of those names would have gone stale rather than prevented the next break, and running those scripts is the same exposure the image already accepts three layers up, where npm install -g runs the install scripts of every transitive dependency of the five CLIs above it with no gate at all.

    Documentation caught up with two things it had wrong: the image smoke test in docs/docker-cases.md now covers dsh and omp, and checks the dsh profile rather than only the binary (dsh is a launcher, so dsh --version says nothing about whether a session can start), and docs/deepseek-integration.md names pnpm as a prerequisite for installing a terminal profile at all, by hand or through the UI button. A comment in the /api/deepseek/install-profile route claimed the opposite of what this bug proved, and is corrected; the route's behaviour was already right, surfacing dsh's own "pnpm not found on PATH" line as the install error.

    Thanks

    • @opticon454 for #350, with a reproduction that made this a confirmation rather than a hunt
    • @timkjr for reporting #352, and for finding it while verifying Docker support for someone else's PR

1.24.0 2026-08-30 release →

Minor Changes

  • OMP (Oh My Pi) as a tenth run mode, mode-faithful Resume for external CLIs, and a cleaner plan-usage chip.

    OMP (omp) run mode (#353): Oh My Pi joins Claude Code, shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok Build and DeepSeek Harness as a run mode, in local, Docker and remote-SSH sessions: toolbar dropdown, welcome button, phone overview, command palette, clone-repo brain picker, cron agent types, tab badges and per-mode colours, plus GET /api/omp/status, a codeman doctor entry, install.sh detection and the docker agent image. The resolver leads with ~/.local/bin (the upstream installer's real target) and demands omp/<semver> from --version, so an unrelated binary with the same three-letter name is never spawned. Past omp conversations appear in Past Sessions, read from omp's own session files (the header line carries the real working directory, so nothing has to reverse-engineer omp's directory mangling), and a respawned or resumed omp session is pinned to an exact conversation with --resume <id> instead of omp's newest-file --continue. Review hardening before merge: the pin is resolved only at the moment a respawn is actually confirmed (an eager resolve on boot recovery used to alias two omp tabs in one case directory onto one conversation), candidates are verified against their own header cwd and claimed process-wide so siblings cannot double-pin; OMP_* joins the env-override allowlist and OMP_AUTH_BROKER_URL/OMP_AUTH_BROKER_TOKEN are clamped for non-granted owners in multi-user mode, the same shape as DEEPSEEK_BASE_URL. Known and documented: omp's own knobs are mostly PI_* (it is a pi fork), its default tools.approvalMode is yolo, and in-container --resume pinning does not reach a Docker omp pane.

    Resume keeps the row's own CLI (#353): clicking Resume on an OpenCode, Pi, Grok, DeepSeek or OMP row used to create a plain Claude session, since the create request never carried the row's mode. Resume now relaunches in the row's own mode with that CLI's continue flag, and retires the stale row it came from so three clicks no longer leave three copies of the same name. Codex, Gemini and Antigravity rows have no continuation wired yet, so their rows are deliberately left in place. DELETE /api/sessions/:id accepts a persisted-only session (ownership enforced through the same helper as live lookups, 404 rather than 403 so nothing leaks) and broadcasts session_deleted so other tabs drop the row too.

    Plan-usage chip drops the provider label when there is only one: a machine with only Claude limits rendered CLAUDE 5H 60% 7D 23%, a 46px label naming the only thing it could be. The name exists to tell two rows apart, so it now appears only when both Claude and Codex have windows; the tooltip still names the provider either way.

    Thanks

    • @timkjr for #353, and for turning every review finding around within a day

1.23.2 2026-08-28 release →

Patch Changes

  • Codex plan usage in the header chip, a visible inline rename in the session sidebar, and an installer that no longer loses Tailscale access on a re-run.

    Codex plan usage in the header chip (#346): the plan-usage chip used to show Claude's 5-hour and weekly limits without saying they were Claude's, which stops being a detail the moment you run more than one CLI. It now renders one compact row per provider, Claude above Codex, each labelled and colour-coded by how much is used up. Claude's numbers still come from Codeman's marked statusLine.command exporter; Codex's come from the signed-in host CLI's read-only account/rateLimits/read app-server request at startup and every five minutes, so credentials stay inside the CLI and no auth material reaches the browser. Only the main codex bucket is read (model-specific buckets such as Spark are separate limits and are deliberately excluded), and the Codex row is omitted entirely when no 5-hour or weekly window is available, rather than inventing one.

    Inline rename is visible in the session sidebar (#345): starting a rename on a sidebar row opened a focused input you could not see. The row's ellipsis clamp was still painting over the live editor, so text and caret went in blind. The sidebar now gets the same unclamped editor layout the vertical tab rail already had. Covered by a Chromium regression test that asserts the painted overflow and the input's measured width, not just the class name.

    install.sh keeps Tailscale access on a re-run: a re-run whose build failed could drop a working Tailscale binding instead of preserving it. The installer now offers Tailscale setup again on re-run rather than losing it, and the README describes the three-way network-access prompt (Tailscale / LAN / local-only) as it actually behaves.

    Thanks

Older releases (0.2.0 to 1.23.1) →