Agent-readable wiki

cmux — Plain-Language Wiki

cmux is a macOS terminal app built on the Ghostty terminal engine, designed specifically for running AI coding agents. It adds vertical tabs, notification rings, split panes, an in-app browser, and a socket-based CLI so agents can control the UI programmatically.

Pages

  1. Explain It SimplyWhat cmux does in plain language: a Ghostty-powered macOS terminal that treats AI coding agents as first-class citizens, with vertical tabs, notification rings, an in-app browser, and a CLI that lets agents drive the UI over a Unix socket.
  2. Five Ideas to Keep in Your HeadThe handful of concepts — workspace, surface, socket, notification, panel — that unlock every other part of the codebase. Understanding these five terms means understanding how cmux hangs together.
  3. Workspaces, Surfaces & Panels — How the Screen Is OrganizedA workspace is one tab (row in the sidebar). Each workspace can be split into multiple surfaces (terminal panes). Panels are extra panes — browser, file preview, markdown viewer — that live alongside terminals. This page traces how TabManager creates, tracks, and switches between them.
  4. Notification Rings — How Agents Get Your AttentionWhen an AI coding agent needs input, cmux lights up a blue ring around the pane and badges the sidebar tab. This page explains the queue, policy, and store that decide when to ring, how rings are cleared, and why the removal calls run off the main thread to avoid UI freezes.
  5. The Socket & CLI — How Agents Drive cmux ProgrammaticallyThe cmux CLI connects to a Unix socket that the running app exposes. Agents (Claude Code, Codex, etc.) send commands over that socket to create workspaces, send keystrokes, report metadata to the sidebar, and trigger notifications — all without touching the GUI. This page covers the CLI entry point, the socket path rules, and the Go-based remote daemon used for SSH sessions.
  6. The One Analogy to Keep & What to Read NextA short plain-English recap: cmux is a terminal window manager where every tab is a workspace, every split is a surface, the sidebar is a live dashboard, and the socket is the intercom agents use to talk to the app. What to look at next depending on whether you are building an agent integration, customizing the UI, or hacking on the terminal core.

Complete Markdown

# cmux — Plain-Language Wiki

> cmux is a macOS terminal app built on the Ghostty terminal engine, designed specifically for running AI coding agents. It adds vertical tabs, notification rings, split panes, an in-app browser, and a socket-based CLI so agents can control the UI programmatically.

## Context Links

- [Agent index](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/llms.txt)
- [Human interactive wiki](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a)
- [GitHub repository](https://github.com/manaflow-ai/cmux)

## Repository Metadata

- Repository: manaflow-ai/cmux

- Generated: 2026-05-24T05:25:51.692Z
- Updated: 2026-05-24T05:29:15.806Z
- Runtime: Claude Code
- Format: Explain Like I'm 5
- Pages: 6

## Page Index

- 01. [Explain It Simply](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/01-explain-it-simply.md) - What cmux does in plain language: a Ghostty-powered macOS terminal that treats AI coding agents as first-class citizens, with vertical tabs, notification rings, an in-app browser, and a CLI that lets agents drive the UI over a Unix socket.
- 02. [Five Ideas to Keep in Your Head](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/02-five-ideas-to-keep-in-your-head.md) - The handful of concepts — workspace, surface, socket, notification, panel — that unlock every other part of the codebase. Understanding these five terms means understanding how cmux hangs together.
- 03. [Workspaces, Surfaces & Panels — How the Screen Is Organized](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/03-workspaces-surfaces-panels-how-the-screen-is-organized.md) - A workspace is one tab (row in the sidebar). Each workspace can be split into multiple surfaces (terminal panes). Panels are extra panes — browser, file preview, markdown viewer — that live alongside terminals. This page traces how TabManager creates, tracks, and switches between them.
- 04. [Notification Rings — How Agents Get Your Attention](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/04-notification-rings-how-agents-get-your-attention.md) - When an AI coding agent needs input, cmux lights up a blue ring around the pane and badges the sidebar tab. This page explains the queue, policy, and store that decide when to ring, how rings are cleared, and why the removal calls run off the main thread to avoid UI freezes.
- 05. [The Socket & CLI — How Agents Drive cmux Programmatically](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/05-the-socket-cli-how-agents-drive-cmux-programmatically.md) - The cmux CLI connects to a Unix socket that the running app exposes. Agents (Claude Code, Codex, etc.) send commands over that socket to create workspaces, send keystrokes, report metadata to the sidebar, and trigger notifications — all without touching the GUI. This page covers the CLI entry point, the socket path rules, and the Go-based remote daemon used for SSH sessions.
- 06. [The One Analogy to Keep & What to Read Next](https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/06-the-one-analogy-to-keep-what-to-read-next.md) - A short plain-English recap: cmux is a terminal window manager where every tab is a workspace, every split is a surface, the sidebar is a live dashboard, and the socket is the intercom agents use to talk to the app. What to look at next depending on whether you are building an agent integration, customizing the UI, or hacking on the terminal core.

## Source File Index

- `CLAUDE.md`
- `CLI/CLISocketPathResolver.swift`
- `CLI/cmux.swift`
- `CLI/CMUXCLI+Events.swift`
- `daemon/remote/go.mod`
- `ghostty.h`
- `Packages/CMUXAgentLaunch`
- `README.md`
- `skills.sh`
- `Sources/AppDelegate.swift`
- `Sources/cmuxApp.swift`
- `Sources/CmuxConfig.swift`
- `Sources/ContentView.swift`
- `Sources/GhosttyTerminalView.swift`
- `Sources/NotificationsPage.swift`
- `Sources/Panels/BrowserPanel.swift`
- `Sources/Panels/Panel.swift`
- `Sources/Panels/TerminalPanel.swift`
- `Sources/SocketControlSettings.swift`
- `Sources/TabManager.swift`
- `Sources/TerminalNotificationCallerResolver.swift`
- `Sources/TerminalNotificationPolicy.swift`
- `Sources/TerminalNotificationQueue.swift`
- `Sources/TerminalNotificationStore.swift`
- `Sources/Workspace.swift`
- `Sources/WorkspaceContentView.swift`

---

## 01. Explain It Simply

> What cmux does in plain language: a Ghostty-powered macOS terminal that treats AI coding agents as first-class citizens, with vertical tabs, notification rings, an in-app browser, and a CLI that lets agents drive the UI over a Unix socket.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/01-explain-it-simply.md
- Generated: 2026-05-24T05:20:38.530Z

### Source Files

- `README.md`
- `Sources/cmuxApp.swift`
- `Sources/AppDelegate.swift`
- `Sources/GhosttyTerminalView.swift`
- `ghostty.h`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [README.md](README.md)
- [Sources/cmuxApp.swift](Sources/cmuxApp.swift)
- [Sources/AppDelegate.swift](Sources/AppDelegate.swift)
- [Sources/GhosttyTerminalView.swift](Sources/GhosttyTerminalView.swift)
- [ghostty.h](ghostty.h)
- [Sources/SocketControlSettings.swift](Sources/SocketControlSettings.swift)
- [Sources/TabManager.swift](Sources/TabManager.swift)
- [Sources/Workspace.swift](Sources/Workspace.swift)
- [Sources/TerminalNotificationStore.swift](Sources/TerminalNotificationStore.swift)
- [Sources/TerminalNotificationCallerResolver.swift](Sources/TerminalNotificationCallerResolver.swift)
- [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift)
- [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift)
- [Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamKind.swift](Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamKind.swift)
- [Packages/CmuxExtensionKit/README.md](Packages/CmuxExtensionKit/README.md)
</details>

# Explain It Simply

cmux is a macOS terminal app that makes running multiple AI coding agents manageable. It wraps Ghostty's GPU-accelerated terminal renderer in a native Swift/AppKit shell, then adds the layers that matter when you have ten agent sessions running at once: a sidebar with vertical tabs that each show live context (git branch, linked PR, listening ports, latest notification), a notification ring system that highlights exactly which pane needs your attention, an in-app browser agents can drive, and a Unix socket API that lets any tool — or any agent — script the whole UI from the command line.

This page explains what each of those pieces is, why it exists, and how it fits together, in plain language a new contributor would need before touching the code.

---

## The Terminal Engine: Ghostty Under the Hood

cmux does not write its own terminal emulator. It uses **libghostty**, the C library behind the open-source [Ghostty](https://github.com/ghostty-org/ghostty) terminal, linked in as an XCFramework through a git submodule.

The bridge is deliberately thin. `ghostty.h` at the repo root is just a single-line re-export of Ghostty's canonical C API header:

```c
// ghostty.h
#include "ghostty/include/ghostty.h"
```

This keeps Swift's ABI bridge and the bundled `GhosttyKit.xcframework` in sync automatically as the submodule is updated.

`GhosttyTerminalView.swift` holds the AppKit view that hosts each terminal surface. It calls into the C API (`ghostty_surface_clear_selection`, `ghostty_surface_select_cursor_cell`, etc.) using Swift's `@_silgen_name` mechanism to resolve the C symbols directly. Metal and QuartzCore handle compositing; the terminal renders at GPU speed.

Because cmux reads the user's existing `~/.config/ghostty/config`, any Ghostty theme, font, or color preference carries over automatically — no duplicate configuration.

Sources: [ghostty.h:1-8](), [Sources/GhosttyTerminalView.swift:1-20]()

---

## Workspaces, Tabs, and Splits

The core organizational unit is the **Workspace** (the type alias `Tab = Workspace` in `TabManager.swift` shows the historical name change). Each workspace maps to one "project context": a working directory, a git branch, and a set of panes.

Within a workspace you can split panes **horizontally** (⌘D) or **vertically** (⌘⇧D). This is handled by the **Bonsplit** Swift package, imported in nearly every top-level file. Bonsplit owns the recursive split-tree layout; cmux owns the workspace model on top of it.

The sidebar on the left shows one row per workspace. Each row is live-updated via `FSEventStream` watches (`WorkspaceGitMetadataWatcher` in `TabManager.swift`) and displays:

- **Git branch** — updated from filesystem events on `.git/HEAD`
- **Linked PR status and number** — polled from GitHub
- **Working directory** — path of the shell in focus
- **Listening ports** — network services the shell has open
- **Latest notification text** — the most recent agent message

```
┌─────────────────────────────────────┐
│  Sidebar                            │
│  ┌────────────────────────────────┐ │
│  │ 🔵 my-feature   main  #234     │ │
│  │    ~/projects/app  :3000       │ │
│  │    Claude is waiting…          │ │
│  ├────────────────────────────────┤ │
│  │    other-task   dev-branch     │ │
│  └────────────────────────────────┘ │
└─────────────────────────────────────┘
```

The blue ring (🔵) appears when an agent in that workspace is waiting.

Sources: [Sources/TabManager.swift:1-50](), [Sources/Workspace.swift:43-60]()

---

## Notification Rings: Knowing Which Agent Needs You

When you run five Claude Code sessions in parallel, every macOS notification reads "Claude is waiting for your input" — no way to know which one. cmux replaces this with a targeted visual system.

### How it works

Agents emit notifications through one of two paths:

1. **Terminal escape sequences** — OSC 9, OSC 99, or OSC 777 sequences in terminal output are intercepted by `TerminalController` and handed to `TerminalNotificationStore`.
2. **The `cmux notify` CLI command** — agents run this from a hook script. It connects to the Unix socket and calls `v2NotificationCreateForCaller`, which resolves the correct workspace using the calling process's TTY, or explicit workspace/surface IDs if provided.

The `TerminalNotificationCallerResolver` logic uses the TTY of the process that sent the command to find the right workspace automatically — so an agent running inside pane X always highlights pane X, not a random one.

```
Agent hook fires
       │
       ▼
cmux notify "Claude needs input" (CLI)
       │
       ▼  (Unix socket)
TerminalController.v2NotificationCreateForCaller()
       │
       ├─ resolves workspace by TTY or explicit ID
       │
       ▼
TerminalNotificationStore.deliverNotificationSynchronously()
       │
       ├─ blue ring on pane
       ├─ tab highlight in sidebar
       └─ macOS UNUserNotification (optional sound)
```

**⌘⇧U** jumps focus to the most recent unread pane across all workspaces.

Sources: [Sources/TerminalNotificationCallerResolver.swift:1-50](), [Sources/TerminalNotificationStore.swift:1-60]()

---

## The In-App Browser

A browser pane can be opened alongside any terminal split (⌘⇧L). This is a WKWebView embedded in AppKit (imported in `AppDelegate.swift`), not a separate Electron window.

The browser is **scriptable**: its API is ported from Vercel's open-source `agent-browser` project, so agents can:

- Snapshot the accessibility tree
- Get references to page elements
- Click, fill forms, and evaluate JavaScript
- Navigate to URLs

This means a Claude Code session can open a dev server in the browser pane and interact with the running application directly — clicking buttons, reading results — without leaving cmux.

Cookie import from Chrome, Firefox, Arc, and 20+ other browsers means browser sessions start pre-authenticated.

For **SSH workspaces** (`cmux ssh user@remote`), browser panes route through the remote network so `localhost` URLs resolve on the remote machine. Drag-and-drop uploads images via `scp`.

Sources: [README.md:50-90](), [Sources/AppDelegate.swift:1-10]()

---

## The Socket API: Agents as First-Class Drivers

cmux runs a **Unix domain socket** that any local process can talk to. The socket path and access model are controlled by a five-level permission enum in `SocketControlSettings.swift`:

| Mode | Who can connect |
|------|----------------|
| `off` | Nobody — socket is disabled |
| `cmuxOnly` | Processes spawned inside cmux terminals only (ancestry check) |
| `automation` | Any process owned by this macOS user (no ancestry check) |
| `password` | Any local process with the right password from a local file |
| `allowAll` | Any process and user — no auth (unsafe) |

The default for agents running inside cmux terminals is `cmuxOnly`: a Claude Code session inside cmux can drive the UI; a random external process cannot.

Through the socket, a script or agent can:

- Create or close workspaces and panes
- Send keystrokes to any surface
- Send notifications to specific workspaces
- Open URLs in the in-app browser
- Query workspace/surface state

Sources: [Sources/SocketControlSettings.swift:1-60]()

---

## Claude Code Teams and Agent Launch

Running `cmux claude-teams` launches Claude Code's "teammate" multi-agent mode natively. Each teammate spawns as a real split pane with its own sidebar metadata and notification ring — no tmux, no wrappers.

The `CMUXAgentLaunch` package manages the environment that agents inherit. `AgentLaunchEnvironmentPolicy` keeps a strict allowlist of environment variables that flow into agent processes. API keys (`AMP_API_KEY` is explicitly blocked) do not pass through; only safe configuration variables like `ANTHROPIC_BASE_URL`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, and equivalents for other agents (Copilot, RovoDev, CodeBuddy) are forwarded.

This approach is **provider-neutral by design**: the same launch path works for Claude Code, OpenCode, Amp, GitHub Copilot, and any other agent that reads standard config environment variables.

Sources: [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchEnvironmentPolicy.swift:27-55]()

---

## The Workstream Feed

`CMUXWorkstream` is an internal package that captures structured events from running agent sessions into a persistent feed. Events are classified into two groups:

**Actionable** (shown by default in the Feed panel):
- `permissionRequest` — agent is asking for user approval
- `exitPlan` — agent has finished a planning step
- `question` — agent is asking the user something

**Telemetry** (stored, hidden behind an "All" filter):
- `toolUse`, `toolResult`, `userPrompt`, `assistantMessage`, `sessionStart`, `sessionEnd`, `stop`, `todos`

This gives the user a concise "needs attention" view without drowning in every tool call an agent makes.

Sources: [Packages/CMUXWorkstream/Sources/CMUXWorkstream/WorkstreamKind.swift:1-30]()

---

## Extension Sidebars

`CmuxExtensionKit` is a prototype API for custom sidebar panels. A sidebar provider is a Swift struct conforming to `CmuxExtensionSidebarProvider`. It receives a snapshot of workspace state and returns a render model; the host (cmux) owns selection, popovers, and mutation dispatch.

State stays cheap because rows receive immutable value snapshots and action closures — not live `ObservableObject` references — avoiding the re-render feedback loops that caused 100% CPU spin bugs in earlier designs.

Extensions persist their own grouping state to `~/.config/cmux/extensions/<extension>/state.json`.

Sources: [Packages/CmuxExtensionKit/README.md:1-65]()

---

## How the Pieces Fit Together

```text
┌──────────────────────────────────────────────────────────┐
│                  cmux macOS App (Swift/AppKit)            │
│                                                          │
│  ┌─────────────┐  ┌────────────────┐  ┌──────────────┐  │
│  │  Sidebar    │  │ Terminal Panes │  │  Browser     │  │
│  │ (vertical   │  │  (Bonsplit +   │  │  (WKWebView) │  │
│  │  tabs, git, │  │   libghostty)  │  │              │  │
│  │  notifs)    │  │                │  │              │  │
│  └──────┬──────┘  └───────┬────────┘  └──────┬───────┘  │
│         │                 │                   │          │
│  ┌──────▼─────────────────▼───────────────────▼───────┐  │
│  │         TabManager / Workspace model               │  │
│  │    (git watch, ports, notification state)          │  │
│  └──────────────────────────┬─────────────────────────┘  │
│                             │                            │
│  ┌──────────────────────────▼─────────────────────────┐  │
│  │         Unix Socket (SocketControlMode)             │  │
│  │  cmuxOnly | automation | password | allowAll        │  │
│  └──────────────────────────┬─────────────────────────┘  │
└─────────────────────────────┼────────────────────────────┘
                              │
              ┌───────────────┼────────────────┐
              │               │                │
         cmux CLI        Claude Code      Any agent
        (cmux notify,   (teammate mode,   (via socket
         cmux ssh…)      hooks wired)      or CLI)
```

---

## Quick-Reference Feature Table

| Feature | What it gives you | Where in the code |
|---|---|---|
| Ghostty rendering | GPU-accelerated terminal, reads your existing config | `GhosttyTerminalView.swift`, `ghostty.h` |
| Vertical tabs + sidebar | Per-workspace git/PR/port/notification status | `TabManager.swift`, `Workspace.swift` |
| Notification rings | Visual pane highlight when an agent is waiting | `TerminalNotificationStore.swift`, `TerminalNotificationCallerResolver.swift` |
| `cmux notify` CLI | Agents push targeted notifications from hook scripts | `TerminalController` socket handler |
| In-app browser | WKWebView split pane with scriptable accessibility API | `AppDelegate.swift`, browser panel sources |
| Socket API | Programmatic control over the whole UI | `SocketControlSettings.swift` |
| Claude Code Teams | Native splits for multi-agent teammate sessions | `CMUXAgentLaunch` package |
| Workstream feed | Structured actionable-vs-telemetry event feed | `CMUXWorkstream` package |
| Extension sidebars | Custom Swift sidebar providers | `CmuxExtensionKit` package |

---

## Summary

cmux is a native macOS terminal built for the era of parallel AI agents. Its core insight is simple: when you run many agents at once, you need a terminal that treats notifications, context metadata, and programmability as first-class features — not afterthoughts. By wrapping libghostty for rendering (preserving GPU speed and Ghostty config compatibility), layering a live-updating sidebar with per-workspace git and notification state, and exposing a Unix socket API that agents can talk to from inside their own session, cmux gives developers composable primitives rather than an opinionated orchestrator. The `SocketControlMode` enum (off → cmuxOnly → automation → password → allowAll) makes the trust boundary explicit, and the agent-launch environment allowlist keeps secrets out of child processes regardless of which agent tool you use.

Sources: [Sources/SocketControlSettings.swift:8-48](), [README.md:119-138]()

---

## 02. Five Ideas to Keep in Your Head

> The handful of concepts — workspace, surface, socket, notification, panel — that unlock every other part of the codebase. Understanding these five terms means understanding how cmux hangs together.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/02-five-ideas-to-keep-in-your-head.md
- Generated: 2026-05-24T05:19:57.512Z

### Source Files

- `Sources/Workspace.swift`
- `Sources/TabManager.swift`
- `Sources/SocketControlSettings.swift`
- `Sources/Panels/Panel.swift`
- `Sources/TerminalNotificationStore.swift`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [Sources/Workspace.swift](Sources/Workspace.swift)
- [Sources/TabManager.swift](Sources/TabManager.swift)
- [Sources/SocketControlSettings.swift](Sources/SocketControlSettings.swift)
- [Sources/Panels/Panel.swift](Sources/Panels/Panel.swift)
- [Sources/TerminalNotificationStore.swift](Sources/TerminalNotificationStore.swift)
- [Sources/Panels/TerminalPanel.swift](Sources/Panels/TerminalPanel.swift)
- [Sources/GhosttyTerminalView.swift](Sources/GhosttyTerminalView.swift)
</details>

# Five Ideas to Keep in Your Head

cmux is a macOS terminal multiplexer built on top of the Ghostty terminal engine. Its source is large — Workspace.swift alone exceeds 680 KB — but almost everything you encounter in the codebase is a variation on five vocabulary words. Once you know what *workspace*, *surface*, *socket*, *notification*, and *panel* mean in cmux's own terms, you have the skeleton key to every other file.

This page introduces each concept plainly, maps it to the Swift type that owns it, and shows how the five concepts fit together. Reading it once is enough to navigate the rest of the code without getting lost.

---

## 1. Workspace — the row in your sidebar

A **workspace** is one entry in the left-hand sidebar. Think of it as a named project slot. It holds a title, a custom color, a pinned flag, a current directory, and — most importantly — an arbitrarily complex split-pane layout of terminals and other panels.

The class is declared in `Sources/Workspace.swift`:

```swift
// Sources/Workspace.swift:8976-9001
final class Workspace: Identifiable, ObservableObject {
    let id: UUID
    @Published var title: String
    @Published var customTitle: String?
    @Published var isPinned: Bool = false
    @Published var currentDirectory: String
    @Published var panels: [UUID: any Panel] = [:]
    let bonsplitController: BonsplitController
    weak var owningTabManager: TabManager?
    ...
}
```

`TabManager.swift` keeps the compatibility alias `typealias Tab = Workspace` (line 12), so any older code that says "tab" still means the same thing.

A workspace is owned by a `TabManager`, which holds the ordered list you see in the sidebar:

```swift
// Sources/TabManager.swift:1106
@Published var tabs: [Workspace] = []
@Published var selectedTabId: UUID?
```

**Analogy:** a workspace is like a browser window's tab — except each "tab" can contain a whole grid of split panes, and the sidebar is your tab bar.

Sources: [Sources/Workspace.swift:8976-9053](), [Sources/TabManager.swift:1106-1127]()

---

## 2. Surface — the live Ghostty terminal instance

A **surface** is a single running terminal. Where a workspace is the container, the surface is the hot core: it holds the Ghostty C handle, renders text, and accepts keyboard input. You can have many surfaces inside one workspace (one per split pane).

The type is `TerminalSurface`, defined in `Sources/GhosttyTerminalView.swift`:

```swift
// Sources/GhosttyTerminalView.swift:5083
final class TerminalSurface: Identifiable, ObservableObject {
    ...
    enum InputSendResult: Equatable {
        case sent
        case queued
        case inputQueueFull
        case surfaceUnavailable
        case processExited
    }
    ...
}
```

`TerminalSurface` wraps the low-level Ghostty surface handle. The socket command layer (see §3) routes keystrokes and paste events through a `PendingSocketInput` queue inside `TerminalSurface`. The queue exists so that commands arriving while a surface is busy (e.g., mid-resize) are held safely and replayed.

Surfaces are not directly stored in `Workspace.panels`; they are wrapped inside a `TerminalPanel` (see §5). The identifier `surfaceId` seen in notification and socket code is the UUID of the `TerminalPanel` that wraps a given surface.

Sources: [Sources/GhosttyTerminalView.swift:5083-5140](), [Sources/Panels/TerminalPanel.swift:1-29]()

---

## 3. Socket — the local automation door

A **socket** is a Unix domain socket that lets external processes (scripts, agents, the `cmux` CLI) send commands to the running app. It is the reason the CLAUDE.md includes dogfood patterns like `CMUX_SOCKET_PATH` — every CLI command goes through it.

`SocketControlSettings` (in `Sources/SocketControlSettings.swift`) owns the policy for who may connect:

```swift
// Sources/SocketControlSettings.swift:8-18
enum SocketControlMode: String, CaseIterable, Identifiable {
    case off
    case cmuxOnly       // only processes started inside cmux terminals
    case automation     // any process from the same macOS user
    case password       // requires a file-based password
    case allowAll       // no auth; unsafe
    ...
}
```

The default mode is `.cmuxOnly` — only processes whose parent chain started inside a cmux terminal can connect. File permissions reinforce this: modes other than `allowAll` set the socket to `0o600` (owner-only access).

Socket path resolution is non-trivial because debug, staging, and release builds must never share a socket (they would steal each other's commands). The rule is:

| Build variant | Socket path pattern |
|---|---|
| Release (`com.cmuxterm.app`) | `~/Library/Application Support/cmux/cmux.sock` |
| Debug untagged | Blocked at launch — must pass `CMUX_TAG` |
| Debug tagged | `/tmp/cmux-debug-<tag>.sock` |
| Staging | Separate path, configurable |

The `socketPath(environment:bundleIdentifier:isDebugBuild:...)` function in `SocketControlSettings` encodes all of this logic and is tested extensively in `cmuxTests/`.

Sources: [Sources/SocketControlSettings.swift:8-62](), [Sources/SocketControlSettings.swift:293-460]()

---

## 4. Notification — the alert that travels through the stack

A **notification** in cmux means a terminal-originated alert: a long-running command finished, a monitored pattern matched output, etc. It is not a Swift `Notification` or `NotificationCenter` event — it is a domain object of type `TerminalNotification`.

```swift
// Sources/TerminalNotificationStore.swift:694-740
struct TerminalNotification: Identifiable, Hashable {
    let id: UUID
    let tabId: UUID       // workspace
    let surfaceId: UUID?  // which terminal pane triggered it
    let panelId: UUID?
    let title: String
    let subtitle: String
    let body: String
    let createdAt: Date
    var isRead: Bool
    var paneFlash: Bool   // whether to flash the panel ring
    var clickAction: TerminalNotificationClickAction?
}
```

`TerminalNotificationStore` (a `@MainActor` singleton) holds all live notifications and drives:

- **Dock badge** — the unread count badge on the app icon.
- **Sidebar badges** — per-workspace unread counts.
- **Panel flash / ring** — a brief visual pulse on the originating pane (`paneFlash`).
- **System notification** — a macOS `UNUserNotificationCenter` delivery when the app is not focused.
- **Custom sound or shell command** — optional hooks configured by the user.

The store suppresses external delivery (system notification + sound) when the originating workspace and surface are already focused and the app is in the foreground, playing only a local sound instead. Policy hooks (user-defined scripts) can transform or suppress a notification before it is recorded.

Sources: [Sources/TerminalNotificationStore.swift:694-740](), [Sources/TerminalNotificationStore.swift:742-815]()

---

## 5. Panel — the polymorphic pane occupant

A **panel** is whatever occupies one split-pane slot in a workspace. Every pane holds exactly one panel; one workspace can hold many panels. The word captures the fact that panes are not terminal-only — they can contain a terminal, an embedded browser, a Markdown preview, a file preview, or a sidebar tool.

`Panel` is a `@MainActor` protocol in `Sources/Panels/Panel.swift`:

```swift
// Sources/Panels/Panel.swift:259-307
@MainActor
public protocol Panel: AnyObject, Identifiable, ObservableObject where ID == UUID {
    var id: UUID { get }
    var panelType: PanelType { get }  // .terminal | .browser | .markdown | .filePreview | .rightSidebarTool
    var displayTitle: String { get }
    var isDirty: Bool { get }
    func close()
    func focus()
    func unfocus()
    func triggerFlash(reason: WorkspaceAttentionFlashReason)
    func captureFocusIntent(in window: NSWindow?) -> PanelFocusIntent
    func restoreFocusIntent(_ intent: PanelFocusIntent) -> Bool
    ...
}
```

The concrete implementations are:

| Type | File | `panelType` |
|---|---|---|
| `TerminalPanel` | `Sources/Panels/TerminalPanel.swift` | `.terminal` |
| `BrowserPanel` | `Sources/Panels/BrowserPanel.swift` | `.browser` |
| `MarkdownPanel` | `Sources/Panels/MarkdownPanel.swift` | `.markdown` |
| `FilePreviewPanel` | `Sources/Panels/FilePreviewPanel.swift` | `.filePreview` |

`TerminalPanel` is the most common. It wraps a `TerminalSurface` and bridges between the SwiftUI layout system and the Ghostty C layer:

```swift
// Sources/Panels/TerminalPanel.swift:9-23
@MainActor
final class TerminalPanel: Panel, ObservableObject {
    let id: UUID
    let panelType: PanelType = .terminal
    let surface: TerminalSurface
    private(set) var workspaceId: UUID
    ...
}
```

`Workspace.panels` is a `[UUID: any Panel]` dictionary. The key is the panel's own UUID, which is also the `surfaceId` the rest of the codebase uses when routing notifications, socket commands, and focus events.

Sources: [Sources/Panels/Panel.swift:259-307](), [Sources/Panels/TerminalPanel.swift:6-29](), [Sources/Workspace.swift:9031-9033]()

---

## How the Five Fit Together

```text
TabManager
  └─ [Workspace]  (sidebar rows, each has a UUID = "tabId")
       └─ [Panel]  (keyed by UUID = "panelId" / "surfaceId")
            └─ TerminalPanel → TerminalSurface  (live Ghostty renderer)
            └─ BrowserPanel, MarkdownPanel, ...

Socket  ──────────────────────────────────────────────────────►  commands routed to Workspace or TerminalSurface
                                                                   via surfaceId / tabId

TerminalNotificationStore  (singleton)
  ├─ per-tabId  (workspace badge)
  └─ per-surfaceId / panelId  (pane flash, macOS UNNotification)
```

A socket command arrives at the app, is dispatched by `tabId` to a `Workspace`, then by `surfaceId` to the correct `TerminalPanel` and its `TerminalSurface`. A notification follows the same addressing scheme in reverse: the terminal emits an event, `TerminalNotificationStore` records it with `tabId` and `surfaceId`, and then drives the sidebar badge, pane flash, and optional macOS system notification from that single store.

The five terms — workspace, surface, socket, notification, panel — are therefore the coordinate system for the entire event loop: every command, every display update, and every alert is identified by some combination of these IDs and routed through one of these types.

Sources: [Sources/TerminalNotificationStore.swift:1156-1168](), [Sources/Workspace.swift:9031-9065](), [Sources/SocketControlSettings.swift:421-460]()

---

## 03. Workspaces, Surfaces & Panels — How the Screen Is Organized

> A workspace is one tab (row in the sidebar). Each workspace can be split into multiple surfaces (terminal panes). Panels are extra panes — browser, file preview, markdown viewer — that live alongside terminals. This page traces how TabManager creates, tracks, and switches between them.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/03-workspaces-surfaces-panels-how-the-screen-is-organized.md
- Generated: 2026-05-24T05:21:49.095Z

### Source Files

- `Sources/Workspace.swift`
- `Sources/TabManager.swift`
- `Sources/ContentView.swift`
- `Sources/Panels/BrowserPanel.swift`
- `Sources/Panels/TerminalPanel.swift`
- `Sources/WorkspaceContentView.swift`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [Sources/Workspace.swift](Sources/Workspace.swift)
- [Sources/TabManager.swift](Sources/TabManager.swift)
- [Sources/WorkspaceContentView.swift](Sources/WorkspaceContentView.swift)
- [Sources/Panels/Panel.swift](Sources/Panels/Panel.swift)
- [Sources/Panels/TerminalPanel.swift](Sources/Panels/TerminalPanel.swift)
- [Sources/Panels/BrowserPanel.swift](Sources/Panels/BrowserPanel.swift)
- [Sources/Panels/MarkdownPanel.swift](Sources/Panels/MarkdownPanel.swift)
- [Sources/Panels/FilePreviewPanel.swift](Sources/Panels/FilePreviewPanel.swift)
- [Sources/RightSidebarToolPanel.swift](Sources/RightSidebarToolPanel.swift)
</details>

# Workspaces, Surfaces & Panels — How the Screen Is Organized

cmux organizes your screen into three nested layers: **workspaces** (entries in the left sidebar), **panes** (split regions within a workspace), and **panels** (the actual content living inside each pane). Understanding this stack is the key to making sense of how cmux creates, tracks, and switches between terminal sessions, browser views, and file previews.

This page traces the full hierarchy from the top-level `TabManager` that owns all workspaces, down to the individual `Panel` protocol that every piece of content (terminal, browser, markdown viewer, file preview) conforms to, and explains the glue layer — the Bonsplit library — that manages how panes are split and resized.

---

## The Three-Layer Model

```text
┌─────────────────────────────────────────────────────────────┐
│  TabManager                                                 │
│  ┌────────────────────┐  ┌────────────────────┐             │
│  │  Workspace (tab 1) │  │  Workspace (tab 2) │  ...       │
│  │  ┌─────┬─────────┐ │  │  ┌───────────────┐ │            │
│  │  │Pane │  Pane   │ │  │  │     Pane      │ │            │
│  │  │  ┌──┤  ┌────┐ │ │  │  │  ┌─────────┐ │ │            │
│  │  │  │T │  │ B  │ │ │  │  │  │Terminal │ │ │            │
│  │  │  └──┘  └────┘ │ │  │  │  └─────────┘ │ │            │
│  │  └─────┴─────────┘ │  │  └───────────────┘ │            │
│  └────────────────────┘  └────────────────────┘            │
│   T = TerminalPanel, B = BrowserPanel                       │
└─────────────────────────────────────────────────────────────┘
```

---

## TabManager — The Workspace Registry

`TabManager` is the single `ObservableObject` that owns the ordered list of all open workspaces. Every window gets its own `TabManager` instance.

```swift
// Sources/TabManager.swift:899,1106
class TabManager: ObservableObject {
    @Published var tabs: [Workspace] = []
    @Published var selectedTabId: UUID?
    ...
}
```

The active workspace is tracked via `selectedTabId`. A convenience computed property wraps the lookup:

```swift
// Sources/TabManager.swift:2277-2283
var selectedWorkspace: Workspace? {
    guard let selectedTabId else { return nil }
    return tabs.first(where: { $0.id == selectedTabId })
}
var selectedTab: Workspace? { selectedWorkspace }  // legacy alias
```

> **Backward compatibility note:** The type alias `typealias Tab = Workspace` exists at the top of `TabManager.swift` (line 12) so that call sites written before the rename still compile.

### Creating a Workspace

`addWorkspace(...)` is the canonical entry point. It:
1. Snapshots the current workspace list and working directory.
2. Computes an insertion index based on user preference (`top`, `afterCurrent`, or `end`).
3. Constructs a new `Workspace`, which immediately boots an initial `TerminalPanel`.
4. Inserts the workspace into the `tabs` array.
5. Optionally sets `selectedTabId` to switch to it.

```swift
// Sources/TabManager.swift:2500-2578
func addWorkspace(...) -> Workspace {
    ...
    let insertIndex = newTabInsertIndex(snapshot: snapshot, placementOverride: placementOverride)
    let newWorkspace = makeWorkspaceForCreation(...)
    ...
    tabs.insert(newWorkspace, at: insertIndex)
    ...
    selectedTabId = newWorkspace.id   // if select == true
}
```

`addTab(select:eagerLoadTerminal:)` is a thin wrapper that calls `addWorkspace` — it exists for backward-compatible call sites (Sources/TabManager.swift:5132–5133).

### Switching Workspaces

Setting `selectedTabId` triggers a `didSet` observer that:
- Records the previous workspace's focused panel in `lastFocusedPanelByTab` for later restoration.
- Appends to the history stack (`tabHistory`) for back/forward navigation.
- Calls `focusSelectedTabPanel` to transfer macOS first-responder focus.
- Posts a notification so the sidebar highlight can update.

Sources: [Sources/TabManager.swift:1127-1203]()

---

## Workspace — One Sidebar Tab

`Workspace` represents a single entry in the sidebar. It is declared as a `final class` conforming to `Identifiable` and `ObservableObject`.

```swift
// Sources/Workspace.swift:8973-9019
/// Workspace represents a sidebar tab.
/// Each workspace contains one BonsplitController that manages split panes and nested surfaces.
@MainActor
final class Workspace: Identifiable, ObservableObject {
    let id: UUID
    @Published var title: String
    @Published var customTitle: String?
    @Published var isPinned: Bool = false
    @Published var customColor: String?
    @Published var currentDirectory: String
    let bonsplitController: BonsplitController
    @Published var panels: [UUID: any Panel] = [:]
    ...
}
```

Key published properties:

| Property | Purpose |
|---|---|
| `title` | Auto-derived from the active process (e.g., shell prompt) |
| `customTitle` | User-set override shown in the sidebar |
| `customColor` | Hex color string for the sidebar accent dot |
| `isPinned` | Pinned workspaces stay at the top of the list |
| `currentDirectory` | Working directory (drives git metadata watching) |
| `panels` | Map from `UUID` → `any Panel` for all content panes |
| `bonsplitController` | Owns the split-pane layout tree |

### The Bonsplit Bridge

Every `Workspace` owns exactly one `BonsplitController`. Bonsplit is the split-pane engine (imported from the `Bonsplit` package). It manages a binary tree of panes, each holding one or more tabs (surfaces). When bonsplit creates a split, it calls back into `Workspace` via delegate methods like `splitTabBar(_:didSplitPane:newPane:orientation:)`.

The bridge between Bonsplit's internal `TabID` and cmux's `UUID`-based `Panel` system is the `surfaceIdToPanelId` dictionary:

```swift
// Sources/Workspace.swift:9921-9924
// MARK: - Surface ID to Panel ID Mapping
/// Mapping from bonsplit TabID (surface ID) to panel UUID
var surfaceIdToPanelId: [TabID: UUID] = [:]
```

Lookup is bidirectional:
- `panelIdFromSurfaceId(_ surfaceId: TabID) -> UUID?` — used when Bonsplit fires a delegate event
- `surfaceIdFromPanelId(_ panelId: UUID) -> TabID?` — used when cmux needs to tell Bonsplit which tab to select

Sources: [Sources/Workspace.swift:9924](), [Sources/Workspace.swift:10010-10024]()

---

## Panel — Content Inside a Pane

Every piece of content rendered inside a pane — whether a terminal, browser, markdown file, or file preview — conforms to the `Panel` protocol:

```swift
// Sources/Panels/Panel.swift:258-307
@MainActor
public protocol Panel: AnyObject, Identifiable, ObservableObject where ID == UUID {
    var id: UUID { get }
    var panelType: PanelType { get }
    var displayTitle: String { get }
    var displayIcon: String? { get }
    var isDirty: Bool { get }

    func close()
    func focus()
    func unfocus()
    func triggerFlash(reason: WorkspaceAttentionFlashReason)
    func captureFocusIntent(in window: NSWindow?) -> PanelFocusIntent
    func restoreFocusIntent(_ intent: PanelFocusIntent) -> Bool
}
```

### Panel Types

The `PanelType` enum lists every kind of content cmux can place in a pane:

```swift
// Sources/Panels/Panel.swift:6-38
public enum PanelType: String, Codable, Sendable {
    case terminal
    case browser
    case markdown
    case filePreview = "filepreview"
    case rightSidebarTool
}
```

| Panel Type | Class | Description |
|---|---|---|
| `.terminal` | `TerminalPanel` | Wraps a Ghostty terminal surface; the most common panel type |
| `.browser` | `BrowserPanel` | Embedded WKWebView browser with shared cookie pool |
| `.markdown` | `MarkdownPanel` | Live-reloading markdown renderer with text-edit toggle |
| `.filePreview` | `FilePreviewPanel` | PDF, image, media player, or text editor for local files |
| `.rightSidebarTool` | `RightSidebarToolPanel` | File explorer or session index shown as a right-panel tool |

### TerminalPanel — The Core Panel

`TerminalPanel` wraps a `TerminalSurface` (the Ghostty-backed PTY) and adds cmux-layer concerns such as find-in-terminal state, text-box input, and focus management. It explicitly sets `isDirty` to `false` to avoid misleading dirty-dot indicators in the tab strip:

```swift
// Sources/Panels/TerminalPanel.swift:6-17
@MainActor
final class TerminalPanel: Panel, ObservableObject {
    let id: UUID
    let panelType: PanelType = .terminal
    let surface: TerminalSurface
    private(set) var workspaceId: UUID
    @Published private(set) var title: String = "Terminal"
    @Published private(set) var directory: String = ""
    ...
}
```

### BrowserPanel — Embedded Web View

`BrowserPanel` uses a `WKProcessPool` shared across all browser panels so cookies and sessions are common within a window. It conforms to `Panel` and publishes its current URL as the tab title.

Sources: [Sources/Panels/BrowserPanel.swift:2328-2330]()

### MarkdownPanel

`MarkdownPanel` watches a file on disk with FSEvents and reloads when the file changes. It supports two display modes (`preview` and `text`) toggled without navigating away.

Sources: [Sources/Panels/MarkdownPanel.swift:12-38]()

---

## Splitting Panes

Splitting is initiated through `Workspace.splitPaneWithNewTerminal(...)`. The method:
1. Creates a new `TerminalPanel`.
2. Registers it in `panels` and `surfaceIdToPanelId`.
3. Creates a Bonsplit `Tab` value with the panel's title/icon.
4. Calls `bonsplitController.splitPane(paneId, orientation:withTab:insertFirst:)`.
5. Selects the new tab and calls `focus()` on the new panel.

```swift
// Sources/Workspace.swift:15483-15542
func splitPaneWithNewTerminal(
    targetPane paneId: PaneID,
    orientation: SplitOrientation,
    insertFirst: Bool,
    workingDirectory: String?,
    initialInput: String?,
    remoteStartupCommand: String? = nil
) -> TerminalPanel? {
    ...
    let newTab = Bonsplit.Tab(title: ..., kind: SurfaceKind.terminal, ...)
    surfaceIdToPanelId[newTab.id] = newPanel.id
    guard let newPaneId = bonsplitController.splitPane(paneId, orientation: orientation,
                                                        withTab: newTab, insertFirst: insertFirst)
    else { ... return nil }
    bonsplitController.selectTab(newTab.id)
    newPanel.focus()
    return newPanel
}
```

The same pattern is used by `splitPaneWithMarkdown` (Sources/Workspace.swift:12972) and `splitPaneWithFilePreview` (Sources/Workspace.swift:13180) to open non-terminal panels in a split.

### Split Lifecycle Callbacks

`Workspace` implements the Bonsplit delegate protocol. Key callbacks include:

| Delegate Method | Triggered When |
|---|---|
| `splitTabBar(_:didSplitPane:newPane:orientation:)` | A pane was split; creates a panel for the new pane unless `isProgrammaticSplit` suppresses it |
| `splitTabBar(_:didClosePane:)` | A pane was closed; cleans up all panels inside it |
| `splitTabBar(_:didSelectTab:inPane:)` | User switches the active tab within a pane |
| `splitTabBar(_:didFocusPane:)` | User clicks a different pane; updates `focusedPanelId` |

Sources: [Sources/Workspace.swift:16560](), [Sources/Workspace.swift:16280](), [Sources/Workspace.swift:16427](), [Sources/Workspace.swift:16485]()

---

## WorkspaceContentView — Rendering the Active Workspace

`WorkspaceContentView` is the SwiftUI `View` responsible for rendering the visible content of a single `Workspace`. It wraps a `BonsplitView` and supplies a closure that maps each Bonsplit tab to its `PanelContentView`:

```swift
// Sources/WorkspaceContentView.swift:148-218
struct WorkspaceContentView: View {
    @ObservedObject var workspace: Workspace
    let isWorkspaceVisible: Bool
    ...
    var body: some View {
        ...
        let bonsplitView = BonsplitView(controller: workspace.bonsplitController) { tab, paneId in
            if let panel = workspace.panel(for: tab.id) {
                let isFocused = isWorkspaceInputActive && workspace.focusedPanelId == panel.id
                let isSelectedInPane = workspace.bonsplitController.selectedTab(inPane: paneId)?.id == tab.id
                PanelContentView(panel: panel, ...)
            }
        }
        ...
    }
}
```

Inactive workspaces stay mounted in a SwiftUI `ZStack` for state preservation (so terminal scrollback isn't lost), but their `bonsplitController.isInteractive` is set to `false` to stop them from intercepting drags. Visibility is gated on both `isWorkspaceVisible` and whether the panel is selected in its pane or currently focused.

---

## Session Persistence

When the app quits, `Workspace.sessionSnapshot(...)` serializes the full workspace state — the Bonsplit tree, all panel types and their content (URLs for browsers, file paths for markdown/file previews, scrollback for terminals), git branch state, and the focused panel ID. On next launch, `restoreSessionSnapshot(_:)` rebuilds the layout tree and restores each panel in order.

The mapping from old panel IDs (in the snapshot file) to new panel IDs (freshly created at launch) is tracked via `oldToNewPanelIds: [UUID: UUID]` so the focused panel is correctly restored even when IDs are regenerated.

Sources: [Sources/Workspace.swift:163-350]()

---

## Summary

cmux's screen organization is a clean three-layer model: `TabManager` holds an ordered list of `Workspace` objects (one per sidebar row); each `Workspace` owns a `BonsplitController` that manages split panes; each pane holds one or more `Panel` instances (terminal, browser, markdown, file preview, or sidebar tool). The bridge between Bonsplit's tab IDs and cmux's panel UUIDs is the `surfaceIdToPanelId` dictionary on `Workspace`. Switching workspaces is as simple as setting `TabManager.selectedTabId`, which triggers focus transfer, history recording, and window title updates in a single `didSet` observer (`Sources/TabManager.swift:1127-1203`).

---

## 04. Notification Rings — How Agents Get Your Attention

> When an AI coding agent needs input, cmux lights up a blue ring around the pane and badges the sidebar tab. This page explains the queue, policy, and store that decide when to ring, how rings are cleared, and why the removal calls run off the main thread to avoid UI freezes.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/04-notification-rings-how-agents-get-your-attention.md
- Generated: 2026-05-24T05:20:08.891Z

### Source Files

- `Sources/TerminalNotificationStore.swift`
- `Sources/TerminalNotificationQueue.swift`
- `Sources/TerminalNotificationPolicy.swift`
- `Sources/NotificationsPage.swift`
- `Sources/TerminalNotificationCallerResolver.swift`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [Sources/TerminalNotificationStore.swift](Sources/TerminalNotificationStore.swift)
- [Sources/TerminalNotificationQueue.swift](Sources/TerminalNotificationQueue.swift)
- [Sources/TerminalNotificationPolicy.swift](Sources/TerminalNotificationPolicy.swift)
- [Sources/NotificationsPage.swift](Sources/NotificationsPage.swift)
- [Sources/TerminalNotificationCallerResolver.swift](Sources/TerminalNotificationCallerResolver.swift)
- [Sources/Panels/TerminalPanelView.swift](Sources/Panels/TerminalPanelView.swift)
- [Sources/ContentView.swift](Sources/ContentView.swift)
</details>

# Notification Rings — How Agents Get Your Attention

When an AI coding agent running inside a cmux terminal pane finishes a task (or needs input), cmux lights up a **blue ring** around that pane and adds a **badge** to the matching workspace tab in the sidebar. These two visual signals let you notice important events without having to watch every terminal window constantly.

This page explains the full pipeline: how a notification is born from a socket command, queued, policy-evaluated, stored, and finally rendered as that ring — and why the code that removes rings is deliberately moved off the main thread.

---

## The Big Picture

```text
Terminal process / agent
        │
        │ (socket command: notify)
        ▼
TerminalNotificationCallerResolver  ← resolves which pane/workspace
        │
        ▼
TerminalMutationBus (queue)         ← coalesces rapid-fire events
        │  drain on MainActor
        ▼
TerminalNotificationPolicyEngine    ← runs optional hook scripts
        │
        ▼
TerminalNotificationStore           ← single source of truth
        │  @Published notifications
        ├──► TerminalPanelView       ← blue ring (pane-level)
        ├──► ContentView / TabItem   ← badge on sidebar tab
        └──► NotificationsPage       ← in-app notification list
```

---

## Step 1 — Resolving the Caller

A socket command `notification.create` arrives from the terminal process. Before any UI changes can happen, cmux must figure out **which pane** fired the notification, because the agent may not know its own workspace ID.

`TerminalNotificationCallerResolver.swift` (via `TerminalController.v2NotificationCreateForCaller`) resolves the target through a priority chain:

1. **Preferred workspace + surface IDs** supplied directly by the caller.
2. **Caller TTY name** — matched by scanning `workspace.surfaceTTYNames` across all open tab managers.
3. **Currently selected workspace**, used as a final fallback.

Sources: [Sources/TerminalNotificationCallerResolver.swift:53-89]()

---

## Step 2 — The Mutation Queue

Once a target pane is resolved, the notification is handed to `TerminalMutationBus`, which is the serializing queue between off-main-thread socket handlers and the main-actor `TerminalNotificationStore`.

### Why a queue at all?

Socket events can arrive faster than SwiftUI can re-render. Without queuing, each event would trigger a full SwiftUI diffing pass. The bus coalesces duplicate notifications for the same (workspace, surface) pair so only the most recent content lands in the store.

### Coalescing

Every enqueued notification carries a `TerminalNotificationCoalescingKey` keyed on `(generation, tabId, surfaceId)`. When a new notification arrives for the same key, any existing pending entry for that key is removed before the new one is appended:

```swift
// Sources/TerminalNotificationQueue.swift:136-140
if let coalescingKey {
    pending.removeAll { entry in
        entry.notificationCoalescingKey == coalescingKey
    }
}
```

### Batched drain

The bus drains up to 16 mutations per tick (`maxMutationsPerDrain = 16`) on the `@MainActor`, then schedules another drain if more remain. This keeps the main thread responsive even during bursts.

Sources: [Sources/TerminalNotificationQueue.swift:43-165]()

### Generation fencing

`markNotificationClearBoundary()` returns a monotonic generation counter. Clear commands enqueued before a given generation can be discarded by `discardPendingNotifications(forTabId:through:)`, preventing cleared notifications from re-appearing if a slow drain delivers them late.

Sources: [Sources/TerminalNotificationQueue.swift:84-103]()

---

## Step 3 — Policy Evaluation

After the queue drains a `deliverNotification` entry, `TerminalNotificationStore.addNotification` is called on the main actor. Before the notification is stored, the system checks whether the user has configured any **notification hooks** — executable scripts that can inspect and rewrite the notification.

### The policy request

A `TerminalNotificationPolicyRequest` captures the notification content plus context:

| Field | Meaning |
|-------|---------|
| `tabId` / `surfaceId` | Which workspace/pane |
| `title` / `subtitle` / `body` | Notification text |
| `cwd` | Working directory of the pane |
| `isAppFocused` | Whether cmux is the key window |
| `isFocusedPanel` | Whether this specific pane has keyboard focus |

Sources: [Sources/TerminalNotificationPolicy.swift:188-219]()

### Policy effects

Each hook can return a JSON patch that adjusts a `TerminalNotificationPolicyEffects` struct. All effects default to `true`:

| Effect | What it controls |
|--------|-----------------|
| `record` | Whether the notification is saved in the store |
| `markUnread` | Whether the pane ring lights up |
| `paneFlash` | Whether the pane briefly flashes |
| `desktop` | Whether a macOS system notification is sent |
| `sound` | Whether a sound plays |
| `command` | Whether a custom shell command runs |
| `reorderWorkspace` | Whether the workspace moves to the top of the list |

Sources: [Sources/TerminalNotificationPolicy.swift:21-51]()

### Hook execution

If hooks are configured, `TerminalNotificationPolicyEngine.evaluate` spawns each hook via `posix_spawn` with the notification payload as stdin (JSON), environment variables (`CMUX_NOTIFICATION_TITLE`, `CMUX_NOTIFICATION_BODY`, etc.), and a per-hook working directory. Hooks chain: the output of hook N becomes the input of hook N+1. A `"stop": true` in the JSON output short-circuits remaining hooks. Output is capped at 1 MB; hooks that exceed this limit or time out are killed with SIGTERM, then SIGKILL after 750 ms.

Sources: [Sources/TerminalNotificationPolicy.swift:228-311, 691-714]()

---

## Step 4 — The Store

`TerminalNotificationStore` is the single `@MainActor` `ObservableObject` that owns all notification state. It is a shared singleton accessed throughout the app.

### What it holds

```swift
@Published private(set) var notifications: [TerminalNotification]
@Published private(set) var manualUnreadWorkspaceIds: Set<UUID>
@Published private(set) var panelDerivedUnreadWorkspaceIds: Set<UUID>
@Published private(set) var restoredUnreadWorkspaceIds: Set<UUID>
@Published private(set) var focusedReadIndicatorByTabId: [UUID: UUID]
```

Sources: [Sources/TerminalNotificationStore.swift:767-787]()

### The notification struct

`TerminalNotification` is a value type that carries IDs for the workspace (`tabId`), pane (`surfaceId`), and optionally the split panel (`panelId`). `isRead` and `paneFlash` are mutable fields that the store updates in-place.

Sources: [Sources/TerminalNotificationStore.swift:694-740]()

### Fast lookup indexes

Every time `notifications` changes, `buildIndexes` rebuilds a `NotificationIndexes` struct that caches:
- `unreadCount` — total unread across all workspaces
- `unreadCountByTabId` — per-workspace badge count
- `unreadByTabSurface` — set of (tabId, surfaceId) pairs that have unread notifications
- `latestByTabId` — most recent notification per workspace

This means queries like `hasUnreadNotification(forTabId:surfaceId:)` are O(1) set lookups, not O(n) scans.

Sources: [Sources/TerminalNotificationStore.swift:2126-2148]()

---

## Step 5 — Rendering the Ring and Badge

### The blue ring

`TerminalPanelView` receives `hasUnreadNotification` as a plain `Bool` parameter (keeping it outside the snapshot boundary — see the Pitfalls section of `CLAUDE.md`). The ring is rendered via the underlying `GhosttyTerminalView`, which accepts `showsUnreadNotificationRing`:

```swift
// Sources/Panels/TerminalPanelView.swift:36
showsUnreadNotificationRing: hasUnreadNotification && notificationPaneRingEnabled,
```

The `notificationPaneRingEnabled` flag reads `UserDefaults` key `notificationPaneRingEnabled` (default `true`), so users can disable the ring in Settings.

Sources: [Sources/Panels/TerminalPanelView.swift:9-36]()

The `hasUnreadNotification` value itself comes from `TerminalNotificationStore.hasVisibleNotificationIndicator(forTabId:surfaceId:)`, which returns `true` if either there is an unread notification for that pane or there is a focused-read indicator (a transient highlight shown briefly when the focused pane receives a notification while the app is in focus).

Sources: [Sources/TerminalNotificationStore.swift:1134-1137]()

### The sidebar badge

`ContentView` computes `shouldShowUnread` for each workspace tab by combining three sources:
- `notificationStore.hasVisibleNotificationIndicator` (notification-derived unread)
- `workspace.manualUnreadPanelIds` (user-marked unread)
- `workspace.restoredUnreadPanelIds` (persisted unread from session restore)

Sources: [Sources/ContentView.swift:1342-1347]()

### The notification panel

`NotificationsPage` renders the in-app notification list. Each `NotificationRow` shows a filled accent-colored circle (blue dot) for unread entries and a faded circle for read ones. Clicking a row calls `AppDelegate.shared?.openTerminalNotification(notification)`, which focuses the relevant workspace and clears the ring.

Sources: [Sources/NotificationsPage.swift:195-254]()

---

## Step 6 — Clearing Rings

A ring disappears when the notification it represents is marked read. There are several triggers:

| Action | Store method called |
|--------|-------------------|
| User opens/focuses the pane | `markRead(forTabId:surfaceId:)` |
| User clicks "Clear All" in the panel | `markAllRead()` |
| User clicks ✕ on a single row | `remove(id:)` |
| CLI command | `clearNotifications(forTabId:surfaceId:)` |
| Session restore replaces notifications | `restoreSessionNotifications(_:forTabId:)` |

Every one of these methods eventually calls `UNUserNotificationCenter` to remove the corresponding system notification banner.

### Why removal is off the main thread

`UNUserNotificationCenter.removeDeliveredNotifications` and `removePendingNotificationRequests` perform **synchronous XPC** to the `usernoted` daemon. When the system is slow, this call can block indefinitely. Blocking on the main thread would freeze the entire UI.

cmux solves this by adding two extension helpers on `UNUserNotificationCenter` that dispatch the actual removal work to a shared `.utility` serial queue named `com.cmuxterm.notification-removal`:

```swift
// Sources/TerminalNotificationStore.swift:17-35
extension UNUserNotificationCenter {
    private static let removalQueue = DispatchQueue(
        label: "com.cmuxterm.notification-removal",
        qos: .utility
    )

    func removeDeliveredNotificationsOffMain(withIdentifiers ids: [String]) {
        guard !ids.isEmpty else { return }
        Self.removalQueue.async {
            self.removeDeliveredNotifications(withIdentifiers: ids)
        }
    }
    // ... same pattern for pending requests
}
```

Every `markRead`, `clearAll`, `remove`, and `restoreSessionNotifications` call uses these helpers instead of the synchronous versions.

Sources: [Sources/TerminalNotificationStore.swift:12-35]()

---

## Focus Suppression

When the user is actively looking at the pane that fires a notification (cmux is the key window, the correct workspace tab is selected, and the exact surface has keyboard focus), cmux suppresses the macOS system banner and sound — but still records the notification in the store. This avoids interrupting the user when they are already watching.

The check is implemented in `shouldSuppressExternalDelivery`:

```swift
// Sources/TerminalNotificationStore.swift:1474-1481
return AppFocusState.isAppFocused() && isActiveTab && isFocusedSurface
```

When suppressed, a **focused-read indicator** is set (`focusedReadIndicatorByTabId`). This causes the ring to briefly appear even though the notification was immediately read, giving the user a momentary visual acknowledgment.

Sources: [Sources/TerminalNotificationStore.swift:1474-1482, 1445-1446]()

---

## Three Categories of Workspace Unread

The sidebar badge rolls up three separate unread signals, each with its own lifecycle:

| Signal | Set by | Cleared by |
|--------|--------|-----------|
| `manualUnreadWorkspaceIds` | User marks workspace unread | User marks it read, or a new notification arrives |
| `panelDerivedUnreadWorkspaceIds` | A panel inside reports unread activity | User reads the workspace |
| `restoredUnreadWorkspaceIds` | Session restore re-opens a workspace that had unread state | User reads the workspace |

The total workspace badge count is the sum of the notification-derived unread count plus one for each active workspace unread indicator.

Sources: [Sources/TerminalNotificationStore.swift:873-885, 1103-1108]()

---

## Summary

Notification rings in cmux follow a clear assembly line: a socket command is resolved to a specific pane by TTY name or workspace ID, coalesced in a mutex-protected queue, evaluated by optional policy hook scripts that can silence or redirect the notification, stored in a main-actor observable object that computes fast O(1) indexes, and finally rendered as a blue ring in `TerminalPanelView` and a badge in the sidebar. When rings are cleared, the `UNUserNotificationCenter` removal calls run on a dedicated `.utility` serial queue — never on the main thread — to prevent UI freezes caused by slow XPC to `usernoted`. This design is fully documented in the source comment at `Sources/TerminalNotificationStore.swift:12-16`.

---

## 05. The Socket & CLI — How Agents Drive cmux Programmatically

> The cmux CLI connects to a Unix socket that the running app exposes. Agents (Claude Code, Codex, etc.) send commands over that socket to create workspaces, send keystrokes, report metadata to the sidebar, and trigger notifications — all without touching the GUI. This page covers the CLI entry point, the socket path rules, and the Go-based remote daemon used for SSH sessions.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/05-the-socket-cli-how-agents-drive-cmux-programmatically.md
- Generated: 2026-05-24T05:25:51.688Z

### Source Files

- `CLI/cmux.swift`
- `CLI/CLISocketPathResolver.swift`
- `CLI/CMUXCLI+Events.swift`
- `Sources/SocketControlSettings.swift`
- `daemon/remote/go.mod`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [CLI/CLISocketPathResolver.swift](CLI/CLISocketPathResolver.swift)
- [CLI/cmux.swift](CLI/cmux.swift)
- [CLI/CMUXCLI+Events.swift](CLI/CMUXCLI+Events.swift)
- [Sources/SocketControlSettings.swift](Sources/SocketControlSettings.swift)
- [Sources/SocketControlSettings+SocketPathMarkers.swift](Sources/SocketControlSettings+SocketPathMarkers.swift)
- [daemon/remote/go.mod](daemon/remote/go.mod)
- [daemon/remote/cmd/cmuxd-remote/main.go](daemon/remote/cmd/cmuxd-remote/main.go)
- [daemon/remote/cmd/cmuxd-remote/cli.go](daemon/remote/cmd/cmuxd-remote/cli.go)
</details>

# The Socket & CLI — How Agents Drive cmux Programmatically

When you run cmux, it opens a Unix domain socket and listens for commands. The `cmux` CLI is the primary client for that socket. Any process — including AI coding agents such as Claude Code or Codex — can send commands over the socket to create workspaces, inject keystrokes, post sidebar notifications, stream events, and more, entirely without touching the GUI.

This page explains how the socket works, how the CLI finds it, what commands are available, how access is controlled, and how the Go-based remote daemon bridges the socket into SSH sessions and cloud VMs.

---

## The Socket in One Sentence

When the cmux app starts, it creates a Unix socket file. The CLI (and any other authorized local process) connects to that file, writes JSON-RPC or text commands, and reads responses — all without any GUI interaction.

```text
┌──────────────────────────────────────────────────────────┐
│                  macOS machine                           │
│                                                          │
│  ┌─────────────────┐    Unix socket     ┌─────────────┐ │
│  │  cmux.app       │◄──────────────────►│  cmux CLI   │ │
│  │  (listener)     │  ~/...cmux/cmux.sock│  (client)   │ │
│  └─────────────────┘                    └─────────────┘ │
│                                                 ▲        │
│                                          ┌──────┘        │
│                                    Claude Code / Codex / │
│                                    any authorized process │
└──────────────────────────────────────────────────────────┘
```

---

## Socket Path Rules

The app and the CLI must agree on which socket file to use. The rules live in two places: `Sources/SocketControlSettings.swift` on the app side and `CLI/CLISocketPathResolver.swift` on the CLI side. Both share constants via the `CMUXSocketPathDomain` module.

### Canonical paths by app variant

| App variant | Default socket path |
|---|---|
| Release (`com.cmuxterm.app`) | `~/Library/Application Support/cmux/cmux.sock` |
| Debug tagged (`com.cmuxterm.app.debug.<tag>`) | `/tmp/cmux-debug-<tag>.sock` |
| Debug untagged (blocked at launch) | `/tmp/cmux-debug.sock` |
| Nightly | `/tmp/cmux-nightly.sock` |
| Staging | `/tmp/cmux-staging.sock` |
| User-scoped fallback (port conflict) | `~/Library/Application Support/cmux/cmux-<uid>.sock` |

Legacy path `/tmp/cmux.sock` is still tracked as a fallback for older installs.

Sources: [Sources/SocketControlSettings.swift:1-62](), [CLI/CLISocketPathResolver.swift:83-88]()

### How the CLI resolves which socket to connect to

`CLISocketPathResolver.resolve()` is the single entry point. It works in three tiers:

1. **Explicit** — `--socket <path>` flag passed on the command line.
2. **Environment** — `CMUX_SOCKET_PATH` env var (legacy alias: `CMUX_SOCKET`; both set and different → error).
3. **Auto-discovery** — when neither flag nor env is set, builds a candidate list and probes each one.

The auto-discovery candidate list, in priority order:
1. Computed default for this executable's bundle ID and variant.
2. Last recorded socket path (written to marker files when the app successfully binds).
3. Implicit fallback paths for the variant.
4. Up to 12 recently-modified tagged debug sockets found by scanning `/tmp` and `~/Library/Application Support/cmux/` for files matching `cmux-debug-*.sock`.

For each candidate path, `canConnect()` does a non-blocking Unix `connect()` followed by a `poll()` with a 150 ms timeout. The first path that is actively accepting connections wins. If nothing is live, the resolver falls back to the first path where the socket file exists at all.

```text
CLI starts
    │
    ├─ --socket flag?  ──yes──► use it directly
    │
    ├─ CMUX_SOCKET_PATH set? ──yes──► use env path
    │
    └─ auto-discover
           │
           ├─ default path for bundle ID
           ├─ last-known path (marker file)
           ├─ variant fallback paths
           └─ scanned tagged sockets (mtime-sorted, up to 12)
                   │
                   └─ canConnect() probe (150ms each)
                          │
                          ├─ live connection found ──► use it
                          └─ socket file exists ──► use it
```

Sources: [CLI/CLISocketPathResolver.swift:76-190](), [CLI/CLISocketPathResolver.swift:249-329]()

### Untagged debug builds are blocked

To prevent a developer's untagged debug build from stomping on another agent's socket, `SocketControlSettings.shouldBlockUntaggedDebugLaunch()` refuses to start if bundle ID is `com.cmuxterm.app.debug` and `CMUX_TAG` is not set. XCTest environments and `CMUX_UI_TEST_*` vars bypass this. This is why `CLAUDE.md` insists on `./scripts/reload.sh --tag <name>` for all dev work.

Sources: [Sources/SocketControlSettings.swift:370]()

---

## Access Control Modes

The app side exposes five modes configurable in Settings (and overridable via env vars `CMUX_SOCKET_ENABLE` / `CMUX_SOCKET_MODE`):

| Mode | Who can connect | Socket file permissions |
|---|---|---|
| `off` | Nobody | `0o600` |
| `cmuxOnly` *(default)* | Only processes that cmux itself launched | `0o600` |
| `automation` | Any process running as the same macOS user | `0o600` |
| `password` | Any process that provides the correct password | `0o600` |
| `allowAll` | Any local process, any user, no auth | `0o666` |

In `cmuxOnly` mode the app checks process ancestry — the connecting process must be a descendant of the cmux app process. This is the default and prevents arbitrary processes from driving the terminal without explicit opt-in.

### Password storage

When `password` mode is in use, the secret is stored at `~/Library/Application Support/cmux/socket-control-password` (directory `0o700`, file `0o600`). The env var `CMUX_SOCKET_PASSWORD` overrides the file at runtime. On first launch after an upgrade, cmux migrates any existing password from the legacy keychain entry (`com.cmuxterm.app.socket-control` / `local-socket-password`) to the file.

Sources: [Sources/SocketControlSettings.swift:8-62](), [Sources/SocketControlSettings.swift:64-291]()

---

## Wire Protocols: V1 Text and V2 JSON-RPC

The socket speaks two distinct protocols. Legacy commands use **V1** (plain text); all modern automation uses **V2** (newline-delimited JSON-RPC).

### V1 — Plain text

Used for a handful of window-management commands inherited from earlier versions. The client writes one line, the server writes one or more lines back. The CLI reads until 120 ms of idle silence after the first newline.

```
# Request
list_windows\n

# Response (one line per window)
window:1 My Project
window:2 Server Logs
```

V1 commands: `ping`, `new_window`, `current_window`, `close_window`, `focus_window`, `list_windows`.

Sources: [daemon/remote/cmd/cmuxd-remote/cli.go:65-70](), [daemon/remote/cmd/cmuxd-remote/cli.go:229-256]()

### V2 — JSON-RPC (newline-delimited)

All workspace, surface, pane, notification, browser, and streaming commands use V2. Each request and response is a single JSON object terminated by `\n`.

**Request shape:**
```json
{"id": "a3f2", "method": "workspace.create", "params": {"title": "my-project", "initial_command": "npm run dev"}}
```

**Success response:**
```json
{"id": "a3f2", "ok": true, "result": {"workspace_id": "workspace:7"}}
```

**Error response:**
```json
{"id": "a3f2", "ok": false, "error": {"code": "not_found", "message": "workspace not found"}}
```

Read timeout: 15 s. Max frame size in the remote daemon: 4 MB.

Sources: [daemon/remote/cmd/cmuxd-remote/main.go:26-53](), [daemon/remote/cmd/cmuxd-remote/cli.go:259-303]()

---

## CLI Command Reference

The Swift `cmux` CLI and the Go `cmuxd-remote` CLI share the same logical command set (the Go binary is a relay, not a separate command surface). All the V2 commands below map flag names to JSON-RPC method names.

### Workspace management

| CLI command | JSON-RPC method | Key flags |
|---|---|---|
| `list-workspaces` | `workspace.list` | — |
| `new-workspace` | `workspace.create` | `--command`, `--working-directory`, `--name` |
| `close-workspace` | `workspace.close` | `--workspace` |
| `select-workspace` | `workspace.select` | `--workspace` |
| `current-workspace` | `workspace.current` | — |

### Surface (panel/split) management

| CLI command | JSON-RPC method | Key flags |
|---|---|---|
| `list-panels` | `surface.list` | `--workspace` |
| `focus-panel` | `surface.focus` | `--panel`, `--workspace` |
| `new-surface` | `surface.create` | `--workspace`, `--pane` |
| `new-split` | `surface.split` | `--surface`, `--direction` |
| `close-surface` | `surface.close` | `--surface` |
| `refresh-surfaces` | `surface.refresh` | — |

### Sending input

| CLI command | JSON-RPC method | Key flags |
|---|---|---|
| `send` | `surface.send_text` | `--surface`, `--text` |
| `send-key` | `surface.send_key` | `--surface`, `--key` |

### Notifications

```bash
cmux notify --title "Build done" --body "Tests passed" --workspace workspace:1
```

Maps to `notification.create`. Agents use this to post status updates visible in the cmux sidebar without any shell output.

Sources: [daemon/remote/cmd/cmuxd-remote/cli.go:63-91]()

### Browser control

The `browser` subcommand drives an embedded browser panel. Sub-commands: `open`, `navigate`, `back`, `forward`, `reload`, `get-url`, `snapshot`, `eval`, `click`, `dblclick`, `hover`, `focus`, `check`, `uncheck`, `type`, `fill`, `press`, `select`, `screenshot`.

```bash
cmux browser open --url https://localhost:3000 --workspace workspace:1
cmux browser screenshot --surface surface:2 > preview.png
```

Sources: [daemon/remote/cmd/cmuxd-remote/cli.go:93-121]()

### Streaming events

```bash
cmux events --category workspace --reconnect --cursor-file /tmp/cmux-cursor.txt
```

The `events` subcommand opens a persistent streaming connection over V2 (`events.stream` method) and prints one NDJSON object per line to stdout. The `--cursor-file` flag atomically writes the last seen `seq` number so a restarted consumer can resume without gaps. With `--reconnect`, transient errors (socket not found, broken pipe, ECONNRESET, etc.) trigger a 1-second sleep and retry rather than an immediate exit.

Frame types: `ack`, `heartbeat`, `event`. Each `event` frame carries a monotonically increasing `seq` number.

Sources: [CLI/CMUXCLI+Events.swift:8-134]()

### Environment variables set by cmux terminals

When the app spawns a shell inside a terminal panel, it automatically sets:

| Variable | Value |
|---|---|
| `CMUX_SOCKET_PATH` | Path to the active socket |
| `CMUX_WORKSPACE_ID` | ID of the containing workspace |
| `CMUX_SURFACE_ID` | ID of the surface (terminal panel) |
| `CMUX_TAB_ID` | ID of the tab |

Because `CMUX_SOCKET_PATH` is already set, child processes (including AI agents) automatically connect to the right socket without any extra configuration.

Sources: [CLI/cmux.swift]() (env resolution at line 2796-2826)

---

## How an AI Agent Drives cmux: End-to-End Flow

```text
Agent process (Claude Code / Codex / script)
        │
        │  inherits CMUX_SOCKET_PATH from terminal env
        ▼
cmux CLI  (or direct socket write)
        │
        │  1. Resolve socket path
        │  2. Authenticate (ancestry check, password, or open)
        │  3. Write JSON-RPC request line
        │  4. Read JSON response line
        ▼
cmux.app  (socket listener in TerminalController)
        │
        │  Dispatch to workspace / surface / pane / notification handler
        ▼
UI updates, keystroke injection, sidebar notifications
```

A typical Claude Code session might run:

```bash
# Create a dedicated workspace for the agent's task
cmux new-workspace --name "feature-branch" --command "zsh"

# Send a command to the new workspace's terminal
cmux send --workspace workspace:3 --text "npm test\n"

# Post a sidebar notification when done
cmux notify --title "Tests complete" --body "All 142 tests passed" --workspace workspace:3
```

---

## The Remote Daemon: Driving cmux over SSH

When working inside a cloud VM or an SSH session, there is no direct path to the local Unix socket. The `cmuxd-remote` Go binary (at `daemon/remote/cmd/cmuxd-remote/`) bridges the gap.

### Two transport modes

**stdio (for cloud VMs):** The app spawns `cmuxd-remote serve --stdio` as a child process. JSON-RPC frames flow over stdin/stdout. The `cmuxd-remote` binary inside the VM is the RPC server; it manages PTY sessions and TCP proxy streams.

**WebSocket (for SSH relay):** `cmuxd-remote serve --ws --auth-lease-file <path>` starts a WebSocket server. The app connects to it as a client over the relay transport.

### Socket path resolution inside a remote session

In a remote (SSH) context the Go CLI cannot see the local Unix socket. It resolves the socket address in this order:

1. `--socket <path>` flag
2. `CMUX_SOCKET_PATH` env var
3. `~/.cmux/socket_addr` file — written by the app after a reverse SSH tunnel is established

If `socket_addr` contains a `host:port` value (no leading `/`), the Go CLI dials TCP. For TCP connections over the relay, it performs an HMAC-SHA256 challenge-response handshake:

```text
Server → {"protocol":"cmux-relay-auth","version":1,"relay_id":"…","nonce":"…"}
Client → {"relay_id":"…","mac":"HMAC-SHA256(token, "relay_id=…\nnonce=…\nversion=1")"}
```

Relay credentials come from `CMUX_RELAY_ID`/`CMUX_RELAY_TOKEN` env vars or from `~/.cmux/relay/<port>.auth`.

Sources: [daemon/remote/cmd/cmuxd-remote/cli.go:582-724](), [daemon/remote/go.mod:1-8]()

### Remote daemon RPC capabilities

The server side of `cmuxd-remote` advertises these capability strings on `hello`:

| Capability | What it covers |
|---|---|
| `session.basic` | Terminal window-size tracking across multiple clients |
| `session.resize.min` | Min-of-all-attachments dimension negotiation |
| `proxy.http_connect` | HTTP CONNECT tunnel through the daemon |
| `proxy.socks5` | SOCKS5 proxy tunnel |
| `proxy.stream` | Raw TCP stream proxy |
| `proxy.stream.push` | Server-initiated data push on a proxy stream |
| `pty.session` | Full PTY session management |
| `pty.session.token` | Per-attachment token auth for PTY sessions |

Sources: [daemon/remote/cmd/cmuxd-remote/main.go:330-380]()

---

## Security Considerations

- **Default mode is `cmuxOnly`** — only processes that cmux itself started can connect. An AI agent running in a cmux terminal inherits the right ancestry automatically; an agent started elsewhere must use `automation` or `password` mode.
- **Socket file permissions** are `0o600` for all modes except `allowAll` (`0o666`). Even in `automation` mode, no other macOS user can read or write the socket file.
- **Password is stored on disk**, not in the keychain (migrated from keychain in older versions). The file is mode `0o600` in a `0o700` directory. The env var `CMUX_SOCKET_PASSWORD` can override it without touching the file.
- **Relay auth uses HMAC-SHA256** with a per-connection nonce, so replay attacks are not viable.
- **The app refuses to start an untagged debug build** if another debug instance is running, preventing accidental socket theft between agents.

Sources: [Sources/SocketControlSettings.swift:8-62](), [Sources/SocketControlSettings.swift:64-291](), [daemon/remote/cmd/cmuxd-remote/cli.go:672-724]()

---

## Summary

The cmux socket is the control plane for programmatic automation. The app binds a Unix socket at a well-known path (primarily `~/Library/Application Support/cmux/cmux.sock` for release builds), and the CLI auto-discovers it via a live-probe mechanism that scans candidate paths and tagged debug sockets. Access is governed by five modes from locked-down ancestry checking to full open access. Two wire protocols coexist: a simple text protocol for legacy window commands and a newline-delimited JSON-RPC protocol for all modern workspace, surface, pane, notification, browser, and event-streaming operations. For remote sessions, the Go `cmuxd-remote` binary relays the same command set over stdio or WebSocket with HMAC-based relay authentication, allowing AI agents operating inside cloud VMs or SSH sessions to drive cmux as if they were local.

Sources: [Sources/SocketControlSettings.swift:760-780](), [daemon/remote/cmd/cmuxd-remote/main.go:24-80]()

---

## 06. The One Analogy to Keep & What to Read Next

> A short plain-English recap: cmux is a terminal window manager where every tab is a workspace, every split is a surface, the sidebar is a live dashboard, and the socket is the intercom agents use to talk to the app. What to look at next depending on whether you are building an agent integration, customizing the UI, or hacking on the terminal core.

- Page Markdown: https://grok-wiki.com/public/wiki/manaflow-ai-cmux-e789cf316a9a/pages/06-the-one-analogy-to-keep-what-to-read-next.md
- Generated: 2026-05-24T05:22:18.070Z

### Source Files

- `README.md`
- `CLAUDE.md`
- `Sources/CmuxConfig.swift`
- `Packages/CMUXAgentLaunch`
- `skills.sh`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [README.md](README.md)
- [CLAUDE.md](CLAUDE.md)
- [Sources/CmuxConfig.swift](Sources/CmuxConfig.swift)
- [Sources/CmuxWorkspaceDefinition.swift](Sources/CmuxWorkspaceDefinition.swift)
- [Sources/SocketControlSettings.swift](Sources/SocketControlSettings.swift)
- [Sources/SidebarPortDisplayText.swift](Sources/SidebarPortDisplayText.swift)
- [Sources/Sidebar/SidebarState.swift](Sources/Sidebar/SidebarState.swift)
- [Sources/ExtensionSidebarWorkspaceRowView.swift](Sources/ExtensionSidebarWorkspaceRowView.swift)
- [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift)
- [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift)
- [skills/cmux/skill.md](skills/cmux/skill.md)
- [skills/cmux-browser/skill.md](skills/cmux-browser/skill.md)
- [skills/cmux-workspace/skill.md](skills/cmux-workspace/skill.md)
- [skills/cmux-settings/skill.md](skills/cmux-settings/skill.md)
- [skills/cmux-diagnostics/skill.md](skills/cmux-diagnostics/skill.md)
- [skills.sh](skills.sh)
</details>

# The One Analogy to Keep & What to Read Next

cmux is a macOS terminal app with four moving parts you need to keep in your head. Once the analogy clicks, every file, command, and setting finds its place. This page gives you that analogy, maps it to real source files, and then points you at the right corner of the codebase depending on what you want to do.

This page is for people who just opened the repository and want to know where to look next — whether they are writing an agent integration, tweaking the UI, or hacking on the terminal core.

---

## The Analogy

Think of cmux like a hotel.

| Hotel concept | cmux concept | Where it lives |
|---|---|---|
| The building | **Window** — a top-level macOS app window | `AppDelegate.swift`, `ContentView.swift` |
| A floor / suite | **Workspace** — one sidebar tab, groups related terminals | `Sources/CmuxWorkspaceDefinition.swift` |
| A room | **Pane** — a split container inside a workspace | `vendor/bonsplit` submodule (the `Bonsplit` package) |
| The desk / TV screen | **Surface** — the actual terminal or browser session | `Sources/GhosttyTerminalView.swift` |
| The lobby notice board | **Sidebar** — shows git branch, PR, cwd, open ports, and latest notification for every workspace | `Sources/Sidebar/`, `Sources/ExtensionSidebarWorkspaceRowView.swift` |
| The hotel intercom | **Socket** — Unix socket that lets agents, CLI, and scripts talk to the live app | `Sources/SocketControlSettings.swift` |

```text
Window
 └─ Workspace  (sidebar tab)
     ├─ Pane  (split container)
     │   ├─ Surface:terminal
     │   └─ Surface:browser
     └─ Pane
         └─ Surface:terminal
```

Every CLI command and socket message uses these four handle types — `window:N`, `workspace:N`, `pane:N`, `surface:N` — as its coordinate system. The short-ref style is the default; UUIDs are accepted as input but only printed on request.

Sources: [skills/cmux/skill.md](skills/cmux/skill.md), [Sources/CmuxWorkspaceDefinition.swift:1-50](Sources/CmuxWorkspaceDefinition.swift)

---

## The Four Concepts in Plain English

### Workspace = a sidebar tab for a project

A workspace is the unit you see in the left sidebar. Each entry shows the git branch it's on, any linked PR number and status, the current working directory, the listening ports (formatted as `:3000`, `:8080`, etc.), and the most recent notification text from any agent running inside it.

Workspaces can be defined statically in `cmux.json` (with a name, cwd, color, and layout) or created dynamically at runtime. The `CmuxWorkspaceDefinition` struct is the parsed form of the static config.

Sources: [Sources/CmuxWorkspaceDefinition.swift:1-37](Sources/CmuxWorkspaceDefinition.swift), [Sources/SidebarPortDisplayText.swift:1-17](Sources/SidebarPortDisplayText.swift)

### Surface = what you actually type into

A surface is a single terminal session or browser tab. Inside one workspace you can have multiple panes (created with splits), and each pane can hold multiple surfaces (horizontal tabs at the top of the pane). The Ghostty rendering engine drives each terminal surface; browser surfaces run a WebKit view routed through the same pane container.

Sources: [README.md:60-65](README.md), [skills/cmux-workspace/skill.md](skills/cmux-workspace/skill.md)

### Sidebar = a live dashboard, not just a nav panel

The sidebar is not just a list of workspaces you can click on. Every row is a real-time snapshot: it renders git metadata, PR status, listening ports, and notification badge counts. The row view (`CmuxExtensionSidebarWorkspaceRowView`) is an `Equatable` SwiftUI view that receives immutable value snapshots and closure action bundles — no direct store references — so it can skip body re-evaluation during high-frequency typing.

Sources: [Sources/ExtensionSidebarWorkspaceRowView.swift:1-55](Sources/ExtensionSidebarWorkspaceRowView.swift), [Sources/Sidebar/SidebarState.swift:1-16](Sources/Sidebar/SidebarState.swift)

### Socket = the intercom agents use

The Unix socket (default path `/tmp/cmux-debug-<tag>.sock` for tagged Debug builds, discoverable at runtime via `CMUX_SOCKET_PATH`) is how the CLI, agent skills, and external automation scripts communicate with the running app. Access is controlled by a `SocketControlMode` enum with five levels: `off`, `cmuxOnly` (only processes started inside cmux terminals), `automation` (any local process from the same macOS user), `password`, and `allowAll` (unsafe, no auth).

The socket threading policy matters for agent builders: high-frequency telemetry commands (`report_*`, `ports_kick`, status/progress/log updates) must not use `DispatchQueue.main.sync` — parse and deduplicate off-main, then push minimal mutations to main with `async`.

Sources: [Sources/SocketControlSettings.swift:9-54](Sources/SocketControlSettings.swift), [CLAUDE.md — Socket command threading policy](CLAUDE.md)

---

## The Configuration File

The single user-facing config file is `~/.config/cmux/cmux.json` (JSONC). The in-app watcher reloads it on save — no restart needed.

The `CmuxConfigFile` struct is the parsed representation:

```swift
// Sources/CmuxConfig.swift:10-39
struct CmuxConfigFile: Codable, Sendable {
    var actions: [String: CmuxConfigActionDefinition]
    var ui: CmuxConfigUIDefinition?
    var notifications: CmuxNotificationConfigDefinition?
    var newWorkspaceCommand: String?
    var surfaceTabBarButtons: [CmuxSurfaceTabBarButton]?
    var commands: [CmuxCommandDefinition]
    var vault: CmuxVaultConfigDefinition?
}
```

Top-level sections split into two groups:

| Section | Purpose |
|---|---|
| `app`, `terminal`, `notifications`, `sidebar`, `sidebarAppearance`, `workspaceColors`, `automation`, `browser`, `shortcuts` | **Settings** — live-reloaded app behavior |
| `actions`, `ui`, `commands`, `vault`, `rightSidebar` | **Definitions** — custom commands, palette entries, workspace layouts |

Terminal rendering (font, theme, cursor, background opacity, blur) goes in Ghostty config at `~/.config/ghostty/config`, not in `cmux.json`.

Sources: [Sources/CmuxConfig.swift:10-86](Sources/CmuxConfig.swift), [skills/cmux-settings/skill.md](skills/cmux-settings/skill.md)

---

## Agent Integration: What the Codebase Provides

cmux is designed to host AI coding agents. Three subsystems exist specifically for this:

### 1. Launch sanitizer (`CMUXAgentLaunch` package)

When an agent session is suspended and resumed, cmux needs to reconstruct a safe launch command without re-applying transient flags (like `--use-system-ca`) or non-restorable subcommands. The `AgentLaunchSanitizer` handles this for every supported agent kind: `claude`, `claudeTeams`, `codex`, `codexTeams`, `grok`, `amp`, `cursor`, `gemini`, `hermes-agent`, `rovodev`, `copilot`, and more.

Sources: [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift:24-68](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift)

### 2. Hook injection (`HermesAgentHookConfig`)

cmux can inject its own hook entries into an agent's config file (e.g. a YAML hooks section) and cleanly remove them on uninstall. The begin/end markers and base64-encoded restore lines let the injection be idempotent and reversible. The same pattern is used for `hermes-agent` and `rovodev`.

Sources: [Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift:1-90](Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift)

### 3. Skills (`skills/` directory + `skills.sh`)

Agent skills are bundled Markdown instruction files that teach agents how to drive cmux via the CLI and socket. They are installed into `~/.codex/skills/` (or a custom `--dest`) by `skills.sh`. Available skills:

| Skill | Purpose |
|---|---|
| `cmux` | Core topology control: windows, workspaces, panes, surfaces, focus, reorder |
| `cmux-browser` | Browser surface automation: open, navigate, snapshot, click, fill |
| `cmux-workspace` | Scope actions to the calling agent's workspace without stealing focus |
| `cmux-settings` | Read and write `cmux.json` via the helper script |
| `cmux-diagnostics` | Health check: CLI, socket, hooks, session restore, notifications |
| `cmux-keyboard-shortcuts` | Inspect and customize shortcuts |
| `cmux-markdown` | Render Markdown in a side pane |

Install all at once:
```bash
./skills.sh
# or remotely:
curl -fsSL https://raw.githubusercontent.com/manaflow-ai/cmux/main/skills.sh | bash
```

Sources: [skills.sh:1-30](skills.sh), [skills/cmux/skill.md](skills/cmux/skill.md), [skills/cmux-browser/skill.md](skills/cmux-browser/skill.md)

---

## What to Read Next

Your next step depends on what you want to build or change.

### Building an agent integration

1. **Start**: `skills/cmux/skill.md` — the handle model, core commands, and topology API
2. **Stay in scope**: `skills/cmux-workspace/skill.md` — non-disruptive automation, how to read `CMUX_WORKSPACE_ID` and `CMUX_SURFACE_ID`
3. **Browser automation**: `skills/cmux-browser/skill.md`
4. **Agent session resume**: `Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizer.swift` — how launch arguments are sanitized per agent kind
5. **Hook injection**: `Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/HermesAgentHookConfig.swift`
6. **Debugging**: `skills/cmux-diagnostics/skill.md` — socket health, CLI ping, hook validation

### Customizing the UI or adding settings

1. **Config file structure**: `Sources/CmuxConfig.swift` — the full parsed shape of `cmux.json`
2. **UI settings**: `Sources/CmuxConfigUI.swift`, `Sources/AppearanceSettings.swift`
3. **Sidebar row rendering**: `Sources/ExtensionSidebarWorkspaceRowView.swift` — the `Equatable` snapshot pattern required to avoid CPU spin loops
4. **Sidebar state**: `Sources/Sidebar/SidebarState.swift`
5. **Pitfalls**: Re-read the snapshot-boundary and no-state-mutation-in-body-computations rules in `CLAUDE.md` before touching any panel with a `LazyVStack` or `ForEach`

### Hacking on the terminal core

1. **Ghostty submodule**: `ghostty/` — the libghostty rendering engine; changes must be pushed to `manaflow-ai/ghostty` before updating the parent pointer
2. **Terminal surface views**: `Sources/GhosttyTerminalView.swift` — typing-latency-sensitive; `forceRefresh()` is called on every keystroke
3. **Socket commands**: `Sources/SocketControlSettings.swift` for access modes; `Sources/CmuxSocketEventMapper.swift` for event routing
4. **Debug log**: `tail -f "$(cat /tmp/cmux-last-debug-log-path)"` gives a live ring-buffer view of key, mouse, focus, and split events during a tagged Debug run

---

## One Rule to Carry Forward

The socket is the boundary where agents and the app meet. Everything on the agent side (skills, launch sanitizers, hook injectors) is designed to be additive and reversible — it injects, sanitizes, and cleans up without permanently altering the user's config. Everything on the app side (socket modes, threading policy, sidebar snapshots) is designed to let agents act without stealing focus or blocking the main thread. That contract is the reason cmux can run multiple agents in parallel workspaces without the UI freezing.

Sources: [Sources/SocketControlSettings.swift:44-54](Sources/SocketControlSettings.swift), [CLAUDE.md — Socket command threading policy and Socket focus policy](CLAUDE.md)

---