Profile
Back to NewsBack
GitHub Trending 33 min
Reader Mode
asheshgoplani/agent-deck: Terminal session manager for AI coding agents. One TUI for Claude, Gemini, OpenCode, Codex, and more.

asheshgoplani/agent-deck: Terminal session manager for AI coding agents. One TUI for Claude, Gemini, OpenCode, Codex, and more.

8 hours ago

Agent Deck Logo

Agent Deck

Your AI agent command center

GitHub Stars</a> Downloads</a> Go Version</a> License</a> Platform</a> Latest Release</a> Discord</a>

Install . Quick Start . Features . Conductor . Docs . Discord . FAQ

Agent Deck is mission control for your AI coding agents. Running Claude Code on ten projects, OpenCode on five more, another agent somewhere in the background? One terminal shows every session — running, waiting, or done — and one keystroke switches between them. Groups, search, forking, git worktrees, cost tracking, and a phone-controlled conductor keep a whole fleet manageable.

https://github.com/user-attachments/assets/e4f55917-435c-45ba-92cc-89737d0d1401

Contributing: start here

agent-deck is actively maintained by Ashesh, and it welcomes both contributors and co-maintainers. PRs here don't sit: every incoming PR is validated (applied, built, tested) within about a day, and good ones merge in the next release batch. Recent releases have shipped dozens of community fixes.

Beyond one-off PRs, we're looking for 1-2 regular co-maintainers: people who want to own an area (a tool integration, the TUI, the web view, CI) and help triage and review. The validation pipeline does the heavy lifting; maintainers steer.

To get started:

  1. Read CONTRIBUTING.md: how the pipeline works, what makes a PR land fast, and the house rules.
  2. Contributing with an AI agent? Point it at .github/INTAKE.md, the machine-readable intake spec — or the agent-deck-contributor skill, which walks an agent (or you) through building, testing, and shaping a clean PR.
  3. Comment on an issue before starting work so we can tell you if someone (human or agent) is already on it.
  4. Look for issues labeled good first issue: curated to be small and self-contained.
Looking for something specific to work on? docs/ROADMAP.md lists ideas and follow-ups that are real but not yet scheduled.

If you've had a couple of PRs land here and want to help steer, open an issue titled "maintainer: your area". We'd love the help.

Installation

Works on: macOS, Linux, Windows (WSL)

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

Then run: agent-deck

Other install methods

Homebrew

brew install asheshgoplani/tap/agent-deck

Go

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

From Source

git clone https://github.com/asheshgoplani/agent-deck.git && cd agent-deck && make install

Uninstalling

agent-deck uninstall              # Interactive uninstall
agent-deck uninstall --keep-data  # Remove binary only, keep sessions

See Troubleshooting for full details.

Shell completion

agent-deck completion prints a completion script for commands and subcommands (agent-deck se → session, agent-deck session st → start/stop), plus live resource names where it matters — agent-deck remote update offers your configured remotes, agent-deck session set-parent offers session titles at both positions, and agent-deck -p offers your profiles.

# bash
echo 'source <(agent-deck completion bash)' >> ~/.bashrc

zsh

echo 'source <(agent-deck completion zsh)' >> ~/.zshrc

fish

agent-deck completion fish > ~/.config/fish/completions/agent-deck.fish

Open a new shell (or re-source the config file) afterwards.

Quick Start

agent-deck                        # Launch TUI
agent-deck add . -c claude        # Add current dir with Claude
agent-deck session fork my-proj   # Fork a supported session
agent-deck session send my-proj --message-file task.md # Send a multiline prompt
agent-deck session ref my-proj reviewer # Type a reference to my-proj into reviewer (not sent)
agent-deck session remove my-proj # Remove stopped/errored session from registry (transcripts preserved)
agent-deck mcp attach my-proj exa # Attach MCP to session
agent-deck skill attach my-proj docs --source pool --restart # Attach skill + restart
agent-deck web                    # Start web UI on http://127.0.0.1:8420
⚠️ Changed in v1.9.55, extended after v1.16.5: in the new-session dialog (n), Enter advances to the next field on every row — Name, Tool, Model, Reasoning effort, Path, checkboxes and each Claude Options row — and only the trailing [ Create session ] button (or Ctrl+S from any field) creates the session, so walking the form with Enter never launches a session before you have chosen the model, path or options. ↓/Space open the model list. The dialog also remembers your last-used tool. Restore the old Enter-creates-from-any-row behavior with [ui].new_session_enter_advances = false.

Key Shortcuts

From the main menu, o and Shift+Q open a message composer above the footer. Press Enter to submit, Ctrl+J for a newline, or Esc to cancel. Ctrl+X, then E, opens the draft in $VISUAL / $EDITOR (fallback: vi); returning from the editor restores the draft without sending it. Queueing uses native Claude/OpenCode 2 queueing where available and waits for the current turn to finish on other commands. OpenCode 2 queueing uses its session API and requires a detected session ID; unconfirmed delivery is reported without retrying or falling back to terminal keys. The main-menu composer currently supports local sessions only, not remote-session rows.

| Key | Action | |-----|--------| | Enter | Attach to session | | Ctrl+Q | Detach from session | | n | New session | | f / F | Fork (quick / dialog) | | A / Shift+U | Archive / unarchive session | | ^ | Show archived sessions | | m | MCP Manager | | o | Send a message to the selected session (a busy Claude session queues it, no interrupt) | | Shift+Q | Queue a message after the current turn | | s | Skills Manager | | $ | Cost Dashboard | | M | Move session to group | | S | Settings | | / / G | Search / Global search | | r / R | Rename / Restart session | | d | Delete | | b | Re-run worktree setup script | | E | Container shell (sandboxed sessions) | | c / B | Copy last AI response / session info (repo, path, branch) | | C | Context inspector (what the session is sent, ranked by cost) | | V / Y | Copy visible terminal text / a fenced code block | | Shift+drag | Select text natively (Option+drag in iTerm2) — see below | | ? | Full help |

Why can't I drag-select text? The TUI holds the terminal in mouse
reporting mode so clicks, wheel scrolling and the divider drag work, which
means your terminal never sees a drag as a selection. Hold Shift while
dragging (Option in iTerm2) to bypass it, or use the c / B / V / Y
copy keys above. Full detail in
Terminal shortcuts.

See TUI Reference for all shortcuts and CLI Reference for all commands.

Quickstart: orchestrate a fleet of AI agents

Five minutes from zero to a Telegram bot that watches every Claude session you have running.

# 1. Create a Telegram bot via @BotFather, grab the token + your user ID from @userinfobot.

2. Run the wizard — it sets up the conductor, bridge daemon, and heartbeat in one shot.

agent-deck conductor setup work --description "Work fleet" agent-deck session start conductor-work

3. Message your bot: /status

That's it. From now on every other agent-deck session you run is supervised by a single "conductor" session that answers routine questions, escalates the interesting ones to your phone, and never lets a waiting worker rot.

Two short guides to read next:

state files, lifecycle, remote channel setup (Telegram/Slack/Discord/Mattermost), gotchas. (GitHub events, gmail, ntfy pushes, meetings) can wake the conductor up.

!Fleet topology: phone → conductor → child sessions, with watchers on the side

Features

MCP Manager

Attach MCP servers without touching config files. Need web search? Browser automation? Toggle them on per project or globally. Agent Deck handles the restart automatically.

  • Press m to open, Space to toggle, Tab to cycle scope (LOCAL/GLOBAL), type to jump
  • Define your MCPs once in $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml), then toggle per session — see Configuration Reference

Skills Manager

Attach/detach Claude skills per project with a managed pool workflow.

  • Press s to open Skills Manager for a Claude session
  • Available list is pool-only ($XDG_CONFIG_HOME/agent-deck/skills/pool, default ~/.config/agent-deck/skills/pool) to keep attach/detach deterministic
  • Apply writes project state to .agent-deck/skills.toml and materializes into .claude/skills
  • Type-to-jump is supported in the dialog (same pattern as MCP Manager)

Declarative groups

Declare groups in config.toml so they exist on startup. Set create = true to ensure a group exists, and default_path to set the working directory for new sessions in it:

[groups."staging"]
create = true                      # ensure the group exists

[groups."projects/devops"] create = true default_path = "~/repos/devops" # working directory for new sessions

On startup each group with create = true is created if missing (along with any parent groups). default_path is written to the state DB for any group that exists — including groups created from your sessions — so create = true is optional when the group is already there. Reconciliation is additive: removing a group from config.toml leaves the group and its sessions in place, and omitting default_path keeps any value already set. Clear a default with agent-deck group update --clear-default-path.

Per-group Claude config

Agent Deck supports per-group CLAUDE_CONFIG_DIR and env_file overrides. Useful when a single profile hosts groups that should authenticate against different Claude accounts — for example, a personal profile hosting a conductor group pinned to ~/.claude-team while other groups stay on ~/.claude.

Override any group by adding a [groups."".claude] table to $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml):

[groups."conductor".claude]
config_dir = "~/.claude-team"
env_file = "~/git/work/.envrc"

Lookup priority: account > conductor > group > env > profile > global > default. The env_file is sourced into the tmux pane before claude (or the custom command) execs, so any exports it contains become part of the session environment.

Human-watchable verification: bash scripts/verify-per-group-claude-config.sh. The harness creates two throwaway groups, launches one normal and one custom-command session, and prints a pass/fail table.

Per-conductor Claude config (v1.5.4)

Conductors are first-class agent-deck entities (see agent-deck conductor setup). Each conductor can carry its own Claude config_dir and env_file via a top-level [conductors..claude] block:

[conductors.gsd-v154.claude]
config_dir = "~/.claude-team"
env_file = "~/git/work/.envrc"

The conductor name is the string you passed to agent-deck conductor setup — it's the same name that appears in session titles (conductor-).

Precedence chain (most-specific → least-specific):

  1. The session's account (--account → [profiles..claude])
  2. [conductors..claude] (when the session is a conductor session, i.e. Title starts with conductor-)
  3. [groups."".claude]
  4. CLAUDE_CONFIG_DIR env var
  5. [profiles..claude]
  6. [claude] (global)
  7. ~/.claude (default)
This means a single [conductors.gsd-v154.claude] line replaces the need to duplicate the config into [groups."conductor".claude] — the conductor block scopes to exactly that conductor, not to every conductor that shares the conductor group.

Backward compat: sessions in the conductor group with NO matching [conductors..claude] block continue to resolve via [groups."conductor".claude].

Context window and the proactive /clear

The session analytics context bar divides the current prompt size by the model's context window. A model id does not carry its window, so a size read off the model-id table is shown as ≈49.5% (window inferred from model id), a reading larger than that table figure is shown as over-limit with the figure named (the window is then unknown, not 100% full), and an id the table has never seen shows window unknown. Set AGENTDECK_CONTEXT_WINDOW= in the deck's environment to supply the real size; it renders plain, with no ≈.

A conductor's proactive /clear (clear_on_compact) only arms on an established window — that variable, or a size the harness reported. On an inferred, unknown or disproved window it stays off and the conductor falls back to Claude's own compaction; the reason is logged once per session as conductor_clear_on_compact_disarmed. See issue #2026.

Switch a session's account on the fly

For a new one-shot session, use agent-deck launch . -c claude --account . Run agent-deck accounts (or agent-deck accounts --json) to list named slots configured under [profiles..claude].config_dir.

agent-deck session switch-account moves an existing session to another Claude account — conversation included. The session stops, its conversation file is migrated into the target account's config dir (copy-only, with a destination backup and size verification), the account is set, and the session restarts with --resume. session set account auto-migrates too. Claude Code may key one working directory under several project directories (the path as typed, its macOS /private form, its realpath); every copy of the conversation under those keys in both accounts is considered, the newest by last event wins (tie: longest), it is installed under every key in the target, each copy it replaces is backed up next to it, and a newer target copy is never overwritten. The receipt names the chosen copy; --json lists every candidate under transcript.

The TUI exposes the same two moments. The New Session dialog's Claude options carry an Account row (←/→ or Space to cycle, inherit = today's conductor/group/env chain), so a session can be created straight onto the right login. The Edit Session dialog (Shift+P) carries an account row for a session that already exists; saving it asks "Switch Account?" first, then runs the same migrate-and-resume flow as session switch-account, and the session card's [account:"…"] badge follows. Both rows are hidden when no [profiles..claude].config_dir blocks are configured.

Rows of a remote deck get the same dialog: Shift+P on a remote session lists that remote's own slots, previews the switch there (session switch-preview) and, once confirmed, runs the remote's own session switch; transcripts and credentials never leave the remote. See docs/REMOTE-COMMANDS.md.

Session naming

Titles and groups answer different questions — "what is this, at a glance?" versus "why do these sessions belong together?" — and each has its own controls.

Stable titles

By default, agent-deck syncs a session's displayed title from the tool's own session name (Claude's /rename, claude --name, etc.), which is useful for watching a list of live agents but means a title you set can later be overwritten. Pick the control that matches how stable you need the title to be:

| You want | Do this | | --- | --- | | This one session keeps the title I gave it | --title-lock (alias --no-title-sync) on agent-deck add / agent-deck launch, or agent-deck session set-title-lock on at runtime | | No session in this installation ever gets renamed by its agent | sync_title = false in config.toml | | Claude should receive the exact deck title at startup | supported Claude launch/restart/resume commands get --name; push_title = false opts out. A deck rename takes effect on the next start, not immediately. | | A throwaway session where the live task description matters more than a fixed name | agent-deck add --quick (-Q short flag) — the list shows the session's current Claude task in place of the generated handle |

An explicit -t/--title locks the title automatically, the same as passing --title-lock — there's no separate opt-in needed. There's also no create-time opt-out: if you want a session with an explicit title to still pick up the agent's renames, unlock it afterward with agent-deck session set-title-lock off. A locked title is never silently overwritten by the sync path — it only changes via an explicit rename or session set-title-lock off.

Groups vs. parent linkage

Groups carry real policy — max_concurrent (serial vs. bounded parallelism), default_path, and per-group Claude account/config (see Declarative groups and Per-group Claude config above). Treat that policy as the test for whether a new group is warranted:

  • Don't encode dispatch relationships in a group name. A conductor fanning out workers doesn't need work, work-infra, work-hygiene siblings to say "these came from the same orchestrator" — that's what parent linkage is for. Launch with --parent (or let agent-deck launch auto-parent), and read the fleet back with agent-deck session children.
  • Create a new group when something policy-shaped differs from the parent group — a different concurrency cap, working directory, or Claude account. If nothing enforced differs, the thing you have is a topic, not a group; carry it in the title and parent linkage instead.
  • Don't rely on titles or group names as machine keys. Both are user-editable; a script or agent matching on either is relying on something that can change under it.
There's no automated warning or enforcement for group sprawl yet — this is a documented convention, not a gate. See docs/design/2026-07-26-session-identity-and-group-purpose.md for the fuller design writeup (task-identity field, group --purpose, advisory sprawl warning) if you hit a concrete gap this section doesn't cover.

MCP Socket Pool

Running many sessions? Socket pooling shares MCP processes across all sessions via Unix sockets, so each server runs once instead of once per session. A health check every 3 seconds restarts a crashed MCP behind a reconnecting proxy. Enable with pool_all = true in config.toml.

Search

Press / to fuzzy-search across all sessions. Filter by status with ! (running), @ (waiting), # (idle), & (error). Press 0 to clear the filter. Press $ for the Cost Dashboard and G for Recall search across every conversation on this machine.

Keyboard navigation (v1.7.60)

Two tiers of keybindings move the cursor around the session list. The global tier is unchanged from earlier versions; the Alt+ tier (added in v1.7.60) restricts movement to the current group only. Press ? in the TUI to see the full table in-app.

| Scope | Keys | What it does | |---|---|---| | Global (flat list) | j / k or ↓ / ↑ | Move cursor down / up through every item | | Global | Home / End | Jump to top / bottom of list | | Global | G | Open Recall search across every conversation on this machine | | Global | 1–9 | Jump to Nth root group header | | Global | / | Open fuzzy search across all sessions | | Group (current group only) | Alt+j / Alt+k | Next / previous session in current group (skips group boundaries) | | Group | Alt+1–Alt+9 | Jump to Nth session within the current group | | Group | Alt+g / Alt+G | First / last session in current group | | Group | Alt+/ | Open fuzzy search filtered to the current group's sessions |

"Current group" is derived from the cursor position: on a session it's that session's group; on a group header it's that group; on a window it's the parent session's group. On a group boundary Alt+j / Alt+k no-op rather than spilling into the next group.

Status Detection

Smart polling detects what every agent is doing right now:

| Status | Symbol | What It Means | |--------|--------|---------------| | Running | ● green | Agent is actively working | | Waiting | ◐ yellow | Needs your input | | Idle | ○ gray | Ready for commands | | Error | ✕ red | Something went wrong |

Notification Bar

Waiting sessions appear right in your tmux status bar. Press Ctrl+b, release, then press 1–6 to jump directly to them.

⚡ [1] frontend [2] api [3] backend

Git Worktrees

Multiple agents can work on the same repo without conflicts. Each worktree is an isolated working directory with its own branch.

  • agent-deck add . -c claude --worktree feature/a --new-branch creates a session in a new worktree
  • agent-deck add . --worktree feature/b -b --location subdirectory places the worktree under .worktrees/ inside the repo
  • agent-deck worktree finish "My Session" merges the branch, removes the worktree, and deletes the session
  • agent-deck worktree cleanup finds and removes orphaned worktrees
Configure the default worktree location in $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml):
[worktree]
default_location = "subdirectory"  # "sibling" (default), "subdirectory", or a custom path

sibling creates worktrees next to the repo (repo-branch). subdirectory creates them inside it (repo/.worktrees/branch). A custom path like ~/worktrees or /tmp/worktrees creates repo-namespaced worktrees at //. The --location flag overrides the config per session.

Sparse Checkout (large monorepos)

If the session you create the worktree from uses sparse checkout, a new worktree normally checks out the whole repository first — minutes of I/O on a monorepo with hundreds of thousands of files. Opt into inheriting the sparse configuration instead:

[worktree]
sparse_checkout = "inherit"   # "off" (default) keeps git's normal checkout

With inherit, agent-deck reads the sparse mode (cone / non-cone, sparse index) and patterns from the worktree you invoked from, creates the new worktree with --no-checkout, and materializes it directly with those patterns — the excluded tree is never written. .worktreeinclude and .agent-deck/worktree-setup.sh still run afterwards, so the setup script sees the same sparse paths the source session has.

A non-sparse source, or off/unset, keeps today's behavior exactly. Inheritance replays the patterns through git sparse-checkout set and pins the sparse-index setting explicitly, so it needs git 2.32 or newer; the default (off) has no version requirement.

Copying Gitignored Files (.worktreeinclude)

Gitignored files (.env, .mcp.json, etc.) aren't copied into new worktrees by default. To declare which gitignored files should be copied automatically, create a .worktreeinclude file in your repo root:

# .worktreeinclude — gitignore-syntax patterns
.env
.env.local
.mcp.json
secrets/

Only files that are both pattern-matched AND gitignored get copied — tracked files are never duplicated. Directories are copied recursively and merged into existing destinations. Existing files in the worktree are not overwritten.

This works for both single-repo and multi-repo worktree sessions. Matches Claude Code Desktop semantics.

Worktree Setup Script

For imperative setup tasks (installing dependencies, running migrations, etc.), create a script at .agent-deck/worktree-setup.sh. Once you have approved it (see Approving worktree hooks), agent-deck runs it after creating a worktree and processing .worktreeinclude.

#!/bin/sh
npm install

The script receives two environment variables:

  • AGENT_DECK_REPO_ROOT — path to the main repository
  • AGENT_DECK_WORKTREE_PATH — path to the new worktree
It runs in the new worktree with your full environment. An executable script runs directly (its #! line picks the interpreter); a non-executable one runs via sh -e. Under the default approval policy, agent-deck runs a private copy of the approved bytes, so a hook changed while you answer the prompt cannot execute instead. Use the working directory and the two environment variables above to locate project files; the script's own $0 path points to that temporary copy. The timeout is 60 seconds ([worktree] setup_timeout_seconds). If it fails or is skipped, the worktree is still created: you'll see a warning and the session proceeds normally.

Worktree Destruction Script

For imperative teardown tasks (stopping containers, removing volumes, releasing ports, etc.), create a script at .agent-deck/worktree-destruction.sh. Once approved, agent-deck runs it just before removing a worktree, while the worktree still exists.

#!/bin/sh
docker compose -p "$(basename "$AGENT_DECK_WORKTREE_PATH")" down

It receives the same environment variables as the setup script (AGENT_DECK_REPO_ROOT, AGENT_DECK_WORKTREE_PATH) and uses the same dispatch and a 60-second timeout. If it fails or is skipped, removal proceeds anyway and you'll see a warning. It does not run for sessions that reuse the main working tree (nothing is removed there).

Approving worktree hooks

These two hooks are code from the repository, and they run as you. Creating or removing a worktree does not by itself approve them, so a repository you just cloned cannot run anything on your machine until you say so.

What is approved. An approval covers one hook (setup and destruction are separate) in one repository, and is bound to the script's exact bytes (sha256), its real location (symlinks resolved) and how it runs (directly via #!, or sh -e). Editing the script, retargeting a symlink or flipping its executable bit asks again. Approvals live in agent-deck's data directory (~/.local/share/agent-deck/worktree-script-consent.json), never in the repository.

When you are asked. The first time a hook would run, and after any change:

  • TUI: a dialog shows the repository, the hook, the command that will run, its sha256 and up to the first 20 lines (with an omitted-line count when space is tight), with Run once, Always trust this version and Skip (the default).
  • CLI on a terminal: the same details, then [o]nce, [a]lways trust this version, [N]o/skip.
  • CLI without a terminal (scripts, CI, remote), and requests from the web UI: the hook is skipped with a one-line notice naming the command that approves it.
Approving ahead of time.
agent-deck worktree trust-hooks .                          # show each hook, ask y/N
agent-deck worktree trust-hooks . --hook setup --yes       # scripts: approve after reviewing
agent-deck worktree trust-hooks . --revoke                 # forget the approvals
agent-deck launch . -w feat -b --run-hooks                 # run unapproved hooks this once (prints sha256)
agent-deck launch . -w feat -b --run-hooks --trust         # ...and remember that version

[worktree] run_repo_scripts sets the policy: "prompt" (default), "never" (hooks never run), or "always" (every hook runs without asking; only for machines where you own every repository you open, because any repository you create a worktree in can then run code as you). Remote hosts apply their own policy and approvals: run trust-hooks on the remote host.

Bare repositories and worktrees

Agent-deck supports two flavors of the bare-repo layout where every worktree is a peer (no "main" checkout). The two are distinguished by convention — the basename of the bare git dir.

##### Nested .bare/ layout

The bare git metadata sits inside a normal-looking project dir at .bare/:

project/
├── .bare/                         # bare git repo (holds refs, objects, HEAD)
├── .agent-deck/
│   └── worktree-setup.sh          # shared setup script (optional)
├── worktree-a/                    # linked worktree on branch-a
│   └── .git                       # file: gitdir: ../.bare/worktrees/worktree-a
└── worktree-b/                    # linked worktree on branch-b
    └── .git

##### True-bare-at-root layout

The result of a plain git clone --bare repo.git: the directory itself is the bare repo and linked worktrees live as direct children alongside its internal files:

project.git/                       # this dir IS the bare repo
├── HEAD, config, objects/, refs/, packed-refs, worktrees/, ...
├── .agent-deck/
│   └── worktree-setup.sh          # shared setup script (optional)
├── main/                          # linked worktree on main
│   └── .git                       # file: gitdir: ../worktrees/main
└── feature-x/                     # linked worktree on feature-x
    └── .git

How agent-deck resolves these layouts (v1.7.58+ for nested, v1.9.10+ for at-root):

  • All three handles work. agent-deck add , agent-deck add , and agent-deck add all resolve to the same project root. For the nested layout that's the dir holding .bare/. For the at-root layout that's the bare dir itself (project.git/). Every linked worktree is treated as equal.
  • The project root is where shared config lives. Place .agent-deck/worktree-setup.sh at /.agent-deck/worktree-setup.sh. Agent-deck looks for it at exactly that path — it does not search individual worktrees. In the at-root layout that means .agent-deck/ lives inside the bare dir alongside HEAD and objects/.
  • AGENT_DECK_REPO_ROOT inside the setup script points to the project root. Same as above — for at-root that's the bare dir itself.
  • New worktree location depends on the layout:
- Nested: follows your [worktree] setting. default_location = "subdirectory" lands worktrees at /.worktrees/; "sibling" lands them next to the project dir. - At-root: auto-overrides sibling/subdirectory and lands new worktrees as direct children of the bare dir (/) — neither default makes sense when the project root is the bare repo. The path_template config still wins if you want a fully custom path.

Example — create a new worktree against a bare repo from anywhere:

# Nested .bare/ layout
agent-deck add project/ -c claude --worktree feature/c --new-branch
agent-deck add project/.bare -c claude --worktree feature/c --new-branch
agent-deck add project/worktree-a -c claude --worktree feature/c --new-branch

All three resolve to project/, create project/.worktrees/feature-c/, run project/.agent-deck/worktree-setup.sh.

True-bare-at-root layout

agent-deck add project.git/ -c claude --worktree feature/c --new-branch agent-deck add project.git/main -c claude --worktree feature/c --new-branch

Both resolve to project.git/, create project.git/feature-c/, run project.git/.agent-deck/worktree-setup.sh.

agent-deck worktree list and agent-deck worktree finish work from any of those locations.

Common gotchas:

  • .agent-deck/ must live at the project root. For nested layouts that's next to .bare/; for at-root layouts that's inside the bare dir. If you commit .agent-deck/ into a specific branch's worktree instead, agent-deck will not find it — the lookup resolves to the project root, not the current worktree.
  • Detection is by convention: basename .bare ⇒ nested layout, anything else ⇒ at-root. A bare repo named something other than .bare inside a project dir (e.g. project/.git-bare/) is treated as at-root if you point at it directly. For a fully nested layout, use the canonical name .bare.
  • If you keep a .git file at the project root pointing to .bare/ (a variant some tutorials recommend), point agent-deck add at .bare/ or at a linked worktree rather than at the project root — the .git file shadows the bare-repo detection path.

Docker Sandbox

Run sessions inside isolated Docker containers. The project directory is bind-mounted read-write, so agents work on your code while the rest of the system stays protected.

  • Check "Run in Docker sandbox" when creating a session, or set default_enabled = true in config
  • Press E on a sandboxed session to open a container shell
  • agent-deck try --sandbox starts a quick experiment session (dated folder) in a sandbox
Host tool auth (Claude, Gemini, Codex, etc.) is automatically shared into containers via shared sandbox directories — no re-authentication needed. The exception is the Claude Code login on macOS, which lives in the Keychain: copying it would fork the host's OAuth refresh chain and log the host out, so the sandbox keeps a login of its own. Run /login once in your first sandbox session (or pass a claude setup-token credential as CLAUDE_CODE_OAUTH_TOKEN via environment); see the single-owner rule in the sandbox reference.
[docker]
default_enabled = true
mount_ssh = true
auto_cleanup = true    # Remove containers when sessions end (default: true)

Set auto_cleanup = false to keep containers alive after session termination, which is useful for debugging container state or inspecting logs.

See the Docker Sandbox Guide for the full reference including overlay details, custom images, and troubleshooting.

Forking sessions

Press f to quick-fork the selected session, or Shift+F for the fork dialog (customize title, group, branch, and toggles). A fork inherits the parent's conversation context through each tool's native fork — supported for Claude, OpenCode, Pi, Codex, and Oh My Pi (and Codex-compatible custom tools) across the TUI, CLI (agent-deck session fork ), and Web UI.

Quick fork (f) is comprehensive by default: it creates a new git worktree + branch, carries the parent's uncommitted working-tree state, matches the parent's Docker isolation, and inherits the parent's Claude launch options. The Shift+F dialog opens pre-seeded from the same defaults ("comprehensive, tweak down"). Jujutsu (jj) repos are supported too — the fork materializes the parent's working state into a new jj workspace.

Tune the defaults in $XDG_CONFIG_HOME/agent-deck/config.toml (default ~/.config/agent-deck/config.toml):

[fork]
inherit_from_parent = false   # true => mirror the parent and ignore the keys below
worktree            = true    # create a new worktree + branch for the fork
with_state          = true    # carry the parent's uncommitted changes into the fork
with_ignored        = false   # also copy gitignored files (implies with_state) — opt-in
docker              = "auto"  # "auto" = match parent | "on" = always | "off" = never
branch_prefix       = "fork/" # auto branch name = <branch_prefix><sanitized-title>
  • Unset keys default to the values shown above. The [fork] section is independent of [worktree].default_enabled / [docker].default_enabled (those govern non-fork session creation).
  • docker = "auto" forks into a fresh container only when the parent is already sandboxed.
  • branch_prefix applies to both quick fork and the dialog's suggested branch name.
  • with_ignored is off by default (since v1.9.54): the gitignored tree is unbounded (node_modules, datasets, virtual envs) and can carry secrets (.env), and copying it silently can block the fork on heavy repos. Opt in globally here, or per fork in the Shift+F dialog.
Web/API fork (POST /api/sessions/{id}/fork) is plain tool-native fork — it does not apply [fork] worktree/state/Docker defaults (those are TUI quick-fork/dialog scope).
Codex forking requires a codex CLI with codex fork support.

Archive Sessions

Done with a session but not ready to delete it? Archive it. Archiving stops the tmux process and hides the session from the default list — the conversation, metadata, worktree, and parent linkage are all preserved.

  • A archives the selected session; Shift+U restores it to the active list without auto-starting the process
  • R on an archived session restores it to the active list and restarts its process
  • ^ filters the TUI to archived sessions; the web UI has a dedicated Archived tab
  • Search and filters work across archived sessions
  • Deleting (d) is the destructive cousin — it removes the session from the registry (Ctrl+Z undoes recent deletes while the TUI stays open; the web UI gives 30 seconds)
!Session lifecycle: create → run → stop, with archive/unarchive, fork, and worktree branches

Conductor

Conductors are persistent agent sessions that monitor and orchestrate all your other sessions. They watch for sessions that need help, auto-respond when confident, and escalate to you when they can't. Optionally connect Telegram, Slack, Discord or Mattermost for remote control (see docs/conductor/).

Create as many conductors as you need per profile:

# First-time setup (asks about Telegram/Slack, then creates the conductor)
agent-deck -p work conductor setup ops --description "Ops monitor"

Add more conductors to the same profile (no prompts)

agent-deck -p work conductor setup infra --description "Infra watcher" agent-deck conductor setup personal --description "Personal project monitor"

Run a conductor on Codex instead of Claude Code

agent-deck -p work conductor setup review --agent codex --description "Codex reviewer"

Use a custom agent endpoint via environment variables

agent-deck conductor setup glm-bot \ -env ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic \ -env ANTHROPIC_AUTH_TOKEN=<token> \ -env ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5

Or use an env file

agent-deck conductor setup glm-bot -env-file ~/.conductor.env

Each conductor gets its own directory, identity, and settings under the XDG data root (default ~/.local/share/agent-deck/conductor/):

~/.local/share/agent-deck/conductor/
├── CLAUDE.md           # Shared knowledge for Claude conductors
├── AGENTS.md           # Shared knowledge for Codex conductors
├── bridge.py           # Bridge daemon (Telegram/Slack, if configured)
├── ops/
│   ├── CLAUDE.md       # Identity: "You are ops, a conductor for the work profile"
│   ├── meta.json       # Config: name, profile, description, env vars
│   ├── state.json      # Runtime state
│   └── task-log.md     # Action log
└── review/
    ├── AGENTS.md
    └── meta.json

Claude conductors use CLAUDE.md. Codex conductors use AGENTS.md. Shared POLICY.md and LEARNINGS.md remain agent-neutral.

CLI commands:

agent-deck conductor list                    # List all conductors
agent-deck conductor list --profile work     # Filter by profile
agent-deck conductor status                  # Health check (all)
agent-deck conductor status ops              # Health check (specific)
agent-deck conductor teardown ops            # Stop a conductor
agent-deck conductor teardown --all --remove # Remove everything

Telegram bridge (optional): Connect a Telegram bot for mobile monitoring. The bridge routes messages to specific conductors using a name: message prefix:

ops: check the frontend session      → routes to conductor-ops
infra: restart all error sessions    → routes to conductor-infra
/status                              → aggregated status across all profiles

Slack bridge (optional): Connect a Slack bot for channel-based monitoring via Socket Mode. The bot listens in a dedicated channel and replies in threads to keep the channel clean. Uses the same name: message routing, plus slash commands:

ops: check the frontend session      → routes to conductor-ops (reply in thread)
/ad-status                           → aggregated status across all profiles
/ad-sessions                         → list all sessions
/ad-restart [name]                   → restart a conductor
/ad-help                             → list available commands

Slack setup

  1. Create a Slack app at api.slack.com/apps
  2. Enable Socket Mode → generate an app-level token (xapp-...)
  3. Under OAuth & Permissions, add bot scopes: chat:write, channels:history, channels:read, app_mentions:read
  4. Under Event Subscriptions, subscribe to bot events: message.channels, app_mention
  5. If using slash commands, create: /ad-status, /ad-sessions, /ad-restart, /ad-help
  6. Install the app to your workspace
  7. Invite the bot to your channel (/invite @botname)
  8. Run agent-deck conductor setup and enter your bot token (xoxb-...), app token (xapp-...), and channel ID (C01234...)

Both Telegram and Slack can run simultaneously — the bridge daemon handles both concurrently and relays responses on-demand, plus periodic heartbeat alerts to configured platforms.

Built-in status-driven notifications: conductor setup also installs a transition notifier daemon (agent-deck notify-daemon) that watches status transitions and sends parent nudges when child sessions move running -> waiting|error|idle.

Dispatch can be suppressed at two scopes (PR #580, v1.7.34):

# Global kill switch in $XDG_CONFIG_HOME/agent-deck/config.toml

(default ~/.config/agent-deck/config.toml)

[notifications] transition_events = false
# Per-session at creation
agent-deck add --no-transition-notify -c claude .
agent-deck -p work launch . --no-transition-notify -c claude -m "Do task"

Per-session at runtime

agent-deck session set-transition-notify worker off agent-deck session set-transition-notify worker on

Suppression only affects dispatch — the parent link itself is unchanged. Deferred/retried events also honour the flag (guard is in transition_notifier.dispatch as well as both daemon entry points).

Heartbeat-driven monitoring: heartbeats still run on the configured interval (default 15 minutes) as a secondary safety net. If a conductor response includes NEED:, the bridge forwards that alert to Telegram and/or Slack.

Telegram conductor topology (v1.7.22+): each conductor bot must own exactly one channel-owning session. Activate telegram per-session with the session's channels field (agent-deck add --channel plugin:telegram@claude-plugins-official, or session set channels ...; agent-deck passes it to Claude as --channels) and inject TELEGRAM_STATE_DIR via [conductors..claude].env_file in $XDG_CONFIG_HOME/agent-deck/config.toml. Do NOT set enabledPlugins."telegram@claude-plugins-official"=true in a profile's settings.json — that leaks a poller to every claude session under the profile. agent-deck emits warnings (GLOBAL_ANTIPATTERN, DOUBLE_LOAD, WRAPPER_DEPRECATED) when it detects these setups. Full guidance: Telegram conductor topology.

Permission prompts during automation: if a conductor keeps pausing on permission requests, set [claude].allow_dangerous_mode = true (or dangerous_mode = true) in $XDG_CONFIG_HOME/agent-deck/config.toml, then run agent-deck session restart conductor-. See Troubleshooting.

Legacy external watcher scripts: optional only. ~/.agent-deck/events/ is not required for notification routing.

Launching sessions from inside a conductor:

# Inherit current conductor as parent (default when AGENT_DECK_SESSION_ID is set)
agent-deck -p work launch . -t "child-task" -c claude -m "Do task"

Keep parent notifications and still force a custom group

agent-deck -p work launch . -t "review-phantom" -g ard -c claude -m "Review dataset"

Tool command with extra args is supported directly

agent-deck -p work launch . -c "codex --dangerously-bypass-approvals-and-sandbox"

When --cmd includes extra args, agent-deck auto-wraps the tool command so args are preserved reliably. Use --no-parent only when you explicitly want to disable parent routing/notifications.

Channels (Telegram / Slack)

Channels are how a conductor talks to you remotely. Each conductor pairs one-to-one with its own bot — bots are not shared between conductors. agent-deck conductor setup walks you through the pairing during creation.

Key constraints:

  • One bot per conductor. The Telegram Bot API delivers updates via long-poll; a second consumer on the same token causes 409 conflicts and dropped messages.
  • Plugin must be installed under the conductor's Claude profile but never globally enabled. Per-session activation happens via the channels = ["plugin:telegram@claude-plugins-official"] field on the conductor's session record. A globally-enabled plugin leaks pollers into every Claude session under that profile.
  • Bot tokens live in the per-conductor channel state directory at /.env (chmod 600). Never committed to git.
Slack pairing follows the same one-bot-per-conductor pattern. See docs/conductor/ for the full quickstart, including @BotFather steps, profile config, and verification commands.

See also

Watchers

Watchers listen for inbound events (webhooks, push notifications, GitHub events, Slack messages) and route them to conductor sessions so running agents can act on them automatically. Four adapter types ship today:

| Type | Use case | Required flags | |------|----------|----------------| | webhook | Generic HTTP POST listener for any service that can fire a webhook; requests are signed with a shared secret | --port (secret: generated, or $WEBHOOK_SECRET / --secret-file) | | github | GitHub repository webhooks (issues, PRs, pushes) with HMAC-SHA256 verification | $GITHUB_WEBHOOK_SECRET or --secret-file (inline --secret is refused) | | ntfy | ntfy.sh push-notification topics (phone / browser → conductor) | --topic or --random-topic | | slack | Slack messages via a Cloudflare Worker bridge into an ntfy topic | --topic or --random-topic |

``bash

Create, start, test — mirrors the four examples from agent-deck watcher --help`

agent-deck watcher create webhook --name my-webhook --port 9000 agent-deck watcher create github --name gh-alerts --secret-file ~/.gh-webhook-secret # chmod 600 agent-deck watcher create ntfy --name phone --random-topic agent-deck watcher create slack --name team-slack --random-topic

agent-deck watcher start agent-

... (README truncated for length)

Chat with me