Agent Deck v1.16.30
Terminal session manager for AI coding agents.
Installation
Homebrew (recommended):
brew install asheshgoplani/tap/agent-deckQuick Install:
curl -fsSL https://raw.githubusercontent.com/asheshgoplani/agent-deck/main/install.sh | bashGo Install:
go install github.com/asheshgoplani/agent-deck/cmd/agent-deck@v1.16.30What's new in 1.16.30
Highlights
- Run Claude slash commands such as
/modelor/compacton a Claude session safely withagent-deck session command, or from a conductor chat with/cmd <session> <command>(see Sessions). - Opt-in browser for agents: sessions can open and read pages in the Mac app's Browser panel with
agent-deck browser. It is off by default; turn it on withagent-deck config set browser.enabled trueandagent-deck config set macapp.plugins true --yes(see macOS app). agent-deck session focus --remote <name> <id>jumps the running TUI to a session on a remote (see Sessions).- New Triggers view:
agent-deck triggersandalt+tin the TUI show what wakes each session and whether it is healthy, with mute and run now (see Triggers). - Web terminal: Cmd-click (Ctrl-click) opens links in sessions that capture the mouse, and
file://links under[web] file_link_rootsopen the file through the deck, also from a phone (see Web and Fixes). - OpenCode 2.x sessions show running while their sub-agents work (see Status).
- Model pickers stay current: OpenCode's list comes from the installed CLI, and
[models.<tool>]pins or extends the list for Claude, Codex, Gemini and OpenCode (see Models). - Watchers pick up changes live without a restart and use the same profile as every other command, and a remote's TUI link follows the profile the remote serves; thanks @AndreIntelas (see Watchers and Remotes).
- Cost budgets can now be enforced:
[costs.budgets] enforce = truestops agent sends once a limit is spent (see Costs). - Security: project MCP config links that leave the project are refused, text from gateways and session titles is cleaned of terminal escapes, and code scanning and Dependabot show no open alerts at release (see Security and toolchain).
Security and toolchain
- agent-deck no longer writes your MCP definitions through a project's MCP config symlink that points outside the project. A cloned repository could ship
.mcp.json(or.cursor/mcp.json, a.cursordirectory link, oropencode.json) as a link to a file elsewhere, such as~/.claude.json, and the next session start or MCP change replaced that file with your MCP definitions, env values included; a dangling link created a file outside the project. Such a link is now refused, also when the outside file already holds the same content, and a dangling link (such as a.cursordirectory link) is refused before anything is created. The error names the link and its target and says thatmanage_mcp_json = falsein config.toml stops agent-deck writing project MCP config files. Links that stay inside the project still work. The plugin pin refresh no longer writes through a fixed<file>.tmpname, and the.mcp.jsonlock file is checked too. Config files in your home directory (~/.claude.json,~/.cursor/mcp.json) still follow links as before (#2737, #2742, #2749, #2757). openclawcommands drop control characters from text the gateway sends before printing it: rejection codes and messages, close reasons and other connect, list and send errors, and the channel and agent names shown byopenclaw statusandopenclaw list. A hostile gateway can no longer send terminal escape sequences (such as a clipboard write) through them (#2735).- Session titles drop control characters and Unicode bidi formatting characters wherever they are set or loaded: agent names stored by
openclaw sync, CLI, web and TUI renames, Claude title sync, titles and groups from a remote's session list, and titles already stored by an earlier sync. Two titles that differ only in such characters now count as the same title. The tmux status line escapes#in the title and the project folder name, so a title or a cloned folder named like#(cmd)no longer runs that command on every status refresh. The bridge TUI cleans chat and history text, errors, agent names and the server version before showing them. JSON output is unchanged (#2741). - A stored Claude session ID that contains a path separator,
..or a glob character (*,?,[) no longer finds a transcript outside the Claude projects directory or another session's transcript; such an ID finds nothing. Plain IDs set by hand withsession set <id> claude-session-idstill work (#2730).
Updates
- A local build (
<version>+local...) is now offered the published release of the same version, soupdate --check, the auto-install and the releases-behind count move a machine running1.16.29+local.<date>.<sha>onto v1.16.29 or later instead of keeping the local build. Other build metadata (such as+build.5) still counts as the same version, and release candidate ordering is unchanged (#2710, #2711). update,update --check,doctorand the TUI's "restarted into" notice warn when an olderagent-deckearlier on PATH (for example a/usr/local/bin/agent-decklink to an old build in front of~/.local/bin) keeps your shells, hooks and scripts on the old release. The warning names the copy, both versions and the fix: thermcommand (withsudowhen needed) or anexport PATH=...line, or the package manager for a Homebrew, Nix, asdf, mise, snap or/usr/bincopy. agent-deck never removes the copy itself.update --check --jsonanddoctor --jsonlist such copies undershadowed_by(empty when PATH is clean).update --unattendeddoes not check (#2725).
Models
- OpenCode's model list comes from the installed CLI (
opencode models, no prompt, about 3s timeout, cached per binary for up to a day and probed again after a CLI upgrade), merged in front of the built-in list, so new models show up in the TUI and web pickers andlaunch -capabilities --jsonwithout an Agent Deck release. A failure keeps the built-in list.[models.<tool>] list = [...]pins a tool's suggestions and[models.<tool>] extra = [...]adds IDs in front, forclaude,codex,geminiandopencode(#2664, #2714, reported by @koenzhao).
Sessions
- Restarting a Hermes entry resumes that entry's own conversation more reliably. The hook handler now records the conversation each entry's own turns report, so restart no longer depends on the TUI having sampled the right moment of a turn.
session startand the web Start button resume the same conversation a restart would. A delegated subagent's conversation is never recorded, and neither is one from a nestedhermesrunning outside the entry's directories. A fresh restart forgets the record, and a late hook from the old process can no longer bring it back. After/new, every later restart keeps the new conversation, and sandboxed entries resume the same way. Entries with nothing recorded behave as in 1.16.29 (#2582, #2722, #2745, #2760, reported by @renky). agent-deck session focus --remote <name> <id> [--attach]moves the running TUI to a session on a remote: the cursor lands on its row under Remotes, folded headers and filters that hide it open, and--attachopens it the way Enter does. Nothing local is created. An unknown remote exits 2 and changes nothing (#2704, #2715).agent-deck session command <session> "/<command> [args]"runs a Claude Code slash command (/model,/compact,/clear,/exit, or any other command or skill) in a Claude session and checks that it ran. Use it instead ofsession send, which presses Enter without looking at Claude's autocomplete picker and can run a different entry. It types nothing unless the composer is empty and the agent idle (--wait-idle 60swaits for that), never accepts a ghost suggestion or another picker entry, leaves a dialog it did not open alone, and reads the outcome from the pane:/modelchecks the model Claude reports,/compactthe Compacted notice,/exitstops the session./model <id>switches that session only: it picks the model in Claude's picker with "use this session only" and never changes the default model for new sessions, and it fails withdefault_changedif the saved default changes to the requested model anyway. It takes aliases (haiku,sonnet,opus,default, with or without[1m]) and full ids; a model that is not in the picker fails withunknown_modeland lists the entries, and a bare/modelis an argument error. A switched model is kept as the session's model, so restart and fork use it;/model defaultclears it./loginand/logoutare refused, and only Claude sessions are supported for now.--jsonprints the result, with acodeon failure (an unknown session exits 2);--dry-runtypes nothing;--backgroundruns it detached, which a session needs when it targets itself.agent-deck session commands [session]lists the built-in, user, project and plugin commands and skills a session can run, without typing anything. Conductors get a/cmd <session> <slash command>quick command (and/cmd <session> what can I run) that uses it and replies with one line, so you can switch a session's model or compact it from Telegram or Slack; on those channels send it ascmd ...without the leading slash (#2734, #2736).- Claude-family sessions (
claudeand custom tools that wrap it) that agent-deck launches get one more line in their identity block, telling them to fan out independent steps instead of doing them one by one. Other tools do not get it.[launch] fanout_hint = false(alsoconfig set launch.fanout_hint false) leaves it out, and--no-identityorcontext_level = "none"still write no block. It applies on the next start or restart (#2719).
Remotes
- A remote set up without a profile (or with
default) whose host selects another profile throughdefault_profileorAGENTDECK_PROFILEpushes session changes to the TUI within a few seconds again, instead of only at the next 15-second fetch:remote-agentnow watches the state DB of the profile it serves. If the host'sdefault_profilechanges while the TUI is connected, the link reconnects within about 5 seconds and follows the new profile. Remotes with an explicit profile are unchanged (#2755, #2756, thanks @AndreIntelas).
Costs
[costs.budgets] enforce = true(alsoconfig set costs.budgets.enforce true) turns the existing budget limits into a gate; without it they stay display-only. With it on, once a session's, its exact group's or the global daily, weekly or monthly limit is spent, agent sends and wakes are refused before anything is typed or queued:session send(and so goal nudges, conductor heartbeats and inbox wakes),--queueand queued sends,launch -m(nothing is created),session start -m, the "continue" after an MCP restart, and the TUI's heartbeat and watcher health alerts. A refusal exits 1 withbudget exceeded, orcode: "BUDGET_EXCEEDED"andresets_atunder--json; at 80% there is a note on stderr. Messages people send through the chat bridges and the web Command Center are never blocked, and--ignore-budgetonsession send,session start -mandlaunch -msends anyway. A group limit counts only sessions in exactly that group (#2727).- Cost totals no longer count a Claude reply more than once. Claude writes one reply as several transcript records with the same usage, and the transcript import stored a cost row for each, while the Stop hook stored another copy of the last one. Both now record one row per reply. On first start, agent-deck removes the Stop hook copies stored next to an import row for the same turn (same session, model and token counts, within minutes); rows stored per record by earlier versions are folded the next time a transcript is imported, and
agent-deck costs syncfolds them for existing Claude sessions now. The TUI's clear-on-compact heartbeat, when a budget refuses it, is tried again once the budget allows (#2727, #2758).
Goals
- The goal skill has opt-in progress controls; goals that do not set them run exactly as before.
no_progress_cap: Nwaits twice as long before each wake of a worker that makes no progress, and after N such wakes runsdone_cmdonce more; if it still fails, the goal escalates with the worker left running (or stops the worker withno_progress_action: "stop").worktree_progress: truecounts changes in the worktree as progress, so a worker that edits files without writing receipts is not woken.worktree_gate: trueskips rerunningdone_cmdwhile the worktree is unchanged since it last failed.goal.shtakes them asNO_PROGRESS_CAP,NO_PROGRESS_ACTION,NO_PROGRESS_BACKOFF_BASE_SECONDS,WORKTREE_PROGRESSandWORKTREE_GATE, all off by default. Seeskills/agent-deck/references/goal.md(#2718). - Goals take run budgets:
goal.sh --max-tokens,--max-turnsand--max-minutes, with defaults from[costs.run_budget](0is no cap). Token counts leave out cache reads. When an enforced daily, weekly or monthly budget refuses a goal's nudge, the goal pauses until the budget resets, with the worker left running and onlydone_cmdchecked meanwhile; a spent session budget ends the goal as failed and stops the worker.max_cycles: 0now means no cap (#2727).
Conductors and inbox
- Conductors and launched children are told that a message needing nothing (a plain "ok", "thanks" or a status ping that changes nothing) gets no reply; a
[HEARTBEAT]still does. Unedited conductor instruction files from v1.16.25 through v1.16.29 are updated in place, including the new/cmdrow (see Sessions); edited ones are left alone (#2721, #2734, #2754). - An inbox notice about a child's finished turn is withdrawn once the parent has read that turn's final reply with a plain
agent-deck session output <child>after the turn ended: it no longer wakes the parent, blocks its Stop, or shows in a digest or the heartbeat count, andinbox drainsays on stderr what it withdrew. Reads with-q,--json,--copy,--paneor a--max-tokenscut, reads during the turn, questions, errors and replies that were still in progress are never withdrawn.inbox statscountswakeups_withdrawnandrecords_withdrawn. The conductor's own result reads now use plainsession output, so they count (#2721, #2754).
Status
- A finished Claude session with a leftover
run_in_backgroundshell (a dev server, atail -f) no longer shows as running for hours. The shell counter in Claude's footer keeps a session running only while the transcript shows a recent background Bash launch, within[status] background_shell_hold(default"3m"); after that the session reads as waiting. A mixed counter with a monitor still reads as watching."0"restores the old rule. A build started in the background that runs longer than the hold shows as waiting while it still runs, so raise the hold if that matters (#2733). - An OpenCode 2.x session shows running while any of its sub-agents (and their sub-agents) is still working, and waiting once all of them are idle, as OpenCode 1.x sessions already did (#2511, #2716, reported by @3rwww1).
Watchers
- Without
-p, thewatchercommands now use the same profile as every other command:AGENTDECK_PROFILE, then the profile inferred fromCLAUDE_CONFIG_DIR, thendefault_profile. Before, they always used thedefaultprofile, so with any of those set a watcher was written todefaultand the selected profile's engine never ran it. Such a watcher now shows only with-p default. To move one:agent-deck -p default watcher listto find it,agent-deck -p default watcher stop <name>, move itswatcher.tomlaside, run the samewatcher createagain without-p, copy any hand edits back into the newwatcher.toml, thenwatcher start <name>. The stopped row stays indefault.watcher help,install-skill,importandroutestouch no profile state and do not resolve the profile (#2740, #2746, #2753, thanks @AndreIntelas). - A running watcher engine picks up changes within about 5 seconds without a restart: a watcher started, stopped or deleted from the CLI or TUI gains or loses its adapter (a stopped webhook frees its port), a changed
[source]table inwatcher.tomlrestarts only that watcher, and a changedclients.jsonswaps the routes. Other watchers are not touched. A file that does not parse leaves things as they were and is reported.agent-deck watcher reload [--json] [--timeout 15s]asks for a pass now, prints what changed and also retries watchers that failed to start; it exits 1 when something failed or the engine did not answer. The[watcher]table in config.toml is still read only at engine start (#2739, #2744, reported by @AndreIntelas).
Triggers
agent-deck triggerslists what wakes each session: launchd jobs and systemd user units that call agent-deck, heartbeats, chat bridges, watchers and other sessions, grouped by host, with what each one wakes, how often, its last fire, its health (ok,failing,dead-targetwhen the target names no session,never-fired, orunknownfor a remote that cannot answer) and how often it repeats itself. Filter with--session,--host <remote>|all,--kind,--health problemsand--all;--jsonfor scripts.triggers mute <id> [--for 24h]andunmutestop a trigger's sends reaching the pane without touching the host job (agent-deck never pauses launchd or systemd jobs),triggers run <id>starts a scanned unit once,triggers fires <id>shows recent fires andtriggers scanrescans. In the TUI,alt+topens the Triggers panel, or "What wakes " on a session row, and rows show a⚡Nbadge, red when a trigger is failing.session send --source <name>(orAGENTDECK_SOURCE) tags a send; without it the sender is inferred from launchd, systemd, cron or ssh, and agent-deck's own heartbeats, chat bridges and command center tag themselves. Fires are kept for 7 days and store only a cleaned, redacted first line of the message (#2702, #2738).
macOS app
- When
[macapp] pluginsis off,limitsandevents publish(andevents publish --help) name the exact command that turns it on (agent-deck config set macapp.plugins true --yes), and--jsonerrors carry ahintthe app can show as a consent prompt. Forremote <name> limitsthe hint says to run it on that remote, and its JSON carriesremote,remote_hostandremote_argvinstead ofargv.config set macapp.plugins truenow asks[y/N]in a terminal; declining, or running without a terminal or with--jsonand no--yes, exits 2 (CONSENT_REQUIREDin JSON) and leaves config.toml unchanged. No consent is needed when plugins is already on, or to turn it off (#2732, #2748). - The Mac app's own reply frames (
macapp.browser.responseandmacapp.app.focus, besidesmacapp.openandmacapp.open.ackas before) can be published without[macapp] plugins;macapp.browser.requeststill needs it.events followandevents publishtake--profile <name>to use another existing profile's log; an unknown name or a path is refused.remote <name> events publishsends one of those reply kinds to a remote with the payload on stdin, capped at 2 MiB, and the remote accepts it only for one of its own sessions; a remote on an older release reportsunsupported remote command "events publish".events publish --data-filerefuses payloads over 2 MiB. Every way a session is deleted (remove,session remove,worktree finish,conductor teardown, TUI and web delete) now publishesmacapp.session.closed, and moving a session to another profile publishes it on the old profile withreason: "moved"(#2726). agent-deck opentakes--wait <duration>to bound the wait for the app's answer (default 3s as before, at most 2m;--wait 0reports Queued at once) and--browser-profile <name>to pick a Browser panel profile in the app (a plain name of up to 64 characters). Each request carries a randomrequest_idthe app can use to drop a repeat (#2728).- Agents can drive their own tabs in the Mac app's Browser panel with
agent-deck browser open|tabs|close|click|type|read|shot, for local sessions and sessions on a remote, through the session's events log (no new socket). It is off by default and needs two settings:agent-deck config set browser.enabled true(oragent-deck config set profiles.<name>.browser.enabled truefor one profile, which overrides the global value either way) and[macapp] plugins = true(agent-deck config set macapp.plugins true --yes). Whilebrowser.enabledis off,browserand anyevents publish --kind macapp.browser.requestexit 2 (BROWSER_DISABLEDwith--json) and name the setting. Other exit codes: 0 ok, 1 error, 3 app not connected, 4 the owner is driving the browser. Page text, titles and URLs are cleaned of control and bidi characters before printing, and large results (page text, screenshots) pass through a private file instead of the log. On a host the app reaches as a remote,browser openandopenrefuse loopback, private, link-local, tailnet and other internal addresses before sending anything; the Mac app enforces its own address policy. Neither setting protects against an agent on the same host, which controls its own config; seedocs/macapp-browser.md.openandbrowserfind the calling session from its tmux session when--sessionand$AGENTDECK_INSTANCE_IDare unset (#2729).
Web
- With
[web] file_link_roots = ["~/projects", ...], afile://link in the web terminal under one of those directories opens the file in a new tab through the deck, with the usual token login, so it works from a phone too. Images are shown, other text (HTML and SVG included) is shown as plain text, and anything else downloads; only regular files up to 32 MiB inside a root can be read, symlinks that lead out are refused, and/or your home directory is not accepted as a root. Without roots (the default), outside them, or for a link naming another host, the click still copies the path.agent-deck file read <path>andagent-deck file roots(also throughremote <name>) apply the same rules, and the Settings drawer lists the roots (#2676, #2717, reported by @bautrey).
Fixes
- Web: Cmd-click (Ctrl-click on Linux and Windows) opens a terminal link in a session that captures the mouse, such as tmux with
mouse onor a fullscreen agent, where a plain click went to the app. The link opens on release through the usual confirm and trusted domains check, and dragging off cancels. Afile://link no longer opens a blank tab: it copies the path and shows a toast, unless it is under[web] file_link_roots(see Web). Other non-web links (javascript:,data:,vscode:,mailto:) open nothing and show a toast. Plain clicks, Option-drag selection and touch scrolling are unchanged (#2675, #2676, #2713, reported by @bautrey). - A sub-session row no longer shows a
▾Nfold badge or takesTab,l,→andhfor a fold that hid nothing, so those keys work on it again. Parent rows are unchanged (#2723). - The TUI no longer crashes on a very narrow preview pane (9 to 12 columns, such as a remote group row). Dialog hints, paths, cost dashboard titles, MCP names, badges and remote titles are cut by display width, so non-ASCII text is no longer split mid-character. A TUI that crashes or is killed by a signal now turns mouse reporting and the other terminal modes off, so the shell is usable afterwards (#2720).
- On macOS an idle TUI no longer uses most of a CPU core rereading every file in the shared hooks directory four times a second; it skips directories that have not changed. Linux is unaffected (#2731).
- Text sent to a session that ends in
;keeps the;; tmux took it as a command separator and dropped it (#2734).
Upgrading
- On a host the Mac app reaches as a remote,
agent-deck opennow refuses loopback, private, link-local, tailnet and other internal addresses. - Cost totals for Claude sessions can go down after the upgrade, as duplicate rows are removed; run
agent-deck costs syncto fold older rows at once. - Project MCP configs: a project
.mcp.json,.cursor/mcp.jsonoropencode.jsonthat is a symlink to a file outside the project (for example a monorepopkg/.mcp.json -> ../.mcp.json, or a dotfiles link) is now refused instead of written.add --mcpandlaunchfail with the error, the TUI and web MCP dialogs show it, and a restart starts the session without the MCP change. Replace the link with a real file or a link inside the project, or setmanage_mcp_json = false. - Scripts that run
config set macapp.plugins truewithout a terminal must add--yes. A declined prompt now exits 2. - Local builds: a machine on a
+localbuild of a version now auto-updates to the published release of that same version when auto-install is on. - Watchers created without
-pwhileAGENTDECK_PROFILE,CLAUDE_CONFIG_DIRordefault_profileselected another profile were stored indefaultand now show only with-p default; see Watchers for how to move one. - Claude-family sessions get the fan-out line in their identity block on their next start or restart; set
[launch] fanout_hint = falseto keep the old block. - Goal files with
max_cycles: 0now run with no cycle cap instead of failing at once. events publish --data-filerefuses payloads over 2 MiB.
Thanks to @AndreIntelas for the watcher and remote-agent profile fixes, to @Totopo27 (Luis Araya), whose #2759 was a correct fix for the same remote-agent profile issue (#2755) written in parallel and closed only because the reporter's own PR (#2756) landed first, to @kickinrad for replacing a realistic-looking example token in the sanitizer docs (#2767), and to @koenzhao, @bautrey, @renky, @3rwww1 and @AndreIntelas for their reports.
Full Changelog: v1.16.29...v1.16.30