github asheshgoplani/agent-deck v1.16.30

3 hours ago

Agent Deck v1.16.30

Terminal session manager for AI coding agents.

Installation

Homebrew (recommended):

brew install asheshgoplani/tap/agent-deck

Quick Install:

curl -fsSL https://raw.githubusercontent.com/asheshgoplani/agent-deck/main/install.sh | bash

Go Install:

go install github.com/asheshgoplani/agent-deck/cmd/agent-deck@v1.16.30

What's new in 1.16.30

Highlights

  • Run Claude slash commands such as /model or /compact on a Claude session safely with agent-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 with agent-deck config set browser.enabled true and agent-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 triggers and alt+t in 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_roots open 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 = true stops 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 .cursor directory link, or opencode.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 .cursor directory link) is refused before anything is created. The error names the link and its target and says that manage_mcp_json = false in 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>.tmp name, and the .mcp.json lock file is checked too. Config files in your home directory (~/.claude.json, ~/.cursor/mcp.json) still follow links as before (#2737, #2742, #2749, #2757).
  • openclaw commands 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 by openclaw status and openclaw 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 with session set <id> claude-session-id still work (#2730).

Updates

  • A local build (<version>+local...) is now offered the published release of the same version, so update --check, the auto-install and the releases-behind count move a machine running 1.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, doctor and the TUI's "restarted into" notice warn when an older agent-deck earlier on PATH (for example a /usr/local/bin/agent-deck link 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: the rm command (with sudo when needed) or an export PATH=... line, or the package manager for a Homebrew, Nix, asdf, mise, snap or /usr/bin copy. agent-deck never removes the copy itself. update --check --json and doctor --json list such copies under shadowed_by (empty when PATH is clean). update --unattended does 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 and launch -capabilities --json without 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, for claude, codex, gemini and opencode (#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 start and 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 nested hermes running 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 --attach opens 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 of session 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 60s waits 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: /model checks the model Claude reports, /compact the Compacted notice, /exit stops 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 with default_changed if 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 with unknown_model and lists the entries, and a bare /model is an argument error. A switched model is kept as the session's model, so restart and fork use it; /model default clears it. /login and /logout are refused, and only Claude sessions are supported for now. --json prints the result, with a code on failure (an unknown session exits 2); --dry-run types nothing; --background runs 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 as cmd ... without the leading slash (#2734, #2736).
  • Claude-family sessions (claude and 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 (also config set launch.fanout_hint false) leaves it out, and --no-identity or context_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 through default_profile or AGENTDECK_PROFILE pushes session changes to the TUI within a few seconds again, instead of only at the next 15-second fetch: remote-agent now watches the state DB of the profile it serves. If the host's default_profile changes 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 (also config 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), --queue and 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 with budget exceeded, or code: "BUDGET_EXCEEDED" and resets_at under --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-budget on session send, session start -m and launch -m sends 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 sync folds 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: N waits twice as long before each wake of a worker that makes no progress, and after N such wakes runs done_cmd once more; if it still fails, the goal escalates with the worker left running (or stops the worker with no_progress_action: "stop"). worktree_progress: true counts changes in the worktree as progress, so a worker that edits files without writing receipts is not woken. worktree_gate: true skips rerunning done_cmd while the worktree is unchanged since it last failed. goal.sh takes them as NO_PROGRESS_CAP, NO_PROGRESS_ACTION, NO_PROGRESS_BACKOFF_BASE_SECONDS, WORKTREE_PROGRESS and WORKTREE_GATE, all off by default. See skills/agent-deck/references/goal.md (#2718).
  • Goals take run budgets: goal.sh --max-tokens, --max-turns and --max-minutes, with defaults from [costs.run_budget] (0 is 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 only done_cmd checked meanwhile; a spent session budget ends the goal as failed and stops the worker. max_cycles: 0 now 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 /cmd row (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, and inbox drain says on stderr what it withdrew. Reads with -q, --json, --copy, --pane or a --max-tokens cut, reads during the turn, questions, errors and replies that were still in progress are never withdrawn. inbox stats counts wakeups_withdrawn and records_withdrawn. The conductor's own result reads now use plain session output, so they count (#2721, #2754).

Status

  • A finished Claude session with a leftover run_in_background shell (a dev server, a tail -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, the watcher commands now use the same profile as every other command: AGENTDECK_PROFILE, then the profile inferred from CLAUDE_CONFIG_DIR, then default_profile. Before, they always used the default profile, so with any of those set a watcher was written to default and 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 list to find it, agent-deck -p default watcher stop <name>, move its watcher.toml aside, run the same watcher create again without -p, copy any hand edits back into the new watcher.toml, then watcher start <name>. The stopped row stays in default. watcher help, install-skill, import and routes touch 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 in watcher.toml restarts only that watcher, and a changed clients.json swaps 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 triggers lists 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-target when the target names no session, never-fired, or unknown for a remote that cannot answer) and how often it repeats itself. Filter with --session, --host <remote>|all, --kind, --health problems and --all; --json for scripts. triggers mute <id> [--for 24h] and unmute stop 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 and triggers scan rescans. In the TUI, alt+t opens the Triggers panel, or "What wakes " on a session row, and rows show a ⚡N badge, red when a trigger is failing. session send --source <name> (or AGENTDECK_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] plugins is off, limits and events publish (and events publish --help) name the exact command that turns it on (agent-deck config set macapp.plugins true --yes), and --json errors carry a hint the app can show as a consent prompt. For remote <name> limits the hint says to run it on that remote, and its JSON carries remote, remote_host and remote_argv instead of argv. config set macapp.plugins true now asks [y/N] in a terminal; declining, or running without a terminal or with --json and no --yes, exits 2 (CONSENT_REQUIRED in 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.response and macapp.app.focus, besides macapp.open and macapp.open.ack as before) can be published without [macapp] plugins; macapp.browser.request still needs it. events follow and events publish take --profile <name> to use another existing profile's log; an unknown name or a path is refused. remote <name> events publish sends 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 reports unsupported remote command "events publish". events publish --data-file refuses payloads over 2 MiB. Every way a session is deleted (remove, session remove, worktree finish, conductor teardown, TUI and web delete) now publishes macapp.session.closed, and moving a session to another profile publishes it on the old profile with reason: "moved" (#2726).
  • agent-deck open takes --wait <duration> to bound the wait for the app's answer (default 3s as before, at most 2m; --wait 0 reports 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 random request_id the 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 (or agent-deck config set profiles.<name>.browser.enabled true for one profile, which overrides the global value either way) and [macapp] plugins = true (agent-deck config set macapp.plugins true --yes). While browser.enabled is off, browser and any events publish --kind macapp.browser.request exit 2 (BROWSER_DISABLED with --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 open and open refuse 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; see docs/macapp-browser.md. open and browser find the calling session from its tmux session when --session and $AGENTDECK_INSTANCE_ID are unset (#2729).

Web

  • With [web] file_link_roots = ["~/projects", ...], a file:// 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> and agent-deck file roots (also through remote <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 on or 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. A file:// 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 ▾N fold badge or takes Tab, l, → and h for 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 open now 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 sync to fold older rows at once.
  • Project MCP configs: a project .mcp.json, .cursor/mcp.json or opencode.json that is a symlink to a file outside the project (for example a monorepo pkg/.mcp.json -> ../.mcp.json, or a dotfiles link) is now refused instead of written. add --mcp and launch fail 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 set manage_mcp_json = false.
  • Scripts that run config set macapp.plugins true without a terminal must add --yes. A declined prompt now exits 2.
  • Local builds: a machine on a +local build of a version now auto-updates to the published release of that same version when auto-install is on.
  • Watchers created without -p while AGENTDECK_PROFILE, CLAUDE_CONFIG_DIR or default_profile selected another profile were stored in default and 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 = false to keep the old block.
  • Goal files with max_cycles: 0 now run with no cycle cap instead of failing at once.
  • events publish --data-file refuses 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

Don't miss a new agent-deck release

NewReleases is sending notifications on new releases.