0413.mp4
Running multiple Claude Code agents across git worktrees is powerful — but managing the VS Code windows that go with them is a nightmare. You end up Alt-Tabbing through a dozen windows, losing track of which agent is doing what, and manually arranging editors every time you switch context.
Existing "hub" tools either force you into a CLI-only workflow or require a proprietary editor. But you already have VS Code. You just need something to keep it organized.
ccdock sits in a narrow terminal sidebar and takes care of the rest: auto-positioning VS Code windows, tracking every Claude Code agent in real time, and letting you switch between sessions with a single click.
- VS Code orchestration — Auto-open, position, and switch VS Code (or Cursor) windows next to the sidebar. Each session manages its own editor window. Clicking a session whose window is open snaps it straight into focus; clicking one whose window is closed asks for confirmation first (guarding against accidental opens).
- Real-time agent monitoring — See exactly what each Claude Code agent is doing: which tool it's calling, what file it's reading, what command it's running.
- Git worktree management — Create, switch, and delete worktrees via git-wt integration. Each worktree gets its own session.
- Activity log — Live feed of tool invocations with session numbers (#N) across all active agents.
- Mouse + keyboard — Click to select sessions, scroll wheel to navigate, or use vim-style
j/kkeys. - Auto-layout — VS Code windows automatically resize and reposition when the terminal resizes.
- macOS (uses AppleScript for window management)
- Bun runtime (v1.0+)
- VS Code or Cursor
- git-wt for worktree creation (
go install github.com/k1LoW/git-wt@latest) - Ghostty terminal (used for sidebar window detection)
- A terminal font with Nerd Font support (for icons)
# ccdock itself
bun install -g ccdock
# git-wt (required for worktree creation)
go install github.com/k1LoW/git-wt@latestEdit ~/.config/ccdock/config.json (auto-created on first run):
{
"workspace_dirs": ["~/workspace"],
"editor": "code",
"terminal": "ghostty",
"sound": {
"enabled": true,
"permission_request": "/System/Library/Sounds/Funk.aiff",
"notification": "/System/Library/Sounds/Glass.aiff"
},
"notifications": {
"enabled": true,
"events": ["PermissionRequest", "Notification"]
}
}| Key | Description |
|---|---|
workspace_dirs |
Directories to scan for git repositories |
editor |
Editor command: "code" for VS Code, "cursor" for Cursor |
terminal |
Terminal app for terminal sessions. Only "ghostty" is supported today |
sound.enabled |
Play a sound when an agent surfaces a PermissionRequest / Notification |
sound.permission_request |
Sound file (afplay-compatible) for permission prompts |
sound.notification |
Sound file for general notifications |
notifications.enabled |
Pop a macOS Notification Center alert in addition to the sound |
notifications.events |
Hook events that trigger an alert (PermissionRequest, Notification, Stop, ...) |
When notifications.enabled is on, the sound is delivered by macOS as part of the alert — the standalone afplay path is suppressed for that event to avoid a double-beep.
Click-through behavior depends on which backend ccdock can find:
- If
terminal-notifieris installed (brew install terminal-notifier), clicking Show activates Ghostty (the sidebar's terminal) so ccdock comes back to the foreground. - Otherwise ccdock falls back to
osascript display notification. macOS attributes those alerts to Script Editor, so the Show button opens Script Editor — installingterminal-notifieris recommended for the better UX.
Override the activated app with CCDOCK_NOTIFY_BUNDLE_ID=<bundle.id> (e.g. com.googlecode.iterm2 for iTerm2). Set CCDOCK_TERMINAL_NOTIFIER=/path/to/terminal-notifier if it lives outside the standard Homebrew prefixes.
Set CCDOCK_SILENT=1 in the environment to mute every sound and notification regardless of config (handy for tests / quiet sessions). Other macOS system sounds live in /System/Library/Sounds/ (Glass, Funk, Submarine, Ping, Sosumi, …).
Claude Code can also notify you directly, without going through ccdock's hooks. In iTerm2, Ghostty, or Kitty, the preferredNotifChannel setting in ~/.claude/settings.json (default "auto") uses terminal OSC escape sequences (OSC 9 / 777) to pop a desktop notification instantly. Add "preferredNotifChannel": "auto" to settings.json if you want to set it explicitly.
- On Ghostty, set
desktop-notifications = truein~/.config/ghostty/config(some versions default to on already). - Over tmux, add
set -g allow-passthrough onto~/.tmux.confso the escape sequence reaches the outer terminal.
This path is faster than ccdock's hook-based notifications (terminal-notifier/osascript) since it skips spawning a hook process and registering with Notification Center. If you enable it, avoid double notifications by setting notifications.enabled to false (or trimming notifications.events) in ~/.config/ccdock/config.json. ccdock's terminal-notifier/osascript path remains useful as a fallback for environments without OSC support, such as the VS Code integrated terminal.
Add to ~/.claude/settings.json to enable agent status monitoring:
{
"hooks": {
"PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PreToolUse" }] }],
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PostToolUse" }] }],
"PermissionRequest": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PermissionRequest" }] }],
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Stop" }] }],
"Notification": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Notification" }] }],
"SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code SessionEnd" }] }]
}
}If you use OpenAI Codex CLI, you can forward its lifecycle hooks to ccdock too. Tested with codex-cli 0.123.0.
-
Enable the under-development
codex_hooksfeature in~/.codex/config.toml:[features] codex_hooks = true
-
Create
~/.codex/hooks.json(Codex 0.123.0 only reads hooks from this file — inline TOML hooks inconfig.tomlare not wired up yet):{ "hooks": { "PreToolUse": [ { "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook codex PreToolUse", "timeout": 30 }] } ], "PostToolUse": [ { "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook codex PostToolUse", "timeout": 30 }] } ], "PermissionRequest": [ { "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook codex PermissionRequest", "timeout": 30 }] } ], "Stop": [ { "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook codex Stop", "timeout": 30 }] } ] } } -
Restart Codex so it picks up the new configuration.
Known limitations (upstream Codex)
- Only
Bash-style shell tools (local_shell/shell/exec_command) fire hooks reliably today.apply_patch, file writes, and MCP tool invocations currently do not emitPreToolUse/PostToolUse(openai/codex#16732, #17794). Stopdoes not fire undercodex exec(openai/codex#18607); interactive sessions are fine.- Hooks are disabled on Windows.
- Codex does not emit
SessionEnd. Agents in thestoppedstate (after theStophook) are preserved indefinitely so completed sessions stay visible; entries are removed when the matching session is deleted or when the agent'scwdno longer maps to a known session.
ccdock # start the sidebar TUI
ccdock help # show help| Key | Action |
|---|---|
j / k |
Navigate between sessions |
Enter |
Focus the session's editor window |
Tab |
Focus the session's editor window (same as Enter) |
n |
Create new session (interactive wizard) |
d |
Delete session |
w |
Close the session's editor window |
r |
Realign all editor windows |
t |
Open a scratch Ghostty terminal at the workspace root (unmanaged — not tracked or positioned) |
c |
Toggle compact mode |
l |
Toggle activity log |
q / Ctrl+C |
Quit (with option to close windows) |
| Mouse click | Focus the window if open, else confirm to open |
| Scroll wheel | Navigate between sessions |
Each card manages a single VS Code (or Cursor) editor window for its worktree; the border and spinner reflect that window's state.
| Card appearance | Meaning |
|---|---|
| White border | The card's window is focused |
| Normal border | The card's window is open but not focused |
Spinning ⠋ indicator |
The card's window is launching |
| Dim border | The card's window is closed |
Pressing t opens a brand-new Ghostty window at the first configured workspace_dirs entry. This window is entirely unmanaged: ccdock does not track it, position it next to the sidebar, or close it — it behaves exactly like a terminal you opened yourself. Use it for one-off shell commands outside any specific session.
ccdock also uses Ghostty for one other purpose unrelated to t: at startup it tags its own window with a unique title (via an OSC escape sequence) so it can reliably identify itself and position itself on screen. The first time ccdock scripts Ghostty for this, macOS shows an Automation permission prompt ("ccdock wants to control Ghostty") — approve it (also available under System Settings → Privacy & Security → Automation).
| Icon | Status | Description |
|---|---|---|
● green |
running | Agent is executing tools |
●/○ yellow pulse |
waiting | Awaiting user permission |
○ gray |
idle | Agent started but hasn't done anything yet |
● teal |
stopped | Agent finished its turn (Stop event received) |
Claude Code hooks --> ccdock hook --> writes agent JSON files
|
ccdock sidebar (polls every 2s) <----------+
|
+--> reads session + agent state files
+--> queries VS Code windows via AppleScript
+--> renders TUI with merged state
- State —
~/.local/state/ccdock/stores session and agent state as JSON files - Hooks —
ccdock hookwrites agent state files when Claude Code fires events - Window management — AppleScript via
osascriptto position VS Code next to the sidebar - Wizard —
nkey scans workspace dirs and offers create/existing/root worktree options viagit wt, opening the repository's editor window directly on selection
src/
main.ts — CLI entry point
sidebar.ts — Main event loop, input handling
types.ts — Type definitions
config/config.ts — Config (~/.config/ccdock/)
workspace/state.ts — Session/agent state persistence
workspace/editor.ts — VS Code open/focus
workspace/terminal.ts— Ghostty self-identification (sidebar window) + scratch terminal launch
workspace/window.ts — AppleScript window management
worktree/manager.ts — Git worktree operations
worktree/scanner.ts — Repository discovery
tui/render.ts — Sidebar rendering
tui/wizard.ts — Session wizard rendering
tui/input.ts — Keyboard input parsing
tui/ansi.ts — ANSI escape codes
agent/hook.ts — Hook handler
# Clone
git clone https://github.com/shibutani/ccdock.git
cd ccdock
bun install
# Run directly
bun run dev
# Type check
bun run typecheck
# Format
bun run format
# Build standalone binary (optional)
bun run buildThe project uses Bun as runtime and Biome for formatting.
src/main.ts— CLI entry point, routesstart/hook/helpcommandssrc/sidebar.ts— Main event loop: keyboard input, timers, state refreshsrc/tui/— Terminal UI rendering (cards, wizard, input parsing, ANSI codes)src/workspace/— File-based state, AppleScript window management, editor controlsrc/worktree/— Git worktree operations and repository scanningsrc/agent/— Claude Code hook handler
npm publishMIT
