Terminal session manager for AI coding agents...
Terminal session manager for AI coding agents. Built with Go + Bubble Tea.
Repo: github.com/asheshgoplani/agent-deck | Discord: discord.gg/e4xSs6NBN8
Run
agent-deck --versionfor your installed version. This skill targets v1.16.11+; most patterns work back to v1.7. See Backward Compatibility for what's gated behind โฅ1.16.11.
This skill includes helper scripts in its scripts/ subdirectory. When Claude Code loads this skill, it shows a line like:
Base directory for this skill: /path/to/.../skills/agent-deck
You MUST use that base directory path to resolve all script references. Store it as SKILL_DIR:
# Set SKILL_DIR to the base directory shown when this skill was loaded
SKILL_DIR="/path/shown/in/base-directory-line"
# Then run scripts as:
$SKILL_DIR/scripts/launch-subagent.sh "Title" "Prompt" --wait
Common mistake: Do NOT use <project-root>/scripts/launch-subagent.sh. The scripts live inside the skill's own directory (plugin cache or project skills folder), NOT in the user's project root.
For plugin users, the path looks like: ~/.claude/plugins/cache/agent-deck/agent-deck/<hash>/skills/agent-deck/scripts/
For local development, the path looks like: <repo>/skills/agent-deck/scripts/
# Launch TUI
agent-deck
# Create and start a session
agent-deck add -t "Project" -c claude /path/to/project
agent-deck session start "Project"
# Send message and get output
agent-deck session send "Project" "Analyze this codebase"
agent-deck session output "Project"
What agent-deck does, at the noun level (independent of which surface โ CLI / TUI / Web UI โ you reach it through). Each row maps to one or more commands, keystrokes, or screens. For full surface coverage and per-row known issues, see your conductor's CAPABILITIES.md (generated by the Self-Improvement workflow below).
| Capability | What it does | Surfaces |
|---|---|---|
| Manage sessions | Create, start, stop, restart, fork, send, output, remove a session | CLI โ ยท TUI โ ยท Web UI ๐ก |
| Sub-agent / worker spawning | agent-deck launch a child Claude session with parent linkage and inherited --add-dir |
CLI โ ยท TUI โช |
| Manage conductors | Set up long-lived orchestrators with their own profile + channel + heartbeat | CLI โ ยท TUI ๐ก |
| Manage groups | Move / delete groups; organize sessions hierarchically | CLI โ ยท TUI โ |
| Manage watchers | Install / configure event-driven adapters (Gmail, GitHub, ntfy) โ doorbell-not-messenger | CLI โ ยท TUI โ |
| Heartbeat orchestration | Cron / ScheduleWakeup feeding the conductor periodic system-state nudges | CLI โ |
| Dead-letter triage | inbox dead-letter list/show/retry/purge; Alt+D in the TUI |
CLI + TUI โ |
| Worktree workflows | --worktree to create isolated git-worktree-backed sessions for parallel branch work |
CLI โ |
| Channel routing | Telegram / Slack inbound delivery to the right conductor, with --channels per-session binding |
CLI โ |
| Attach MCPs | Per-session or global MCP plugin attach / detach / status, with optional pooling | CLI โ ยท TUI โ ยท Web UI โช |
| Attach skills | Pool-based on-demand skill loading; attached skills materialize in the runtime's project skill root | CLI โ ยท TUI โ |
| Session-metadata mutation | session set for claude-session-id, path, wrapper, channels, parent; session unset-parent |
CLI โ |
| State persistence | state.json + task-log.md + LEARNINGS.md + HANDOFF.md survive Claude Code compaction/restart |
CLI โ |
| GitHub pipeline oversight | Conductor-driven release flow: PR merge โ tag โ goreleaser โ release | CLI โ |
| Remote sessions | SSH-based remote register / list / attach across hosts | CLI โ |
| Session sharing | Export / import a Claude conversation for handoff between developers | CLI โ |
| Consult another agent | Launch a Codex / Gemini sub-agent for a second opinion | CLI โ |
| Profile-scoped operation | -p <profile> separation between personal and work auth |
CLI โ ยท TUI โ |
| Self-improvement | Analyze your conductor's own transcripts โ surface bugs / patterns / capabilities, file GH issues with privacy guards | CLI โ |
Status legend: โ verified, ๐ก partial, ๐ด known broken, โช unknown. To update for your own machine, see Self-Improvement below.
The table above is what agent-deck does. This one is what the CLI inside a session can do โ agent-deck launches one per session (-c claude|codex|gemini). A conductor uses this to pick the right tool for a child; a launched child uses it to know its own powers without being told. This is a capability map, not a manual โ run <cli> --help for exact flags. Verified 2026-06 against claude 2.1.x, codex-cli 0.137, gemini 0.45.
| In-session capability | claude (Claude Code) | codex | gemini |
|---|---|---|---|
| Multi-agent fan-out inside one session | โ
Agent tool (parallel subagents, each its own context window; run_in_background) and Workflow tool (deterministic JS: agent()/pipeline()/parallel() over item lists, structured-output schemas, phases) |
โ native subagents/helpers when exposed by the active Codex runtime; use Agent Deck when work needs separately managed visible sessions | โ not exposed โ use separately managed sessions when needed |
| Skills | โ
Skill tool + agent-deck pool skills (~/.agent-deck/skills/pool/; new installs $XDG_DATA_HOME/agent-deck/skills/pool/, default ~/.local/share/agent-deck/skills/pool/) |
โ
agent-deck skill attach materializes project skills in .agents/skills |
โ
gemini skills |
| MCP servers | โ
claude mcp / --mcp-config; agent-deck mcp attach |
โ
codex mcp; also runs as a server (codex mcp-server) |
โ
gemini mcp, --allowed-mcp-server-names |
| Built-in code review | โ
ultrareview (cloud multi-agent) + /code-review skill |
โ
codex review / codex exec review --uncommitted |
via prompt only |
| Plan / read-only mode | --permission-mode plan |
-s read-only |
--approval-mode plan |
| Autonomy / sandbox | --permission-mode acceptEdits|bypassPermissions; config dangerous_mode |
-s read-only|workspace-write|danger-full-access; --dangerously-bypass-approvals-and-sandbox |
--approval-mode auto_edit|yolo, -s/--sandbox |
| Structured output | --json-schema, --output-format json |
codex exec json event stream |
-o json|stream-json |
| Image input (multimodal) | paste / Read an image |
-i/--image (attach to prompt; not image generation) |
multimodal prompt |
| Apply a diff to working tree | Edit/Write tools | codex apply (git-apply the last agent diff) |
Edit tools |
| Git worktree | -w/--worktree |
via agent-deck --worktree |
-w/--worktree |
| Plugins / extensions / hooks / channels | plugin, --plugin-dir/-url, hooks, --channels |
codex plugin |
extensions, hooks |
| Resume / fork conversation | -r/--resume, --fork-session |
codex resume / codex fork |
--resume, --session-file |
| Local / OSS models | 3P providers (Bedrock/Vertex) | --oss, --local-provider lmstudio|ollama |
gemini gemma routing |
Choosing the -c tool for a child: default to claude when its Agent and Workflow tools fit the task. Reach for codex for a fast non-interactive second opinion, code review (codex review), sandboxed exec, or native in-process helpers when exposed by that runtime; use Agent Deck when work needs a separately managed visible session. Reach for gemini for a third opinion or large-context reads.
agent-deck powers every child also has (independent of CLI): agent-deck mcp attach/detach then session restart; launch further child or peer sessions (-no-parent for peers); load pool skills on demand; session send to talk to sibling sessions. See Sub-Agent Launch, Peer (Root) Sessions vs Sub-Agents, MCP Management.
When to go inline vs Agent tool vs Workflow (for claude children) is owned by the shared conductor template's Delegation section (~/.agent-deck/conductor/conductor-claude.md) and is not duplicated here: 1 task = inline; a few independent subtasks = Agent tool; a sweep / audit / matrix = Workflow โ and always adversarially verify findings with a second agent told to refute.
| Command | Purpose |
|---|---|
agent-deck |
Launch interactive TUI |
agent-deck add -t "Name" -c claude /path |
Create session |
agent-deck launch . -c claude --account <name> |
Create and start a session under a named account slot |
agent-deck accounts [--json] |
List configured named account slots |
agent-deck session start/stop/restart <name> |
Control session |
agent-deck session send <name> "message" |
Send message |
agent-deck session send <name> --message-file <file> |
Send message from file (- = stdin); no shell quoting. Also on launch/session start |
agent-deck session output <name> |
Get bounded, ANSI-clean last response (JSON/quiet/copy preserve full source) |
agent-deck session children --json |
Child sessions' live status + asserted completions (non-blocking, read-only) |
agent-deck session current [-q|--json] |
Auto-detect current session |
agent-deck session fork <name> |
Fork Claude/OpenCode/Pi/Codex/Oh My Pi conversation |
agent-deck session switch-account <name> <account> |
Switch Claude account, conversation follows |
agent-deck mcp list |
List available MCPs |
agent-deck mcp attach <name> <mcp> |
Attach MCP (then restart) |
agent-deck status |
Quick status summary |
agent-deck add --worktree <branch> |
Create session in git worktree |
agent-deck try <name> |
Scratch session in a dated experiment folder |
agent-deck worktree list |
List worktrees with sessions |
agent-deck worktree cleanup |
Find orphaned worktrees/sessions |
agent-deck feedback |
Submit feedback (opens rating prompt + optional comment) |
agent-deck session context <name> |
Context inspector: what is in a session's context window and what it costs |
Status: โ running | โ waiting | โ idle | โ error
Every session agent-deck launches (claude, codex, pi, gemini) already carries a short identity block in its instructions: session id, title, tool, group, profile, account, parent session, project path, the core CLI commands, agent-deck session current --json for the live record, and the completion sentinel. Custom --cmd sessions get the same block via $AGENTDECK_IDENTITY_FILE. A child therefore does not need to be told who it is or how to reach its parent; a prompt only has to state the task. Opt out per session with --no-identity or globally with [launch] inject_identity = false; gemini additionally needs its identity folder trusted (see documentation/HARNESS_IDENTITY.md).
Use when: anyone asks "what is in my/this session's context", "why is context so full",
"what can I clean up", or you want to audit a child session's overhead before dispatching
heavy work. Same data as the TUI C overlay โ full CLI parity by design, so agents can
use every feature themselves.
agent-deck -p <profile> session context <name> # overview: gauge + categories
agent-deck -p <profile> session context <name> --tab breakdown --all # every item, ranked, with ids + levers
agent-deck -p <profile> session context <name> --item <id> # ONE item: provenance, lever, verbatim text
agent-deck -p <profile> session context <name> --tab verify # the arithmetic, measured anchor to the digit
agent-deck -p <profile> session context <name> --json # machine-readable; provenance on every figure
agent-deck -p <profile> session context <name> --strict # exit 3 if the report breaks its own invariants
agent-deck -p <profile> session context <name> --capabilities # what this harness can report at all
Reading the output:
--verify types /context into the LIVE session to compare against the harness's own
accounting. It mutates the session: confirmation required, use sparingly, never on a busy session.--verify always asks first; --yes skips the prompt for CI.Agents may run read-only sweeps freely (list --json โ context per session) to find
bloated sessions; report findings, never edit another session's files without its owner.
Use when: a session needs to message another session, a parent needs to collect child
results, anyone asks "how do I notify the conductor", "did my child finish", "how do I read
another session's answer", or a send seems to have vanished. Every command below was
verified against the installed binary (v1.16.11-rc.2). From any non-interactive shell
(cron, systemd, hooks) always pass -p <profile> explicitly, or session resolution
silently uses the default profile and the target is "not found".
| Channel | Direction | Command | Guarantee |
|---|---|---|---|
| send | any โ any live session | session send <id> "msg" |
Best-effort keystrokes into the pane (or, opt-in, Claude's own messaging socket). --json carries a stable 3-way confirmation field (confirmed/unknown/failed) โ read that, not the 11-value delivery diagnostic (submitted, queued, delivered, unverified, queued_socket, line_too_long, menu_open, pane_gone, typed_not_submitted, no_evidence, send_failed, composer_blocked, socket_write_failed). NOT durable: if the send fails or the sender dies, the message is gone. |
| output | read a session's last reply | session output <id> -q |
Read-only transcript snapshot; non-consuming; --pane returns the tmux pane capture instead. Default text is ANSI-stripped and capped at --max-tokens (default 25000) with the full output kept on disk; --json/-q/--copy carry the full source. |
| children | parent reads its child fleet | session children --json, --follow [--until-done] |
Read-only; merges live status with the completion ledger; explicitly does NOT clear the inbox. |
| inbox drain | child completions โ parent | inbox drain self --json |
THE durable channel: fsync'd append + WAL, at-least-once delivery with exactly-once effects (turn-fingerprint dedup), survives crashes and restarts. Last-wins PER CHILD: intermediate events are dropped by design. Single-profile only. Draining consumes. |
| transition events | daemon โ parent's inbox | automatic (requires parent_session_id) |
Only running โ waiting/error/idle edges fire; deduped (90s + 2h windows); sessions with no parent link are WARN-logged once and DROPPED. |
| [DONE] sentinel | worker asserts completion | worker prints ===AGENTDECK_DONE=== status=ok summary=... |
Idempotent per distinct completion; the only trustworthy "finished" signal (see Completion sentinel section). |
| heartbeat | bridge โ conductor | bridge-driven send --wait -q |
Lossy by design: skipped while the conductor is busy, never queued; only fires when waiting>0 or error>0. |
| handoff | Claude โ Codex context copy | session handoff <id> [--json --out file] |
One-shot read-only transcript copy (32k-char tail budget); no ongoing link afterwards. |
agent-deck -p <profile> session send <id> "single line message" # default: waits for readiness, verifies
agent-deck -p <profile> session send <id> "answer now?" --wait -q --timeout 300s # send + wait + raw reply, one call
agent-deck -p <profile> session send <id> "nudge" --no-wait -q # fire immediately (heartbeats/nudges)
agent-deck -p <profile> session send <id> "done ping" --defer-if-busy --defer-timeout 30m # hold until idle; DROPS at timeout
git diff | agent-deck -p <profile> session send <id> --message-file - # long/multiline payload safely from stdin
agent-deck -p <profile> session send <id> "draft text" --draft # type without submitting
agent-deck -p <profile> session output <id> -q # read last response (raw text)
agent-deck -p <profile> session output <id> --pane # pane capture, ANSI stripped + capped (fallback when transcript read refuses)
agent-deck -p <profile> session children --json # child fleet snapshot + parent id
agent-deck -p <profile> session children --follow --until-done # JSONL event stream, exits when all children terminal
agent-deck -p <profile> inbox drain self --json # FIRST step of every heartbeat; consumes exactly-once
agent-deck -p <profile> session show <id> --json # has parent_session_id + substate (list --json does NOT)
agent-deck -p <profile> session handoff <id> --json # build cross-tool handoff prompt, read-only
agent-deck -p <profile> session search "term" --json --limit 5 # substring search across Claude transcripts
send result is evidence-graded, not binary. With --json read the delivery
field: submitted / unverified / typed_not_submitted / no_evidence / send_failed.
Exit 0 plus delivery:"unverified" is NOT proof the turn started (#1793: a boundary-sized
Codex send can pass every check and never submit). Confirm with session show --json
(status flips to running) or output when the answer matters.session output <id> --pane) before resending.send in every mode dies with the sender. Childโparent
completions ride the inbox; everything else (parentโchild, peerโpeer, cross-profile)
is fire-and-forget keystrokes. Cross-profile parents are a terminal drop.--defer-if-busy drops on timeout with a non-zero exit; the message is NOT queued
for later. Treat a defer timeout as undelivered and decide explicitly.send exit 1,
and a backgrounded wrapper swallows the failure (silent loss). For multiline payloads
use --message-file <file> or --message-file - (stdin) โ that path is safe.--no-wait. On a freshly launched session --no-wait
can leave the message typed but not submitted. Wait ~3s, then send an idempotent
Enter: tmux send-keys -t "$(agent-deck -p <profile> session show --json <id> | jq -r .tmux_session)" Enter.
Note --no-wait deliberately disables auto-resend (double-send protection), so this
manual fallback is on you.running target queues your keystrokes into its
composer mid-turn. Use --defer-if-busy, or poll session show --json until
status leaves running. Bound every repeated signal: 3 identical sends/nudges
with no state change means stop and change tactic, not send a fourth.substate, not just status. error + substate auth-401 means dead
credentials (restarting will NOT fix it; the fleet HOLDs these); substate
model-unavailable means the model is down. Never restart-loop either. substate
is omitempty: absent means none, not healthy-confirmed.list --json omits parent_session_id. To see linkage use
session show --json <id> or session children. Verify every launch created the
linkage you expect; children born without a parent link get NO transition events.session set-title-lock <id> on prevents that). Always -p
from non-interactive senders.session output refuses with "colliding transcript" (one claude_session_id
claimed by two live instances, seen after restart/fork), fall back to
session output <id> --pane โ read-only and always available./cmd sends to a freshly restarted child are ignored.Use when: User says "launch sub-agent", "create sub-agent", "spawn agent"
$SKILL_DIR/scripts/launch-subagent.sh "Title" "Prompt" [--mcp name] [--wait]
The script auto-detects current session/profile and creates a child session.
| Mode | Command | Use When |
|---|---|---|
| Fire & forget | (no --wait) | Default. Tell user: "Ask me to check when ready" |
| On-demand | agent-deck session output "Title" |
User asks to check |
| Blocking | --wait flag |
Need immediate result |
This section covers one child (launch + one of the three retrieval modes). For a fleet โ several children in parallel, supervised non-blockingly from the parent โ load the sibling fleet skill instead. It covers parented fan-out, polling live status and asserted completions via agent-deck session children --json (plus the push variant --follow --until-done), answering children stuck in waiting, and the grouping/--parent pitfalls.
| Task Type | MCPs |
|---|---|
| Web research | exa, firecrawl |
| Code documentation | context7 |
| Complex reasoning | sequential-thinking |
When the prompt asks the worker to Edit or Write any existing file, include an explicit prelude-read as the first step. Claude Code's tool guard rejects Edit/Write before Read of the same path, and conductor-spawned workers hit this mid-task (#968) when their prompt jumps straight into the change. The interruption forces the worker to backfill reads inside its main loop, breaking flow and burning cycles.
Template skeleton โ bake this into every worker prompt that mutates code:
## Step 0 โ Prelude reads
Read every file you intend to Edit/Write below. Read calls are cheap
and do NOT count against scope discipline; they prevent tool-guard
interruptions mid-task. Skip only for paths that will be created fresh.
Files to read first:
- <path/to/file/you/will/edit>
- <path/to/other/file/you/will/edit>
## Step 1 โ Investigation
โฆ
## Step 2 โ Implementation
โฆ
Rules:
Read it first, even if the worker "knows" the contents.Write to a path that does not yet exist): no prelude needed โ the guard only applies to modifications.A worker's Stop hook fires at the end of every turn, so "waiting" never means "done" โ the conductor would otherwise have to poll RESULTS files / gh pr / session output to know a task actually finished. Instead, instruct every worker to assert completion by ending its final turn with a single machine-greppable line:
===AGENTDECK_DONE=== status=<ok|fail> summary=<one line to end of line>
agent-deck detects this on the Stop edge (scanning the transcript tail) and emits a distinct event to the parent:
[DONE] Child '<name>' (<id>) finished: status=ok summary=<...>
instead of the generic [EVENT] โฆ is waiting. This is by-construction: completion is asserted by the only party that knows (the worker), not inferred from terminal cosmetics.
Bake this line into every worker prompt's final instruction:
## Final step โ assert completion
When the task is fully done, print exactly this as the last line of your final message:
===AGENTDECK_DONE=== status=ok summary=<what you accomplished, one line>
Use status=fail if you could not complete it; put the blocker in the summary.
Notes:
Stop โ does not re-emit. A genuinely new completion (different summary) emits again.Stop edges, so existing waiting behavior is unchanged.status) are ignored, not guessed at.Use when: User says "consult with codex", "ask gemini", "get codex's opinion", "what does codex think", "consult another agent", "brainstorm with codex/gemini", "get a second opinion"
IMPORTANT: You MUST use the --tool flag to specify which agent. Without it, the script defaults to Claude.
# Consult Codex (MUST include --tool codex)
$SKILL_DIR/scripts/launch-subagent.sh "Consult Codex" "Your question here" --tool codex --wait --timeout 120
# Consult Gemini (MUST include --tool gemini)
$SKILL_DIR/scripts/launch-subagent.sh "Consult Gemini" "Your question here" --tool gemini --wait --timeout 120
DO NOT try to create Codex/Gemini sessions manually with agent-deck add. Always use the script above. It handles tool-specific initialization, readiness detection, and output retrieval automatically.
$SKILL_DIR/scripts/launch-subagent.sh "Title" "Prompt" \
--tool codex|gemini \ # REQUIRED for non-Claude agents
--path /project/dir \ # Working directory (auto-inherits parent path if omitted)
--wait \ # Block until response is ready
--timeout 180 \ # Seconds to wait (default: 300)
--mcp exa # Attach MCP servers (can repeat)
| Tool | Flag | Notes |
|---|---|---|
| Claude | --tool claude |
Default, no flag needed |
| Codex | --tool codex |
Requires codex CLI installed |
| Gemini | --tool gemini |
Requires gemini CLI installed |
--wait: polls until the agent responds, then returns the full output--wait: returns immediately, check output later with agent-deck session output "Title"# Code review from Codex
$SKILL_DIR/scripts/launch-subagent.sh "Codex Review" "Read cmd/main.go and suggest improvements" --tool codex --wait --timeout 180
# Architecture feedback from Gemini
$SKILL_DIR/scripts/launch-subagent.sh "Gemini Arch" "Review the project structure and suggest better patterns" --tool gemini --wait --timeout 180
# Both in parallel (consult both, compare answers)
$SKILL_DIR/scripts/launch-subagent.sh "Ask Codex" "Best way to handle errors in Go?" --tool codex --wait --timeout 120 &
$SKILL_DIR/scripts/launch-subagent.sh "Ask Gemini" "Best way to handle errors in Go?" --tool gemini --wait --timeout 120 &
wait
After getting the response, remove the consultation session:
agent-deck remove "Consult Codex"
# Or remove multiple at once:
agent-deck remove "Codex Review" && agent-deck remove "Gemini Arch"
The default โ sub-agent linkage: agent-deck launch and agent-deck add, when invoked from inside an existing agent-deck session, automatically link the new session as a child of the calling session (sets parent_session_id, inherits the parent's group when -g is omitted, and grants --add-dir to the parent's project path). This is usually what you want for short-lived work sessions (plan / verify / release / consult).
When the default is wrong โ root-level peer sessions: if you are creating a session that should stand independently at the root โ a peer conductor, a standalone project session, a session that should outlive the current one, or anything that semantically is NOT a child of the calling session โ pass the -no-parent flag.
| Use case | Parent linkage | Flag |
|---|---|---|
| Plan / impl / verify worker for the current task | โ child | (default) |
| Consultation (codex / gemini / research) | โ child | (default) |
| Another conductor (root-level peer) | โ child | -no-parent |
| Project session unrelated to current work | โ child | -no-parent |
| Session intended to outlive the caller | โ child | -no-parent |
# Root-level peer conductor, no parent linkage:
agent-deck launch ~/projects/foo -t "conductor-foo" -g "conductor" -c claude -no-parent -m "..."
# Verify after spawn:
agent-deck list --json | jq '.[] | select(.title=="conductor-foo") | .parent_session_id'
# Must print: null
Symptoms you created a sub-agent when you wanted a peer:
parent_session_id is non-null in list --json outputpane_start_command contains --add-dir <caller's path> even though you gave it a different project pathFix for an already-created sub-agent: stop + remove the session, re-launch with -no-parent. There is no in-place un-parent flag.
Note on the launch-subagent.sh script: that script is specifically designed to create sub-agents (the name says so). It does NOT support -no-parent. For peer sessions, skip the script and invoke agent-deck launch -no-parent directly.
A conductor is a persistent agent-deck session that orchestrates other sessions. It watches the rest of your sessions, auto-responds when confident, escalates to you when ambiguous, and optionally pairs with a remote channel (Telegram or Slack) so you can talk to it from your phone.
Use this section when the user says "conductor", "set up a conductor", "monitor sessions", "telegram bot for agent-deck", "slack bot for agent-deck", or "remote control my sessions".
# Create a conductor in the default profile
agent-deck conductor setup ops --description "Ops monitor"
# Create on a specific profile (work/personal/etc.)
agent-deck -p work conductor setup infra --description "Infra watcher"
# Use a non-Claude agent for the conductor itself
agent-deck conductor setup review --agent codex --description "Codex reviewer"
# Provide custom env (e.g., third-party Anthropic-compatible endpoint)
agent-deck conductor setup glm-bot \
-env ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \
-env ANTHROPIC_AUTH_TOKEN=<token>
# Status across all conductors
agent-deck conductor status
# List configured conductors
agent-deck conductor list
Each conductor lives at ~/.agent-deck/conductor/<name>/ (new installs: $XDG_DATA_HOME/agent-deck/conductor/<name>/, default ~/.local/share/agent-deck/conductor/<name>/) with its own CLAUDE.md (or AGENTS.md for Codex), meta.json, state.json, and task-log.md. Multiple conductors per profile are supported and each can pair with its own bot.
Channels are how a conductor talks to you remotely. Each conductor pairs one-to-one with its own bot โ bots are not shared. agent-deck conductor setup interactively walks you through Telegram or Slack pairing during creation.
Key constraints:
settings.json. Per-session activation happens via the channels field on the conductor's session record.<channel-state-dir>/.env (chmod 600). Never committed to git.TELEGRAM_STATE_DIR and can leak duplicate pollers on the same bot token, causing 409 conflicts.See: docs/conductor/ for the full quickstart, channel setup, multi-conductor setups, and lifecycle commands.
| Key | Action |
|---|---|
j/k or โ/โ |
Move up/down |
h/l or โ/โ |
Collapse/expand groups |
Enter |
Attach to session |
| Key | Action |
|---|---|
n |
New session |
r |
Rename session |
R |
Restart session (reloads MCPs) |
m |
MCP Manager |
s |
Skills Manager |
f/F |
Fork Claude/OpenCode/Pi/Codex/Oh My Pi session |
d |
Delete |
A |
Archive (stops tmux, hides from default list) |
Shift+U |
Unarchive (does not auto-start tmux) |
M |
Move to group |
Mouse drag does not select text โ the TUI holds the terminal in mouse reporting
mode so clicks, scrolling and the divider drag work. Hold Shift while dragging
(Option in iTerm2) for native selection, or use these:
| Key | Copies |
|---|---|
c |
Last AI response |
C |
Session info (repo / path / branch) |
V |
Visible terminal text, links included |
Y |
A fenced code block (picker if several) |
| Key | Action |
|---|---|
/ |
Local search |
G |
Recall search over every indexed conversation (Claude, Codex, pi, Gemini, OpenCode, Hermes); footer notice and local search when [recall] enabled = false |
!@#& |
Filter by status (running/waiting/idle/error) |
^ |
View archived sessions |
| Key | Action |
|---|---|
? |
Help overlay |
Ctrl+Q |
Detach (keep tmux running) |
Ctrl+E |
Open feedback dialog |
q |
Quit |
Default: Do NOT attach MCPs unless user explicitly requests.
# List available
agent-deck mcp list
# Attach and restart
agent-deck mcp attach <session> <mcp-name>
agent-deck session restart <session>
# Or attach on create
agent-deck add -t "Task" -c claude --mcp exa /path
Scopes:
.mcp.json in project, affects only that session--global) - Claude config, affects all projectsWhen working on a feature that needs isolation from main branch:
# Create session with new worktree and branch
agent-deck add /path/to/repo -t "Feature Work" -c claude --worktree feature/my-feature --new-branch
# Create session in existing branch's worktree
agent-deck add . --worktree develop -c claude
# List all worktrees and their associated sessions
agent-deck worktree list
# Show detailed info for a session's worktree
agent-deck worktree info "My Session"
# Find orphaned worktrees/sessions (dry-run)
agent-deck worktree cleanup
# Actually clean up orphans
agent-deck worktree cleanup --force
| Use Case | Benefit |
|---|---|
| Parallel agent work | Multiple agents on same repo, different branches |
| Feature isolation | Keep main branch clean while agent experiments |
| Code review | Agent reviews PR in worktree while main work continues |
| Hotfix work | Quick branch off main without disrupting feature work |
agent-deck try)Use when: the user wants a throwaway playground, a quick experiment, or a scratch repo to dry-run something โ "spin up a scratch session", "try this out somewhere disposable", "make a playground".
# Find-or-create a dated experiment folder and start a session in it
agent-deck try redis-cache # โ <experiments-dir>/2026-07-29-redis-cache/
agent-deck try rds # Fuzzy-matches an existing experiment (e.g. redis-cache)
agent-deck try myproject -c gemini # Non-default tool
agent-deck try myproject --no-session # Create/find the folder only
agent-deck try scratch --sandbox # Run the session in a Docker sandbox
agent-deck try --list [query] # List (or fuzzy-search) existing experiments
The argument is an experiment name, not a prompt. try finds or creates <experiments-dir>/<YYYY-MM-DD>-<name>/, reuses an existing session for that path if one exists, and otherwise creates one in the experiments group and starts it.
The base directory is configurable โ important when your machine only trusts certain roots for agent workspaces:
[experiments]
directory = "~/code/tries" # Default: ~/src/tries
date_prefix = true # YYYY-MM-DD- prefix on folder names
default_tool = "claude" # Tool when -c is omitted
Note: try creates a plain folder, not a git repo โ run git init in it first if the experiment needs one.
Watchers listen for inbound events (webhooks, push notifications, GitHub events, Slack messages) and route them into conductor sessions. Use them when the user says "set up a watcher", "listen for webhooks", "route GitHub events to my conductor", "forward ntfy notifications", or similar.
Four adapter types are supported:
| Type | Required flag | Typical use |
|---|---|---|
webhook |
--port |
Generic HTTP listener |
github |
--secret |
GitHub repo webhooks with HMAC verification |
ntfy |
--topic |
ntfy.sh push notifications |
slack |
--topic |
Slack (via Cloudflare Worker bridge) |
agent-deck watcher create <type> --name <name> <adapter-flags...>
agent-deck watcher start <name>
agent-deck watcher list # health + events/hour
agent-deck watcher test <name> # synthetic event (verify routing)
Full conversational setup flow is available as a separate skill:
agent-deck watcher install-skill watcher-creator
After running the install command, read the installed watcher-creator/SKILL.md to walk the user through adapter selection, required settings, and configuring the effective watcher data dir's clients.json routing (${XDG_DATA_HOME:-$HOME/.local/share}/agent-deck/watcher/clients.json for new users; legacy ~/.agent-deck/watcher/clients.json when existing watcher state is present).
See agent-deck watcher --help for the full command surface and per-adapter examples.
Use when: user says "self-improve", "analyze my conductor", "what bugs are we hitting", "file issues from my usage", or asks the conductor to learn from past conversations.
A pipeline that analyzes a conductor's own Claude Code conversation transcripts and surfaces actionable signal:
CLAUDE.mdOutput lives at the conductor root and is regenerated on each run:
~/.agent-deck/conductor/<name>/
โโโ FINDINGS.md # raw synthesis of the latest run
โโโ CAPABILITIES.md # curated inventory (you edit; survives runs)
โโโ analysis-manifest.json # tracking โ sha + line counts + analyzer session IDs
โโโ analysis/ # scripts, prompts, distilled transcripts, per-transcript reports
SKILL_DIR="<base directory shown when this skill was loaded>"
SELFIMP="$SKILL_DIR/scripts/self-improvement"
# Phase 1 โ distill all transcripts for one conductor (Python, no LLM, ~1 min)
mkdir -p ~/.agent-deck/conductor/<name>/analysis/distilled
for f in ~/.claude*/projects/-home-*-agent-deck-conductor-<name>/*.jsonl; do
sid=$(basename "$f" .jsonl | cut -c1-8)
python3 "$SELFIMP/distill.py" "$f" ~/.agent-deck/conductor/<name>/analysis/distilled/$sid.md
done
# Phase 2 โ analyze + synthesize (spawns agent-deck sub-sessions, ~30 min, ~$5)
cp -r "$SELFIMP/prompts" ~/.agent-deck/conductor/<name>/analysis/
bash "$SELFIMP/run-analyzers.sh" # paced, resumable via manifest
# Phase 3 โ file issues from FINDINGS.md (interactive; never auto-files)
bash "$SELFIMP/file-issues.sh"
Three layers run before anything leaves the box: regex sanitize โ AI sanitizer session โ independent AI auditor session. The auditor must verdict SAFE_TO_SHARE before the filer will submit. Each layer covers the others' blind spots (regex catches tokens / IPs / paths; AI catches contextual names; auditor catches what the first two missed with fresh eyes). Human review is non-negotiable โ the filer prints the exact gh command and waits for [f] before running it.
For the full architecture, output schemas, lessons learned from real runs, and per-script reference, see references/self-improvement.md.
Use when: user says "pursue this", "set a goal", "make it work until done", "nudge the agent", "stop me having to message it again", or describes wanting an agent to keep working autonomously toward a specific goal without manual re-prompting.
A complementary layer on top of Self-Improvement. Self-improvement is post-hoc analysis. Goal is the live mechanism that prevents the kinds of stalls self-improvement keeps surfacing โ specifically the FINDINGS pattern where a conductor's hourly cron fires 18 times with identical [STATUS] replies and no actual progress.
Three entities, never collapsed:
| Entity | Job | Restriction |
|---|---|---|
| Worker | Take one bounded step per cycle, write a progress receipt | May NOT decide it's done. May NOT escalate. |
| Verifier | An external shell command โ runs the done-condition independently | NOT an LLM. NOT the worker's self-assessment. |
| Manager | Small Python daemon (cron'd) โ runs the verifier, reads receipts, nudges the worker, escalates to user when stuck | NOT involved in doing the work |
Separating these three concerns is what prevents the "agent keeps reporting status but never finishes" failure mode the FINDINGS captured.
Examples that work:
gh release view v1.6.0 -R asheshgoplani/agent-deck --json publishedAt | jq -e '.publishedAt != null'gh pr view 890 -R asheshgoplani/agent-deck --json mergedAt | jq -e '.mergedAt != null'test -s /tmp/report.csv && [ "$(wc -l < /tmp/report.csv)" -gt 100 ]Examples that DON'T work:
The full spec is in references/goal.md. For early use, follow Phase 1:
~/.agent-deck/goals/<id>.json (schema in the deep-dive doc).agent-deck goal \
--goal "Ship agent-deck v1.6.0" \
--done 'gh release view v1.6.0 --json publishedAt | jq -e ".publishedAt != null"' \
--check-every 5m \
--max-idle 1h \
--escalate-after 3 \
--max-cycles 24
agent-deck goal list # active goals + state
agent-deck goal show <id> # full JSON dump
agent-deck goal tail <id> # tail the worker's task-log.md
agent-deck goal cancel <id> # stop the worker
agent-deck goal resume <id> "<hint>" # send context-rich hint, reset nudge counter
For the full design โ three-entity model, registry schema, worker contract prompt, manager loop pseudocode, nudge generator, escalation bundle, done-condition guidelines, failure modes, implementation phases, and the verification this closes the FINDINGS 18-hour stall โ see references/goal.md.
Use when: A conductor or worker reports any non-trivial completion claim โ "PR is merge-ready", "release shipped", "tap updated", "comment posted", "goal done", "bulk drain complete". Treat all such claims as unverified until an independent verifier session has hit ground truth.
For ANY non-trivial done-claim, spawn a separate Claude session (not the same conductor, not the same worker) whose job is to re-derive the claim from primary sources. The verifier MUST hit:
Same-session reviewers carry the same blind spots that produced the claim. A separate session re-reads the world from scratch.
| Done-claim | Independent verifier MUST run |
|---|---|
| "PR is merge-ready" | gh pr checkout <N> && go test -race ./... AND gh pr view <N> --json mergeable,statusCheckRollup |
| "release shipped" | gh release view <tag> --json assets AND probe each download URL (HTTP 200, non-empty body, correct content-type) |
| "brew tap updated" | gh api repos/<owner>/homebrew-tap/contents/Formula/agent-deck.rb โ confirm the SHA + URL match the new release |
| "comment posted" | gh issue view <N> --comments (or gh pr view <N> --comments) and string-match the expected body |
| "goal worker done" | The manager's done_cmd must return rc=0 AS PART OF the verify cycle โ the worker's self-report alone never closes a goal |
| "bulk drain complete" | Spawn a read-only AUDIT worker that enumerates the residual open items (open issues, open PRs, pending releases) and diffs them against the claimed-closed set |
Use launch-subagent.sh from a different session than the one making the claim (peer session, not child of the claimant). Block with --wait so the conductor gets the verifier's answer before reporting up:
$SKILL_DIR/scripts/launch-subagent.sh "Verify PR #885" \
"Independently verify PR #885 is merge-ready. Run \`gh pr checkout 885 && go test -race ./...\` and \`gh pr view 885 --json mergeable,statusCheckRollup\`. Report PASS only if BOTH succeed; otherwise report the exact failing tests + the mergeable state." \
--wait --timeout 600
Key points:
--wait is mandatory. Fire-and-forget defeats the purpose โ the claim ships before the verifier answers.PR #885 over-claim. A worker review reported "merge-ready; clean diff". Independent local re-test (gh pr checkout 885 && go test -race ./...) caught 2 failing tests. The reviewer had grepped the diff for obvious issues but never ran the suite. Lesson: code-review confidence โ test pass.
ux-rethink-path verdict false-positive. The ux-rethink-path worker reported "merge as-is; clean diff" for #885. User asked for a second look; a fresh peer verifier caught the same 2 failing tests the original worker missed. Lesson: reviewer + author in the same session-tree share priors. A peer session re-derives.
Goal framework "metronome wakes". The conductor's hourly wake cycles were firing [STATUS] replies with no actual work โ the wake-loop had degenerated into a heartbeat instead of a do-work loop. User had to flag it manually. Lesson: a worker that hasn't run a ground-truth check since the last wake is not a working worker, it's a metronome. Bake the priority-0 check (below) into the wake template so this can't recur.
The goal worker contract prompt at scripts/goal/prompts/worker.md now begins each wake with PRIORITY 0: before reporting status or taking the next bounded step, run one ground-truth verifier check against any non-trivial claim made in the previous wake's receipt. If no claim was made last wake, skip priority 0 and go to step 1.
This converts a metronome wake into a do-work wake. See the worker template for the exact wording the cycle uses.
For trivial mechanical actions where the action IS its own verification (and the claim is "I did X"):
agent-deck list reporting current sessions โ list IS verificationThe verifier requirement attaches to claims about external mutable state: PRs, releases, comments, deployments, bulk operations.
Use when: anyone asks "is agent-deck healthy", "why is the deck slow", "did that completion event get lost", or you're deploying a local build to a remote ahead of a release.
agent-deck health --json --since 1h # per-process CPU/RSS/FDs/goroutines vs performance budgets, no data leaves the host
agent-deck inbox dead-letter list --json # inspect records that failed to route (list/show only โ no retry/purge)
agent-deck remote update dev --from-build /path/to/local/dist # push a verified local build to a remote, no release needed
health reports against fixed budgets (status_pass_ms_exclusive, open_fds_exclusive, tmux_calls_per_session, remote_poll_ms_exclusive); use --since to widen the history window when a regression is intermittent.inbox dead-letter list|show is diagnostic-only in this release โ there is no retry or purge subcommand (both are explicitly rejected). Recovering a dead-lettered record means fixing the underlying routing issue and re-draining, not resubmitting the record itself.remote update --from-build <dir> is for shipping a verified local three-platform build (darwin/arm64, linux/amd64, linux/arm64) to a remote before it's published as a release โ same checksum/version verification and downgrade guard as a normal remote update.[ui.remote_preview]/[ui.header] share one field vocabulary: version, sessions_by_status, harnesses, load, memory, disk, last_poll, and the opt-in accounts (named Claude account slots with live 5h/7d usage, one aligned row per slot in the preview, read from each slot's local quota cache โ agent-deck hooks install wires the feed) and ssh (who is connected to the host over SSH right now, per user) โ see Configuration/config-reference.md.Use when: you want a session to remember what it was for, you are finishing a task and want the outcome findable later, or you need to find what an earlier conversation (Claude, Codex, pi, Gemini, OpenCode or Hermes) did. Details: recall skill, docs/recall.md.
agent-deck add . -c claude --hint purpose="fix flaky auth test" --ticket SB-412 --tag auth # also on launch
agent-deck session annotate <id> --decision "clock skew" --outcome worked --tag clock-skew
agent-deck session annotate --self --note-stdin < summary.md # an agent, on its own session
agent-deck recall search "clock skew" --since 30d --json # needs [recall] enabled = true
agent-deck recall search "retry budget" --harness codex --json # one harness; --profile work narrows Claude
agent-deck recall show <session> --turns 20 && agent-deck recall open <session>
agent-deck recall context <session> --tier brief --into current # hand a past conversation to THIS session (any harness)
agent-deck recall search "retry budget" --all-remotes --json # federated: every remote searches its own index
agent-deck remote <host> session annotate <id> --outcome worked # hints on a remote session
agent-deck mcp attach <session> recall # the built-in MCP server (recall mcp)
Hints are single-valued per key (setting again replaces), tags are a set; all of it lives in the profile's state.db and survives any index rebuild. The index (recall backfill|sweep|status|sessions|search|show|open|gc|rebuild) is off by default, covers Claude transcripts of every profile plus Codex, pi, Gemini, OpenCode and Hermes, never runs a daemon or watcher, refuses batch work while a session is busy, and stays fresh through the Claude Stop hook, session stop, worker_done and the daemon's turn-end edge (each appends one line to recall/queue.jsonl; the Stop hook also indexes its own file within 150 ms). The TUI G key is the same search. recall open resumes Claude conversations and starts any bound session; other harnesses are searchable, not resumable. Phase 4: recall context <session> --into current delivers a past conversation (card, brief or excerpt under a token budget) to the calling session through session send; every sweep derives lost_time, session_kind and outcome lines (recall enrich drains the rest; stale ones are marked); recall search --remote <host>/--all-remotes federates over SSH and stores nothing (an older remote is one line + exit 1); card sync (export/pull/import) is off unless [recall] remote_cards = true; recall mcp is an MCP server mcp list offers as recall. session search is unchanged.
File: $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml; legacy ~/.agent-deck/config.toml still honored)
[claude]
config_dir = "~/.claude-team" # Custom Claude profile
dangerous_mode = true # --dangerously-skip-permissions
use_chrome = false # --chrome
use_teammate_mode = false # --teammate-mode tmux
extra_args = ["--agent", "reviewer"]
[logs]
max_size_mb = 10 # Max before truncation
max_lines = 10000 # Lines to keep
[mcps.exa]
command = "npx"
args = ["-y", "exa-mcp-server"]
env = { EXA_API_KEY = "key" }
description = "Web search"
See config-reference.md for all options.
| Issue | Solution |
|---|---|
| Session shows error | agent-deck session start <name> |
| MCPs not loading | agent-deck session restart <name> |
| Flag not working | Put flags BEFORE arguments: -m "msg" name not name -m "msg" |
If something isn't working, create a GitHub issue with context:
# Gather debug info
agent-deck version
agent-deck status --json
cat ~/.config/agent-deck/config.toml | grep -v "KEY\|TOKEN\|SECRET" # Sanitized config (legacy: ~/.agent-deck/config.toml)
# Create issue at:
# https://github.com/asheshgoplani/agent-deck/issues/new
Include:
tail -100 ~/.agent-deck/logs/agentdeck_<session>_*.logSee troubleshooting.md for detailed diagnostics.
Going beyond a bug report to a fix? agent-deck ships a dedicated contributor skill that mirrors the repo's PR intake gate and the maintainer's review machine, so an agent that follows it passes intake on the first try and scores well on all four review lenses (correctness, security, fit, intent).
Load it from the repo checkout:
# In an agent-deck clone
cat .github/skills/agent-deck-contributor/SKILL.md
It walks the full loop and enforces the bar: understand and reproduce the issue first, capture the human's actual ask verbatim (it goes in the PR body), one scoped problem per PR, a test that FAILS without your change, self-check locally before opening (.github/skills/agent-deck-contributor/scripts/self-check.sh), disclose the AI model that wrote the change, and respond directly to review verdicts. Run tests sandboxed โ never against a real home directory:
HOME=$(mktemp -d) XDG_CONFIG_HOME= XDG_DATA_HOME= XDG_CACHE_HOME= go test ./...
Share Claude sessions between developers for collaboration or handoff.
Use when: User says "share session", "export session", "send to colleague", "import session"
# Export current session to file (session-share is a sibling skill)
$SKILL_DIR/../session-share/scripts/export.sh
# Output: ~/session-shares/session-<date>-<title>.json
# Import received session
$SKILL_DIR/../session-share/scripts/import.sh ~/Downloads/session-file.json
See: session-share skill for full documentation.
Move a session โ conversation included โ to a different Claude account (work/personal/client) and continue exactly where it left off.
Use when: User says "switch account", "move this conversation to my other account", "continue this session on account X", "this session should use the
One-time setup โ name each account in $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml; the target profile must already be logged in: CLAUDE_CONFIG_DIR=<dir> claude โ /login):
[profiles.personal.claude]
config_dir = "~/.claude"
[profiles.work.claude]
config_dir = "~/.claude-team"
In the TUI: the New Session dialog's Claude options carry an Account row
(โ/โ or Space to cycle; inherit keeps the conductor/group/env chain), and
the Edit Session dialog (Shift+P) carries an account row that runs the full
switch โ conversation migration and --resume restart included โ on save, after a
"Switch Account?" confirmation. Both rows are hidden when no accounts are configured.
Commands:
# Inspect the account names available to add/launch/switch-account
agent-deck accounts
# Create and start a new session directly under one named account
agent-deck launch . -c claude --account <account>
# Full flow: stop โ copy conversation into the target account โ set account โ restart with --resume
agent-deck session switch-account <session> <account>
# Skip the restart (e.g. switch several sessions, restart later)
agent-deck session switch-account <session> <account> --no-restart
# Equivalent low-level form โ also migrates the conversation; restart required
agent-deck session set <session> account <account>
How it works / guarantees:
.jsonl is copied into <target-config-dir>/projects/<encoded-path>/ โ the old account keeps its copy; a conflicting file in the target is backed up as .bak-<timestamp> first. The session id does not change.claude --resume is a pure file lookup, so the restarted session continues with full history under the new account's auth.CLAUDE_CONFIG_DIR=<dir> claude plugin enable telegram@claude-plugins-official for channel owners).Make a whole group/conductor use the account going forward: set [groups.<name>.claude].config_dir / [conductors.<name>.claude].config_dir to the same dir in config.toml โ new sessions there spawn on that account; switch-account is what carries existing conversations over.
session start -m "Hello" name (not name -m "Hello")session restart after mcp attachagent-deck manages interactive agent sessions. It is not a supervisor for always-on daemons or network listeners, and reaching for it as one leads to subtle failures. Before wrapping a long-lived service (a webhook listener, an SSE bridge, a claude remote-control server, any daemon) in a deck session, check this boundary:
loginctl enable-linger + launch_in_user_scope=true, but the failure mode remains).session restart has conversation semantics, not daemon semantics. For a Claude session it rebuilds the pane command around claude --resume <id> โ correct for resuming a chat, wrong for "bring my listener back exactly as it was". Custom-command sessions re-run their stored wrapper, but registry drift on custom commands is a known trap (#956, #911).Rule of thumb: always-on listeners and daemons belong under the OS supervisor (launchd on macOS, systemd on Linux โ the headless web --no-tui daemon itself is run that way, see #1452); agent-deck owns the interactive sessions and workers. When a session merely talks to a service, supervise the service outside the deck and keep the session disposable.
Friction points discovered during real usage. Work around them per the patterns below.
session send --no-wait can leave prompts typed-but-not-submittedOn a freshly-launched Claude session, agent-deck session send --no-wait <id> "..." may paste the message into the input buffer before Claude is fully ready, leaving it TYPED but not SUBMITTED. Classic race.
Workaround (always safe):
agent-deck -p <profile> session send <id> "..." --no-wait -q
sleep 3
# Get the tmux session name and send Enter to submit
TMUX=$(agent-deck -p <profile> session show --json <id> | jq -r .tmux_session)
tmux send-keys -t "$TMUX" Enter
The Enter is idempotent โ if already submitted, it's just a no-op newline. Use this pattern every time you session send --no-wait to a freshly-launched session.
Alternative: omit --no-wait so the built-in 60s readiness wait kicks in before submitting.
text file busy)If /usr/local/bin/agent-deck is a symlink to a build artifact and the binary is currently running (any tmux session, any daemon), a direct cp over it fails with Text file busy.
Workaround โ move-then-copy (keeps running processes on the old inode):
INSTALL=$(which agent-deck)
TARGET=$(readlink -f "$INSTALL")
go build -ldflags "-X main.Version=X.Y.Z" -o /tmp/agent-deck-new ./cmd/agent-deck
mv "$TARGET" "$TARGET.old"
cp /tmp/agent-deck-new "$TARGET" && chmod +x "$TARGET"
agent-deck --version # verify
rm "$TARGET.old"
Kernel tracks inodes, not names. Running processes keep a reference to the renamed inode; new invocations resolve through the original name to the new inode.
-c "claude <subcommand> ..." silently rewritten โ injected flags demote the subcommand (#1800)This is a known bug, not intended behaviour. Tracked as #1800; a fix is in flight. Delete this entire section once that fix ships โ the workaround below is a stopgap, not the supported way to run claude subcommands.
Passing a claude subcommand as the session command โ e.g. -c "claude remote-control --name X" or -c "claude mcp serve" โ does not run the command you gave. Tool detection splits it into claude + extra args and re-appends the extras after agent-deck's injected flags, so the pane runs:
claude --session-id <uuid> --dangerously-skip-permissions remote-control --name X
The subcommand becomes a positional argument of plain interactive claude; no Remote Control server (or MCP server, etc.) ever starts. This affects any claude subcommand. See #1800.
Workaround โ wrap in a shell so tool detection treats the command as opaque:
agent-deck add -t rc-server -c "bash -c 'exec claude remote-control --name X'" /path
The wrapped form injects nothing and runs the command verbatim. Trade-off: the session is opaque to claude session-id tracking / resume-on-restart โ fine for server-style subcommands, which have no conversation to resume. Extra flags (e.g. -c "claude --model opus") are unaffected โ the wrapper-suffix path handles those correctly.
If ~/.agent-deck/skills/sources.toml (or other config files) were copied verbatim from a macOS machine, paths like /Users/<name>/ won't exist on Linux (should be /home/<user>/). The symptom: agent-deck skill list returns "No skills found" while the pool directory is clearly populated.
Check & fix:
grep -n "/Users/" ~/.agent-deck/skills/sources.toml
# If any matches, substitute the Linux home path:
sed -i "s|/Users/<mac-user>|$HOME|g" ~/.agent-deck/skills/sources.toml
For a session to receive Telegram/Discord/Slack messages as conversation turns (not just as MCP tool calls), it MUST be started with --channels <plugin-id>. Use the first-class field:
# At creation (preferred):
agent-deck -p personal add --channel plugin:telegram@claude-plugins-official -c claude -t my-bot /path
# Or after creation, then restart:
agent-deck -p personal session set my-bot channels plugin:telegram@claude-plugins-official
agent-deck -p personal session restart my-bot
The channels field persists and every session start / session restart rebuilds the claude invocation with --channels. Do NOT rely on .mcp.json telegram entries โ those load the plugin as a regular MCP (tools only), not a channel (inbound delivery).
Note โ v1.7.0 display bug: agent-deck session show --json <id> currently omits the channels field (fix pending). agent-deck list --json | jq '.[] | select(.id==<id>)' shows it correctly. Data is persisted fine regardless.
Telegram's Bot API getUpdates is single-consumer per bot token. If N Claude sessions all load the telegram plugin, N bun pollers race for messages โ deliveries land in whichever wins, not where you want them.
Correct topology: exactly ONE session loads the telegram channel plugin (normally the conductor, via --channels at start-time). All other sessions should NOT have telegram in their enabled plugins.
Disable globally: in ~/.claude/settings.json:
"enabledPlugins": {
"telegram@claude-plugins-official": false
}
Enable per-session: via --channel on the specific session that should receive messages. See "Channel subscription" above.
Debug: pgrep -af "bun.*telegram" | wc -l should return 1. Anything higher means a race. Kill extras: pkill -f "bun.*telegram" then restart only the intended session.
Supported topology โ enforce this on every conductor host:
--channels plugin:telegram@claude-plugins-official. This is the only supported activation path for a conductor bot.TELEGRAM_STATE_DIR is injected exclusively via [conductors.<name>.claude].env_file in $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml). The env file sources deterministically on both fresh-start and --resume spawns.enabledPlugins."telegram@claude-plugins-official" in the profile settings.json must be absent or false. Global enablement makes every claude subprocess (including child agents) load the plugin.Codified anti-patterns โ agent-deck v1.7.22 emits warnings for these:
| Anti-pattern | Code | Why it breaks |
|---|---|---|
enabledPlugins."telegram@claude-plugins-official" = true in profile settings |
GLOBAL_ANTIPATTERN |
Every claude process loads the plugin, including every child agent the conductor spawns. Each one starts a bun telegram poller. |
Global enablement AND --channels plugin:telegram@... on the same session |
DOUBLE_LOAD |
The plugin loads twice in one claude process. Two bun pollers race on one bot token and Telegram rejects with 409 Conflict. |
session set wrapper "TELEGRAM_STATE_DIR=... {command}" |
WRAPPER_DEPRECATED |
Works on the resume path; silently fails on fresh-start due to bash -c argv splitting. The env var never reaches claude, so the plugin falls back to the default state dir and two conductors collide. Use env_file instead. |
Relying on .mcp.json telegram entries for inbound delivery |
โ | .mcp.json loads the plugin as an MCP server (tool-use only). Inbound message โ conversation-turn delivery requires --channels. |
| Using the same bot token for multiple concurrent sessions | โ | getUpdates is single-consumer per token. |
Assuming an empty TELEGRAM_STATE_DIR is fine |
โ | The plugin falls back to ~/.claude/channels/telegram/; any DM approval there leaks across unrelated conductors. |
Verifying steady state (conductor host):
pgrep -af 'bun.*telegram' | grep -v grep | wc -l # expect: exactly one per conductor bot
for PID in $(pgrep -f 'bun.*telegram.*start'); do
echo "PID=$PID TSD=$(tr '\0' '\n' < /proc/$PID/environ | grep ^TELEGRAM_STATE_DIR= | cut -d= -f2-)"
done
# Each PID must show a distinct TELEGRAM_STATE_DIR; collisions indicate env_file is not being sourced.
When agent-deck emits a โ GLOBAL_ANTIPATTERN / DOUBLE_LOAD / WRAPPER_DEPRECATED warning, the problem is in your topology, not in agent-deck. Fix the profile settings or the conductor env_file; the warning is a leading indicator of the 409-Conflict symptom that follows minutes-to-hours later.
These were surfaced by mining real conductor transcripts (see Self-Improvement). Each links to its filed GH issue.
| Symptom | Workaround | Issue |
|---|---|---|
agent-deck rm driven by xargs -P N reports โ Removed but rows persist |
Sequential while read id; do agent-deck rm "$id"; done โ never xargs -P for agent-deck mutations |
#961 |
session send --wait --timeout 300s still times out at 80s if recipient is busy |
Poll until ready: until [ "$(agent-deck session show --json $id | jq -r .status)" = "waiting" ]; do sleep 10; done |
#957 |
Conductor restart wipes Claude history when claude_session_id is empty / "none" |
Use tool: claude not tool: shell for conductors so JSONL-resume applies |
#956 |
Transition-notifier replays events for removed sessions out of ~/.agent-deck/inboxes/<id>.jsonl |
Truncate the inbox file by hand after rm |
#962 |
Feedback dialog never shows for users with feedback_enabled:false (single boolean gates everything) |
Manual flip in feedback-state.json to reopt-in |
#967 |
| All sessions die on SSH logout (tmux server in login-session cgroup) | loginctl enable-linger on the host + launch_in_user_scope=true |
#958 |
Parallel agent-deck launch cascade โ swap thrash โ workers + conductor die |
Sequential launches; cap parallelism; don't reach for vm.overcommit_memory=2 (worsens it) |
#964 |
Orphaned context7 MCP procs (PPID=1) accumulating; pkill -f context7-mcp from inside the conductor self-immolates |
Guard with $$ check: grep -q $$ <(pgrep -f "<pat>") || pkill -f "<pat>" |
#965 |
launch-subagent.sh puts children in parent's conductor group instead of project group |
Always pass -g <project-group> explicitly |
#972 |
Bare slash commands sent via session send ignored on a freshly restarted child |
Wrap conversationally: "Please run /cmd โฆ" |
#966 |
.mcp.json plugin version pins go stale after plugin upgrade |
After /mcp reload, rewrite .mcp.json from current plugin spec |
#960 |
Cron heartbeat NEED: lines repeat unchanged for 12-21h with no auto-retire |
After 3 repeats, change tactic โ escalate explicitly or spawn a different worker | #971 |
agent-deck launch -m "<rich text>" short-flag parser misroutes โ text after -m becomes positional [path] |
Use long-form flags: --message, --title, --group, --parent |
(filed in batch) |
CLI verb inconsistency: session update --no-parent, group remove, launch -parent all rejected |
Correct verbs: session unset-parent, group delete, launch does not accept -parent (it's automatic) |
#974 |
See the Self-Improvement section for how these were discovered and how to surface more from your own conductor's transcripts.
This skill is read by whatever deck version the reading session is on, including a remote running an older build โ check with agent-deck --version before assuming a feature below exists.
| Needs โฅ1.16.11 | On an older deck |
|---|---|
session send confirmation field, queued_socket/delivered/unverified delivery values, send_transport = "auto" |
Read delivery and submitted only; treat any non-submitted exit-0 outcome as "sent, not confirmed" and verify with session show --json or output |
session context (context inspector), TUI C key |
No context inspector at all โ audit context by reading the instruction files and skills directly |
agent-deck health |
No local health/budget reporting โ watch for slowness manually (top, agent-deck status) |
remote update --from-build, remote list --check --json version fields |
remote update from published releases only; remote list without live version drift detection |
remote sessions --json bare-array/--with-errors split, remote drain |
Older builds may return null instead of [] for zero sessions, and lack remote drain/inbox export entirely (a remote drain against one reports a version error pointing at remote update) |
inbox dead-letter list|show |
No dead-letter inspection โ a lost completion is invisible; fall back to reading transcripts / RESULTS.md for the child's actual outcome |
[ui.remote_preview]/[ui.header] accounts field |
Older builds show version/sessions/load only, no per-account usage in the header or remote panel |
session children --follow, --until-done |
Fall back to the until-loop in Fanning out several children? polling --json on an interval |
shell as accepted tool alias |
Use a custom -c "bash -c '...'" command instead โ same effect, more typing |
Harness identity injection (AGENTDECK_IDENTITY_FILE, append-system-prompt for claude/pi, developer-instructions override for codex, trust-gated context dir for gemini) |
A launched session gets none of this โ state its session id, tool, and how to reach its parent explicitly in the launch prompt |
Any command not in this table (session start/stop/send/output, mcp attach, session set-parent, worktrees, groups) has worked unchanged since well before this refresh.
User guides (full how-to, in the repo):
Reference (shipped with this skill):
session children)recall context --into current, derived summaries, federated remote search, card sync and the MCP server