Session matching & hooks
Session Matching with Hooks
For reliable session-to-pane mapping (especially with multiple sessions of the same agent in the same project), install hooks:
ccmux setup # Install hooks for every supported agent found on PATHccmux setup --agent codex # Limit to a single agent (installs even if not on PATH)ccmux setup --status # Report install state without writingccmux setup --uninstall # Remove hooksHooks write PID marker files under ~/.config/ccmux/session-pids/ whenever a session starts or begins its first invocation, a turn completes, or the agent asks the user to approve a tool. The daemon picks up the markers in real time via a filesystem watcher. See docs/architecture.md#hook-lifecycle for the full flow (marker writes, chokidar dispatch, per-agent correlation).
Gemini CLI is tracked through process detection and terminal pattern matching, so it needs no setup.
Claude Code
Uses Claude’s native hooks in ~/.claude/settings.json with three scripts under ~/.claude/hooks/:
ccmux-session-start.sh: writes the marker on session create/resumeccmux-session-end.sh: removes the markerccmux-state-notify.sh: updates state onidle_prompt/permission_prompt
Codex CLI
Uses Codex’s native hooks (~/.codex/hooks.json plus the codex hooks feature flag in ~/.codex/config.toml, which is [features] codex_hooks = true pre-0.124 and [features] hooks = true on 0.124+; ccmux recognizes either) with three scripts under ~/.codex/hooks/:
ccmux-session-start.sh: writes the marker when a Codex session startsccmux-stop.sh: refreshes the marker at the end of every turnccmux-permission-request.sh: marks the session aswaiting_permissionwhen the user is asked to approve a tool
Tool-approval detection (PermissionRequest) needs Codex >= 0.122.
Cursor CLI
Uses Cursor’s native hooks (~/.cursor/hooks.json) with four scripts under ~/.cursor/hooks/:
ccmux-session-start.sh: writes the marker on fresh chat launchccmux-session-end.sh: unlinks the marker when the chat endsccmux-before-submit-prompt.sh: flips state toworkingand records the last prompt (1 KB cap)ccmux-stop.sh: refreshes state back toidleat turn completion
Requires cursor-agent >= 2026.1.16 (when the hooks feature landed).
OpenCode
Uses OpenCode’s plugin system rather than shell hooks. ccmux setup --agent opencode drops a single auto-discovered JS plugin at ~/.config/opencode/plugin/ccmux.js (honors $XDG_CONFIG_HOME). The plugin subscribes to OpenCode’s in-process event bus and writes a marker for every session on the server:
session.created/session.updated: marker with directory + titlesession.status(busy/retry/idle): refreshes state toworkingoridlemessage.updated/message.part.updated: captures the user’s last prompt (1 KB cap) into the marker (parity with Claude/Codex/Cursor)permission.asked/permission.replied: flips state towaiting_permissionwith the pending tool, clears back toworkingon replysession.deleted: unlinks the marker
Because one OpenCode server can host many sessions, the daemon folds all markers sharing a server PID into the single ccmux Session for the tmux pane that hosts the server. Status is worst-of (waiting > working > idle); cwd and nativeSessionId come from the newest-activity marker, while pendingTool and the attention indicator come from the newest-waiting marker.
Pi / oh-my-pi
Both use Pi’s extension system rather than shell hooks (oh-my-pi, omp, is a hard fork of Pi that kept the extension API). ccmux setup --agent pi / --agent omp drops a single auto-discovered JS extension at ~/.pi/agent/extensions/ccmux.js or ~/.omp/agent/extensions/ccmux.js. The extension subscribes to the agent’s lifecycle events and writes one marker per session:
session_start: marker with the session id, transcript path, and cwd (fired at launch, so the marker carries full identity immediately)before_agent_start: captures the user’s last prompt (1 KB cap)agent_start/agent_end: flips state toworking/idle(these bracket one full user prompt, so the row never flickers mid-response the way per-turn events would)session_shutdown: unlinks the marker
omp additionally handles the in-place session swaps that change the session id (session_switch for /new and /resume, session_branch for /branch and fork): each reaps the old session’s marker and seeds a fresh one for the new id, since omp mutates the session in place rather than emitting a shutdown/start pair.
Both run one session per process, so there’s no server-style aggregation; the daemon correlates the marker’s PID to its tmux pane via process ancestry and links nativeSessionId.
Approvals are where the two diverge. Unlike Pi, omp gates tool calls behind an Approve/Deny prompt, so its extension also tracks tool_approval_requested (flips to waiting_permission with the gated tool’s name) and tool_approval_resolved (clears back to working once the last outstanding approval is answered; approve and deny both resume the loop). omp rows show a real waiting state and get actionable Approve/Deny buttons; Pi rows never raise one. omp emits these events only when an approval mode is configured; on its default yolo mode nothing is gated, and installing the ccmux extension does not change that either way.
Antigravity CLI
Uses Antigravity’s global named-hook config at ~/.gemini/config/hooks.json with two scripts under ~/.gemini/config/hooks/:
ccmux-preinvocation.sh: creates or refreshes the marker asworkingbefore each model invocationccmux-stop.sh: refreshes the marker asidlewhen the execution loop stops
Antigravity exposes no session-start hook, so a fresh idle session remains pane-tracked until its first prompt. ccmux deliberately does not install PreToolUse: in Antigravity v1.1.1, an empty {} response silently denies the tool call. Permission attention instead comes from the native permission dialog detected in pane content.
Copilot CLI
Drops one hooks file plus its marker script into Copilot’s auto-discovered ~/.copilot/hooks/ dir (ccmux-copilot.json and ccmux-copilot.sh), registering observational events only:
sessionStart: writes the marker (workingif the session launched with an initial prompt, elseidle)userPromptSubmitted: flips the marker toworkingnotification: flips towaitingwhen the payload is a permission or elicitation dialog (other notification types are ignored)agentStop: flips back toidlesessionEnd: removes the marker
ccmux deliberately does not install Copilot’s permissionRequest hook: it is a deciding hook whose output can allow or deny the tool call. Permission attention is observed through notification instead. Copilot’s events.jsonl is also tailed as a log source (it flushes in real time, including the mid-wait permission.requested), and its held-open session.db backs no-hooks native-id discovery.
Matching priority (with hooks installed)
- Marker file (authoritative): Direct PID/TTY/session-id/transcript from the hook, re-verified on every scan so a wrong binding heals itself
- Process start time: For panes markers don’t claim, ccmux correlates session timestamps with agent process start times, matching each same-directory group as a whole within a 10-minute tolerance. When two candidates are too close to call, the session is left unbound rather than guessed.
Without hooks, the daemon does not use historical session IDs to claim pane ownership. It creates pane-scoped sessions from live process + tmux discovery, then attaches agent log metadata only when it can safely tie a log to the running process.
Sandboxes and pty wrappers
Agents launched through a sandbox or a pty-allocating wrapper (nono run -- claude, fence, script -q /dev/null claude) still get a row: the wrapper keeps the pane’s terminal and moves the agent onto a pty of its own, and ccmux follows the pane’s process tree down to the agent when the terminals don’t match. Verified with Claude Code and pi.
This works for agents ccmux recognizes by executable name (Claude Code, pi, codex, cursor, copilot). Gemini CLI, Antigravity and oh-my-pi are also matched by a path fragment of the full command line, so a wrapper whose own arguments spell that path (script -q /dev/null /opt/homebrew/bin/gemini) is itself detected as the agent, on the pane’s tty, and the tree is never consulted. The row still appears and still points at the right pane; its PID is the wrapper’s.
Hook (native) tracking is a separate question. A sandbox must allow writes to ~/.config/ccmux for the agent’s hook to write its marker file. Denied, the row still appears and still binds to the right pane, but the authoritative per-turn hook signals are gone and status falls back to whatever the agent’s own log says. For Claude that means the transcript alone, and it is worth knowing that a Claude row tracked this way gets no terminal-pattern fallback: its state changes when the transcript records the end of a turn, which can lag the visible answer on screen.