agenticonsult logoagent i /consultDocs
Agentic infrastructureMCP servers

cc-terminals

The terminal control plane — agents open, drive, and read visible terminal sessions and manage the mission fleet, under your pause control and audit log.

This server is a control plane into the running app: it lets an agent session operate the Terminals workspace the same way you do — open and close terminals, write input and send named keys, read output incrementally, run commands and wait for them, resize and focus panes, and manage whole workspaces. On top of that sit the mission-fleet verbs: assign a mission to a commander session, spawn workers, send and signal across the fleet. Unlike the daemon-fronting servers, this one reaches the running desktop app itself through a local control endpoint the app writes at startup.

This is the machinery behind persistent, watchable delegation. A session can stand up a full, grounded agent session in a terminal — context artifacts injected at launch, all capabilities intact — then drive it, read it, and tear it down. Every terminal an agent opens is a regular, operator-visible pane; there is no hidden session type, so what you see in the Terminals view is everything that is running.

The safety posture is the one documented on the Terminals page, and it applies to every tool here: the agent-control pill pauses all mutating agent actions instantly (reads keep working, so you can inspect a paused fleet); the audit log at documents/terminal-workspaces/.control/audit.log records every mutating call as one JSON line; and the whole plane sits behind an enable switch in your configuration, so you can run the platform with agent terminal control off entirely.

Two different questions govern this plane, and they behave in opposite directions. Keeping them apart is the difference between a five-minute fix and an afternoon:

  • Terminal control — may agents drive terminals unattended? Yours, hot, and it fails open: if the configuration cannot be read, the plane reads as on, because a stop that arrives by accident is indistinguishable from a broken feature. Turn it off and the tools here are refused.
  • Your subscription — is this install paid for? The terminal fleet is part of Pro, and this axis fails closed. Without an active subscription the plane refuses everything that acts — opening, writing, running, killing, spawning workers — while the read verbs keep answering, so a session can still list workspaces and read a pane. Anything the plane does not recognize as a read counts as an action and is refused, which means a future verb is covered by default. The refusal names the tier it needs and where to upgrade; both paid tiers pass. Tiers covers what each tier includes, and Autonomy and controls covers why the two fail in opposite directions.

How your agents use it

  • "Stand up a worker on the parser bug with the debugging context." — terminal_open with agent mode and the named context artifacts, then terminal_write + terminal_send_key to direct it, and terminal_read for token-efficient incremental output.
  • "Dispatch the mission fleet." — the Missions Board's dispatch runs through the same verbs an agent can use: mission_assign, mission_spawn_worker, mission_send, mission_signal.
  • "Run the test suite over there and tell me when it is done." — terminal_run and terminal_wait_for_exit against any pane, agent or plain shell.

Prerequisites

The desktop app must be running with agent terminal control enabled — the switch, and the rest of the terminal-plane settings, are covered in the configuration reference.

Grounded sessions run on your own agent CLI subscription. Which CLI is a choice: the spawn tools take an engine of claude or codex, and both get identical treatment — the same MCP servers, the same injected context, the same first-message delivery, the same control switch and the same audit log. Only the command differs. Choosing codex requires the Codex CLI installed and signed in; see requirements.

Tool reference

ToolParametersWhat it does
terminal_ping—Round-trip health check against the Command Center's terminal control server. Returns {pong:true} when reachable, or a structured error if the CC app isn't running or the terminal control plane is disabled (config/cc.config.json control.terminal — ON by default). Call this first to confirm the control plane is live before driving terminals.
terminal_list—List the live terminal session ids in the Command Center. Each id is a pane id (session_id == pane id). Use these ids with terminal_read / terminal_write. Returns [] if no terminal panes are open.
terminal_readid*: string, mode: string, rows: integer, cols: integer, cursor: string, tail_lines: integer, max_bytes: integer, since: integer, strip_ansi: booleanRead a terminal's output. The DEFAULT (no mode) AUTO-CLEANS so you never pull noise: a redrawing TUI / spinner / progress bar is RENDERED to its clean current screen (dozens of redraw frames collapsed → far fewer tokens), while normal append output is returned as the clean stream (no scrollback loss). `rendered` tells you which path was used. Other modes: 'new' = only what's NEW since your last read (incremental follow — a SERVER-SIDE cursor; survives compaction/handoff; NEVER drops, older output served from the transcript) · 'peek' = new without advancing · 'all' = paginate the ENTIRE history (pass `since=next`) · 'screen' = force-render the current screen · `tail_lines=N` = the last N lines · 'raw' = exact bytes, no clean (LAST-RESORT, e.g. inspecting escape sequences). Returns `next`/`has_more`/`has_older`. terminal_info sizes up an unfamiliar terminal; terminal_read_structured gives per-command results; terminal_run runs+captures.
terminal_infoid*: stringSize up a terminal BEFORE reading (cheap — returns NO bulk output). Tells you how much it holds — {running, exit_code, bell_pending, total_bytes, retained_bytes, retained_lines, commands_retained, last_command, has_transcript, transcript_path, cursors}. exit_code = how a finished session ended (null while running); bell_pending = it rang the terminal bell and no input has arrived since (an unresolved needs-attention flag). The size fields tell you how to read a terminal that already accumulated a lot: terminal_read_structured(last_n=N) for a compact 'what happened' · terminal_read(mode='tail', tail_lines=N) for the recent screen · terminal_read(mode='all', since=…) to paginate ALL of it (clean text) · or Read/Grep `transcript_path` directly for surgical access to the full RAW history. NOTE: retained_* reflect the in-memory ring; total_bytes is the full session size.
terminal_read_structuredid*: string, last_n: integer, mode: stringRead a terminal's recent commands as STRUCTURED blocks — [{command, output, exit_code, running}] from OSC-133 markers (PowerShell or zsh sessions from terminal_open). The most TOKEN-EFFICIENT 'what happened / did it succeed' view (far smaller than raw). `last_n` bounds how many recent command blocks you get (default 15) — use it on a terminal with a lot of history so you don't pull everything. `mode='new'` returns only commands since your last structured read (a server cursor) — for ongoing work. Returns `returned`/`total_in_ring`/`has_older` (tail) or `next`/`has_more` (new). OSC-133-INTEGRATED sessions only; other shells (or sessions opened before integration) yield [].
terminal_writeid*: string, data*: string, enter: boolean, paste: booleanWrite input/keystrokes to a live terminal (stdin of the shell process). WRITING TEXT DOES NOT RUN IT: to actually submit/execute, set enter=true (or follow with terminal_send_key enter) — a prompt typed without it just sits in the input line and the session waits. Then call terminal_read to observe output. LONG / MULTILINE TEXT JUST WORKS: bulk input is automatically delivered as a bracketed paste (ESC[200~ … ESC[201~) so a TUI — a nested `claude`, vim, etc. — inserts it verbatim and atomically (no truncation, no per-line auto-submit, no loose-keystroke chunking); the submit Enter is emitted AFTER the paste. Pass paste=false to force raw keystrokes (e.g. a lone control sequence) or paste=true to force paste mode. Works on ANY terminal in the Command Center — full session parity, so a terminal the agent opened and one the operator opened are equivalent. SAFETY: this runs whatever you send in a REAL shell; the operator can pause all agent terminal control at any time from the Terminals view, and every write is recorded to the audit log.
terminal_openid: string, name: string, workspace_id: string, cwd: string, cols: integer, rows: integer, model: string, effort: string, engine: string, skip_permissions: boolean, first_message: string, agent: boolean, contexts: array, inherit_workspace: booleanOpen a NEW PowerShell or zsh terminal in the Command Center and return its id. It is spawned immediately (drive it right away with terminal_write/run/read) AND surfaced as a REGULAR, visible pane in the Terminals UI — the operator sees and can use it like any other terminal (full session parity; no agent-exclusive sessions). Pass workspace_id to choose which workspace the pane lands in (from workspace_list); if omitted, it lands in the currently-active workspace. Close it with terminal_kill (which also removes the pane). cwd defaults to the project root. GROUNDING: set agent=true to launch an interactive Claude Code session in the terminal instead of a plain shell; pass contexts=[...] (context-artifact names or paths from context_list) to inject them as the session's system-prompt context (via --append-system-prompt-file). By default (inherit_workspace=true) the session ALSO inherits the context attached to its workspace — so an agent-spawned terminal gets the same context the operator set. The grounded session is a FULL Claude Code session (it manages its own tools/model/permissions); only the CONTEXT is injected.
terminal_first_messageid*: string, text*: string, submit: boolean, quiet_ms: integer, ceiling_ms: integerType an opening instruction into an EXISTING terminal session once its CLI is READY, then submit it. Use this instead of terminal_write + terminal_send_key enter when the session was only just spawned: a freshly launched Claude Code TUI is not listening for input yet, so a plain write races the boot and the text is silently lost. This waits for the terminal's output to start and then settle, clears any auto-suggested prompt sitting in the input box, types the text and presses Enter. Returns as soon as the wait is queued — delivery happens in the background. For a session you are opening right now, prefer terminal_open's `first_message` (one call). For an established, idle session a plain terminal_write is still fine.
terminal_killid*: stringKill a terminal session by id (the shell process TREE is terminated) AND remove its visible pane from the Command Center. Use to clean up a terminal you opened with terminal_open. Returns {killed:true} if the session existed.
terminal_runid*: string, command*: string, timeout_sec: numberRun a PowerShell or zsh command in a session and return its output + exit code (the reliable 'execute and capture' primitive). Completion is detected via the shell's own OSC-133 markers — no command mutation, no visible sentinel — so the command shows cleanly in the pane (falls back to a sentinel for non-integrated sessions). Best for PowerShell or zsh sessions opened with terminal_open. For a different shell or an interactive TUI use terminal_write + terminal_read instead. Long output is tail-capped.
terminal_wait_for_exitid*: string, timeout_ms: integerBlock until the session's process exits (or the timeout elapses), then return its exit code. Use for a session running a finite command/process. An interactive shell (the default agent session) never exits on its own — use terminal_run for per-command completion instead.
terminal_resizeid*: string, cols*: integer, rows*: integerResize a terminal session's PTY (columns x rows). Returns the applied size.
workspace_list—List the Command Center's terminal WORKSPACES (folder-bound, tabbed containers of panes) and which one is active. Returns {workspaces:[{id,name,color,order,root,panes:[{id,name,shell,status,attention,unseen_stopped}]}], active, focused_pane_id, focused}. Per-pane runtime state answers 'which terminals need attendance?': status = running|exited; attention = null|bell|permission|idle (an UNRESOLVED needs-a-human flag — a permission prompt or idle wait; it clears when input reaches the session, e.g. your terminal_write); unseen_stopped = it exited and the operator hasn't seen that yet. (`focused`/`focused_pane_id` = the operator's currently-focused terminal pane — call terminal_focus for just that, denormalized.) NOTE: requires the Terminals view to have been opened in CC once (so the index exists).
mission_runtimeslug: stringRead a MISSION's fleet runtime + ASSIGNMENT DIRECTORY — the panes bound to a mission joined to their live status, PLUS the mission's operations and who is on what. Returns {agents:[{pane_id,role,operation_id,status,attention,ready,live}], operations:[{id,op_slug,status,name,folder_path}], orphaned_operations:[…] (planned/active ops with NO assigned worker — spawn or assign one), idle_workers:[{pane_id}] (workers with NO operation — give them work), agent_count, running_count, attention_count}. role = commander|worker; ready = safe to message (running, not at a permission prompt). Call on the commander's turn to see who is working on what and what is orphaned — do NOT poll it in a loop.
mission_signalmission*: string, from: string, kind: string, text*: stringAppend a worker→commander signal to a mission's durable _mailbox.md (the commander drains it on its turns). Use to report progress/blockers up the fleet. kind = DONE|BLOCKED|ATTENTION|note. The mission must be activated (have a working folder). Mission/operation/goal CONTENT is authored as markdown directly — this verb is ONLY the signal channel, not a data-write API.
mission_assignpane*: string, operation: stringAssign (or clear) a worker pane's OPERATION — sets the runtime binding so mission_runtime + the board show who is working on what. Pass operation as the operation id '<mission>/<op-slug>' to assign, or omit it to clear (return the worker to idle). The operation CONTENT is authored as markdown; this only sets the assignment.
mission_sendmission*: string, from: string, to*: string, text*: string, submit: boolean, interrupt: booleanSend a live message to another pane in your fleet — the primary coordination channel. Durably enqueues to the mission mailbox (the GUARANTEED path the recipient drains on its turn) AND best-effort delivers it live into the recipient's terminal when it is ready. Commander→worker: dispatch ('start op X · brief at <path> · begin'). Worker→commander: use 'commander' as `to`. submit (default true) presses Enter; interrupt=true re-tasks a busy worker (Ctrl-C first). Heavy payload belongs in the operation markdown — the message is the trigger + pointer, not the data.
mission_spawn_workermission*: string, operation: stringStaff an orphaned operation by standing up a NEW worker terminal in the mission's workspace and binding it to the mission (so mission_runtime sees it). Returns its pane_id. Then dispatch its context with mission_send (the message carries the dossier + op pointers). Use when you have MORE operations than workers (parallel capacity); for serial work, re-task a freed worker with mission_assign + mission_send instead. Pass operation to pre-assign it.
terminal_focus—Get the operator's CURRENTLY-FOCUSED terminal + active workspace — the deictic 'here'. Returns {active_workspace_id, active_workspace_name, focused_pane_id, focused_pane_name, focused_workspace_id, focused_workspace_name}. When the operator says 'run X in my focused terminal' / 'in the workspace I have open' WITHOUT naming it, call this, then act on focused_pane_id (terminal_write / terminal_run / send_key). `focused_pane_id` is null if no terminal is focused (or the Terminals view was never opened) — then ask which terminal. The focused pane may live in a different workspace than the active tab (a popped-out terminal). Read-only.
workspace_switchid*: stringSwitch the Command Center's active terminal workspace by id (from workspace_list).
workspace_createname: string, root: string, count: integer, run: string, model: string, effort: string, template: string, connected: boolean, mcp_scope: string, first_message: string, first_message_target: string, contexts: array, context_target: string, skip_permissions: boolean, terminals: array, preset: stringCreate a terminal workspace EXACTLY as the operator does in the UI: a working folder + how many terminals + what each runs. `count` terminals (1-12, default 1) each launch `run` — 'claude' (default → a grounded, CC-anchored Claude session with the workspace's connection + context), blank (a plain PowerShell or zsh), or any command (a shell that runs it on startup). `model` + `effort` tune a grounded ('claude') run. The frontend creates it + returns the real id. CROSS-ROOT: `root` may be ANY absolute path. When it is OUTSIDE the Command Center the workspace is CONNECTED by default — its sessions reach ALL CC tools/agents/memory AND the folder (both repos); connected=false = Standalone (folder only). `mcp_scope` picks the MCP servers for a connected external workspace. In-root workspaces are always CC-connected (connected/mcp_scope ignored). Every terminal inherits the workspace's connection.
workspace_closeid*: stringClose (delete) a terminal workspace by id. Kills its panes' sessions and trashes its .md.
workspace_reorderfrom*: integer, to*: integerReorder workspace tabs — move the workspace at index `from` to index `to` (indices from workspace_list `order`).
pane_addworkspace_id*: string, target_pane_id: string, dir: string, shell: stringAdd an OPERATOR-VISIBLE terminal pane to a workspace (splits the last pane, or fills an empty workspace). After, call workspace_list for the new pane id, then drive it with terminal_write/read/run. IMPORTANT: a GUI pane's shell only starts when that workspace is VIEWED in Command Center (lazy-mount) — for a reliable agent-private terminal use terminal_open (headless) instead.
pane_closeworkspace_id*: string, pane_id*: stringClose an operator-visible pane in a workspace by id (from workspace_list). Kills its session.
pane_renameworkspace_id*: string, pane_id*: string, name*: stringRename a pane in a workspace (the pane's display label).
terminal_send_keyid*: string, key*: stringSend a NAMED key/keystroke to a terminal — the clean way to drive interactive TUIs (e.g. submit a prompt to a nested `claude` with key='enter', interrupt with 'ctrl_c', navigate with 'up'/'down'). A TUI treats text+Enter in one terminal_write as a paste, so type with terminal_write then submit with terminal_send_key enter. Keys: enter, tab, esc, space, backspace, delete, ctrl_c/d/z/l/u/a/e/r, up, down, left, right, home, end, pageup, pagedown.
terminal_messageid*: string, text*: string, submit: boolean, verify: boolean, clear_ghost: boolean, paste: boolean, interrupt: booleanSend a message into an ALREADY-RUNNING session — the coordination verb for a session that has been up for a while (a nudge, a re-task, an instruction to a worker you spawned earlier). Delivers immediately: it does NOT wait for boot readiness, because an idle session emits no output and a readiness wait would burn its full ceiling before typing a byte. Keeps every delivery protection — bracketed paste so long or multiline text arrives verbatim, a gap before the Enter so it actually submits, and an outcome event so a swallowed message is visible instead of silent. WHICH VERB: opening the session right now → terminal_open(first_message=...) · it was just spawned and may still be booting → terminal_first_message · an established session → this one · a plain (non-agent) shell → terminal_write. SAFE BY DEFAULT: it REFUSES when the session is waiting on a blocking dialog (trust / hook review / usage limit) rather than typing your text into a menu where it would be swallowed, and refuses when no input prompt is visible at all (still booting, or a plain shell) and tells you which verb to use instead. `verify` and `clear_ghost` default from what the session is ACTUALLY doing — on when it is demonstrably idle, off when it is mid-turn (clearing the input mid-turn would eat its queued work). Pass them explicitly only to override.
workspace_broadcastworkspace_id*: string, text*: stringWrite the same text to the live terminals in a workspace (e.g. broadcast a command). Done directly via the host, so it works even if the workspace isn't being viewed. Append \r in the text to run it. REACHES: every grounded agent pane, always. PLAIN SHELLS only when the operator has ticked 'include shells' for that workspace — a bare terminal is often running a dev server, a build or an ssh session, and a broadcast types straight into it. That is an OPERATOR setting; there is no parameter to override it, and the reply reports `skipped_shells` + `include_shells` so a smaller `written_to` is never mistaken for a failed write. An agent pane gets proper TUI framing (bracketed paste + a gap before the Enter) so a long or multiline broadcast actually submits; a plain shell gets the raw bytes. A payload containing control characters (e.g. \u0003 to interrupt a whole fleet) is sent raw everywhere, deliberately.
context_list—List the context artifacts available in the Command Center (name, description, knowledge-graph state) — the 'what context can I attach or inject' surface. Use a returned name/path with terminal_open(contexts=[...]) to ground a session, or with context_attach to attach it to a workspace/pane.
context_attachartifact*: string, workspace_id*: string, pane_id: string, detach: booleanAttach (or detach) a context artifact to a terminal WORKSPACE default or a specific PANE — just like the operator does in the Context UI. Pass workspace_id to set the workspace default (every grounded terminal in it inherits the context); add pane_id to set one pane's own context. Affected agent panes respawn to pick up the change. Set detach=true to remove. `artifact` is a context name or .md path (from context_list). SAFETY: a pause-gated, audited control-plane op.
list_drives—Discover WHERE a workspace can live on this machine BEFORE creating one: returns the available filesystem ROOTS (drive letters like 'C:\\' on Windows; '/' + mounted /Volumes entries on macOS) + the current DEFAULT working folder (what workspace_create uses when `root` is omitted). Call this when you need to place a workspace on a specific drive you don't yet know exists, or to learn the default root. Lean by design — it lists DRIVES only, never folder trees (to browse inside a folder, run a shell command in a terminal). Read-only.

Where to go next

On this page