# 12. Claude Channel Integration ## 12.1 Overview Hot Sheet can push events to a running Claude Code session via the Claude Channels protocol (MCP-based). This enables two workflows: - **On-demand**: User clicks the play button to tell Claude to process the Up Next worklist. - **Automatic**: When tickets are added to Up Next, Claude is automatically notified (with a debounced delay using exponential backoff) to pick up the new work. The feature is disabled by default and must be enabled in Settings. It is listed under an "Experimental" heading. ## 12.2 Architecture The integration has three components: 1. **Channel server** — A small MCP server script bundled with Hot Sheet (`dist/channel.js` in production, `src/channel.ts` in dev). Registered in `.mcp.json` when the feature is enabled. Claude Code spawns it as a subprocess. It listens on a local HTTP port for commands from Hot Sheet. 2. **Hot Sheet server** — Detects when the channel is active by checking the channel port file. POSTs to the channel server's HTTP endpoint to push events to Claude. Provides `/api/channel/done` endpoint for Claude to signal completion. 3. **Hot Sheet UI** — Shows a play button in the sidebar when the channel is enabled. Single-click for on-demand, double-click for automatic mode. ``` Hot Sheet UI → Hot Sheet Server → Channel Server (HTTP) → Claude Code (stdio/MCP) ↑ | └──────────── Claude uses Hot Sheet API + /channel/done ←────────────┘ ``` ## 12.3 Channel Server The channel server (`src/channel.ts`) is an MCP server that: - Declares the `claude/channel` capability - Connects to Claude Code over stdio (spawned as subprocess) - Listens on a local HTTP port for commands from Hot Sheet - Writes its port to `.hotsheet/channel-port` for Hot Sheet to discover - Forwards HTTP POST content as `notifications/claude/channel` events - Provides a `/health` endpoint for liveness checks Instructions tell Claude: when a hotsheet channel event arrives, run `/hotsheet` to process the worklist, and signal completion when done. ## 12.4 Settings The Claude Channel and custom commands are configured in the **Experimental** settings tab (Lucide Flask icon). This tab is always visible in Settings. - **Claude Channel**: Enable/disable toggle. **Enabled by default for new installs (HS-8492, 2026-05-22)** — prior to HS-8492 the default was `false`. The flip happens inside the one-time `migrateGlobalConfig` boot step in `src/cli.ts`: when neither the global config (`~/.hotsheet/config.json`) nor the legacy per-project DB (`channel_enabled` row in `settings`) has an explicit value, the migration writes `channelEnabled: true`. Users with a persisted value — either branch — are NOT affected; only genuinely first-run installs hit the new default. - The toggle is always visible, but disabled when the `claude` CLI is not detected on the system (`GET /api/channel/claude-check`). - If Claude Code is installed but below v2.1.80, the toggle is disabled with a message to upgrade. - When enabled: registers the channel server in `.mcp.json` (merging with existing entries) and shows launch instructions with a copyable command. - When disabled: removes the channel entry from `.mcp.json`. - Stored as `channelEnabled` boolean in the global config at `~/.hotsheet/config.json`. Legacy per-project `channel_enabled` setting in the database is used as a fallback if the global config has no value. ## 12.5 `.mcp.json` Registration When the channel is enabled, Hot Sheet adds an entry to `.mcp.json` under a per-project key (HS-8349). The key is `hotsheet-channel-` where `` is derived from the basename of the project root via `slugifyDataDir(dataDir)` in `src/channel-config.ts` (lowercase, non-alphanumeric runs collapse to `-`, fallback `project`): **Production** (installed via npm): ```json { "mcpServers": { "hotsheet-channel-myproject": { "command": "node", "args": ["/channel.js", "--data-dir", ""] } } } ``` **Dev mode** (running via tsx): ```json { "mcpServers": { "hotsheet-channel-myproject": { "command": "npx", "args": ["tsx", "/channel.ts", "--data-dir", ""] } } } ``` The path detection uses `dirname(fileURLToPath(import.meta.url))` and checks for `dist/channel.js` first (production), then `src/channel.ts` (dev mode). Existing `.mcp.json` entries are preserved (merge, not overwrite). The legacy single-key `hotsheet-channel` entry (pre-HS-8349) is opportunistically removed by `registerChannel` when it writes the new per-project key, so a one-time upgrade leaves no orphan entry behind. The launch command shown in Settings → Experimental matches the per-project key: `claude --dangerously-load-development-channels server:hotsheet-channel-`. The slug surfaces in Claude Code's tool list as `mcp__hotsheet-channel-__hotsheet_update_ticket`, so multi-project sessions can distinguish each project's MCP tools without spawn-order namespacing collisions. ## 12.6 Port Coordination The channel server writes its listening port to `.hotsheet/channel-port`. Hot Sheet reads this file to know where to send commands. The file is written on channel server startup and cleaned up on exit. ## 12.7 UI — Play Button A green play button (Lucide "play" icon) appears in the sidebar above the custom commands area and the "Copy AI prompt" button when the channel is enabled. ### States | State | Appearance | Trigger | |-------|-----------|---------| | Idle | Green play icon | — | | On-demand fired | Brief pulse animation | Single click | | Automatic mode | Lucide fast-forward icon (replaces play icon) | Double click to enable | | Automatic off | Returns to play icon | Single click while in auto mode | ### Behavior - **Single click**: First verifies the channel is connected via `isChannelAlive()`. If disconnected, shows a "Claude is not connected" alert before proceeding. Then checks for Up Next items. If none, shows a yellow warning alert "No Up Next items to process" (auto-dismisses after 4 seconds). If items exist, flushes any pending debounced markdown syncs (worklist.md, open-tickets.md) to ensure files are up to date, then sends a one-time event to Claude. Button pulses briefly. - **Double click**: Toggles automatic mode. The play icon swaps to a fast-forward icon. - **Single click while in auto mode**: Turns off automatic mode, restores play icon. ## 12.8 Automatic Mode When automatic mode is active: 1. **Immediate trigger** — When entering automatic mode (double-click), Claude is triggered immediately to process the current Up Next items. 2. **Up Next watching** — Hot Sheet then monitors for subsequent `up_next` changes on tickets. Each change restarts the debounce. The initial debounce delay is 5 seconds, but subsequent delays follow the exponential backoff schedule (see below). 3. **Trigger after debounce** — After the debounce expires, Hot Sheet checks for Up Next items and whether Claude is idle: - If Claude is idle and Up Next items exist, a channel event is sent immediately. - If Claude is busy, Hot Sheet retries with the current backoff delay until Claude becomes idle, then triggers. 4. **Exponential backoff** — After each trigger, Hot Sheet verifies Claude became busy within 10 seconds. If not (e.g. Claude is in a weird state), the backoff counter increments. The retry delay doubles with each consecutive failed attempt: 5s → 10s → 20s → 40s → 80s → 120s (max 2 minutes). When Claude successfully picks up work (becomes busy), the backoff resets to 0. 5. **Re-trigger on completion** — When Claude signals done (`/channel/done`), if automatic mode is still active, a new debounce starts to check for remaining or newly added Up Next items. 6. The event content tells Claude to run `/hotsheet` to process the current Up Next items. ## 12.9 Channel Communication ### Hot Sheet → Claude (via channel) Events are sent as MCP `notifications/claude/channel` messages with `{ content, meta: { type: 'worklist' } }` params. Claude Code renders these to the user as XML-wrapped channel events: ``` Process the Hot Sheet worklist. Run /hotsheet to work through the current Up Next items. When you are completely finished processing all items (or if the worklist was empty), signal completion by running: curl -s -X POST http://localhost:/api/channel/done ``` ### Claude → Hot Sheet (via API) Claude communicates back using: 1. **Existing Hot Sheet REST API** — updating ticket status, adding notes, etc. as described in the worklist.md file and AI tool skills. 2. **Completion signal** — `POST /api/channel/done` when finished processing (or when the worklist was empty). This clears the "Claude working" indicator in the UI. ### Busy/Idle Status - When a trigger is sent, the status bar shows "Claude working" with a spinning loader icon. - When Claude calls `/api/channel/done`, the status changes to "✓ Claude idle" (auto-hides after 5 seconds). - The done flag is consumed on read (one-shot) and reset on each new trigger. - A 60-second timeout fallback clears the busy state if Claude never signals completion. ### Heartbeat-Based Busy/Idle Detection Claude Code hooks are installed in `~/.claude/settings.json` when the channel is enabled to provide real-time busy/idle detection: - **PostToolUse** hook sends a "heartbeat" state signal - **UserPromptSubmit** hook sends a "busy" signal - **Stop** hook sends an "idle" signal Each heartbeat or busy signal extends a 30-second sliding timer per project. If no heartbeat is received within 30 seconds, the project is automatically marked idle. The Stop hook immediately clears the busy state without waiting for the timer. Hooks are installed and updated by `installHeartbeatHook()` during server startup and when the channel is enabled via settings. ### PTY-activity busy detection (HS-6702) The hook-based detection above can lag reality: a Claude session that crashes mid-tool-use might not fire a Stop hook, leaving the channel state stuck on "busy" even though Claude is idle. HS-6702 adds a PTY-activity heuristic that complements (does NOT replace) the hooks. The server-side PTY data handler in `src/terminals/registry.ts` stamps two timestamps on every chunk: - `lastOutputAtMs` — wall-clock ms when the PTY last emitted ANY bytes. - `lastSpinnerAtMs` — wall-clock ms when the PTY last emitted one of the Claude busy-spinner glyphs (`· ✢ ✳ ✶ ✻ ✽` — captured from the user's HS-6702 ticket note). Detected via `containsClaudeSpinner` (pure helper in `src/terminals/claudeSpinner.ts`). Both are reset on PTY restart so a stale spinner from the previous Claude session doesn't paint the new process as still busy. `/api/terminal/list` annotates each entry with these timestamps so the client can read them without a separate endpoint. Client side: `src/client/channelUI.tsx` runs a 2-second polling loop while `isChannelBusy()` is true. The poll calls `/api/terminal/list` for the active project, picks the most recent `lastSpinnerAtMs` across all alive terminals, and feeds it to `shouldShowDegradedBusy(channelBusy, lastSpinnerAtMs, now)` (pure helper, 6 unit tests). When the helper returns true (channel busy AND Claude has been spinner-silent for ≥5 s), the indicator renders a "**Claude idle (channel busy)**" label with a paused spinner + muted color — letting the user spot a stuck channel without the false confidence of the regular animated indicator. 11 unit tests in `src/terminals/claudeSpinner.test.ts` cover the spinner-glyph constant, the chunk scanner, and the degraded-busy decision helper across boundary timestamps. ## 12.10 Permission Relay When Claude needs approval to run a tool (Bash, Write, Edit, etc.), the channel server receives a permission request notification and forwards it to Hot Sheet. ### How it works 1. The channel server declares `claude/channel/permission` capability 2. When Claude calls a tool that needs approval, Claude Code sends `notifications/claude/channel/permission_request` to the channel server 3. The channel server enqueues the pending request into a FIFO queue (`src/channelPermissions.ts`) and exposes the queue **head** via `GET /permission`. Pre-HS-8047 the channel server held a single `pendingPermission` slot — a follow-up `permission_request` silently overwrote the prior one, the popup the user was looking at vanished, and the first request could never be responded to from the UI. The queue preserves every concurrently-pending request in arrival order; once the user responds to (or dismisses) the head, the next 100 ms client poll surfaces the next queued request. Wire shape on `/permission` stays single-slot (`{ pending: head | null }`), so client + main-server are oblivious to the queue beneath 4. Hot Sheet long-polls `GET /api/channel/permission` — the server holds the connection up to 3 seconds, returning immediately when a permission arrives (the channel server notifies via `POST /api/channel/permission/notify`) - **HS-9036 — polls every alive channel server, not just the leader.** Each Claude instance (the main agent AND each git-worktree worker, HS-8936) spawns its own channel server, all registered under the owner data dir (`channel-ports.d/`). `fetchPermission` polls `listAliveEntries(dataDir)` (the leader was the only one polled before), so a permission raised inside a **worker** surfaces too. It records `request_id → source port` so `POST /api/channel/permission/respond` routes the answer back to the server that raised it (and `…/dismiss` clears every alive server). - **HS-9036 (the actual root cause — step 2 above).** The multi-server polling alone wasn't enough: a worker's Claude never *sent* `permission_request` to its channel server, so EVERY worker permission fell back to the worker's terminal and never reached Hot Sheet. The reason: the worker launched as a bare `claude "/hotsheet-worker"` — it connected to the channel MCP (the `hotsheet_*` tools worked) but lacked the **`--dangerously-load-development-channels server:hotsheet-channel-`** flag that the main project's Claude command carries (`claudeWithChannelCommand`). That flag is what opts Claude Code into routing permission prompts (and channel events) to the channel server. Fix: `workerLaunchCommand(ownerDataDir)` (`src/workers/launchWorker.ts`) now prepends the same flag (keyed to the OWNER data dir, where the worker's channel server registers). Existing workers must be **relaunched** to pick it up (the flag is set at `claude` start). 5. When a pending permission is detected, the popup described below appears anchored to the owning project's tab ### Popup (single codepath for active and non-active tabs) Every pending permission — whether it arrived for the active project or a background one — renders as a popup anchored below the owning project's tab (HS-6536; the previous full-screen "Claude is waiting for permission" overlay was removed). The popup contains: - Tool name, full description (wraps to multiple lines), and — when present — the `input_preview` block in a monospace code box (HS-6476). Max popup width is the smaller of 640 px or the viewport minus 16 px; the preview block scrolls vertically if taller than 240 px - Claude's raw `input_preview` JSON is formatted for readability: Bash commands show just the `command` field; other known tools (Read, WebFetch, WebSearch, Glob, NotebookRead) show their most useful field; unrecognised JSON is flattened to `key: value` lines. Truncated JSON (common for long Bash commands) still gets primary-field extraction with a trailing ellipsis (HS-6634) - Compact green Allow (checkmark) and red Deny (X) icon buttons on the right - Two text links at the bottom-left of the popup (HS-7266): - **"Minimize"** drops the popup into the owning project tab's pulsating blue dot — the dot advertises a waiting check, clicking the tab re-shows the same popup (switching to that project if needed), and the minimized popup auto-dismisses after 2 minutes so it can't linger forever (HS-6637) - **"No response needed"** dismisses the popup outright (use this when the user plans to respond via Claude directly) — the request stays pending server-side, the attention dot stays blue, and the popup does not re-appear until the channel server resolves the request (HS-6637) - The associated project tab gets a highlighted background (blue border/tint) to indicate which tab the permission belongs to - Popup horizontal position is clamped to the viewport after layout so a wide popup opened from a right-edge tab does not overflow off-screen - The popup is **non-modal** (HS-7266). Clicks elsewhere in the UI — on tickets, the sidebar, other tabs, the drawer, etc. — are delivered normally; the popup does not dismiss or minimize on outside clicks. The only ways to close it are Allow, Deny, the **Minimize** link, or the **No response needed** link. Earlier behavior (HS-6637 v1) routed outside-clicks through a capture-phase handler that minimized the popup and swallowed the click on the owning tab to prevent a toggle-bounce; that handler was removed because it blocked legitimate interactions with other parts of the UI while a permission was pending - Responding via the popup clears the attention dot - Allow/Deny POST `/api/channel/permission/respond` with the **owning project's** secret in `X-Hotsheet-Secret`, so a response initiated from a background-project popup still routes correctly. The body also includes the `tool_name`, `description`, and `input_preview` the client already has, so the server-side command-log entry is detail-rich even if the respond races ahead of the long-poll's `permission_request` log entry (HS-6477) The pending permission expires on the channel server after 120 seconds if not acted on; the popup auto-closes the next poll cycle when the server reports no pending request. If the user had minimized the popup, the minimized record is GC'd the same way. Note: The local terminal dialog stays open in parallel. Whichever is answered first (Hot Sheet or terminal) takes effect. ## 12.11 Custom Commands When the Claude Channel is enabled, users can create custom command buttons that appear below the play button in the sidebar. ### Configuration In Settings → Experimental → Custom Commands: - Click "Add Command" to create a new command - Each command has: - **Color** — chosen from a dropdown palette of 9 colors (Neutral, Blue, Green, Orange, Red, Purple, Pink, Teal, Gray). Defaults to Neutral (#e5e7eb). Text/icon color auto-computed for contrast. - **Icon** — chosen from a picker with all 1693 Lucide icons. 24 featured action icons shown at top, with search to find any icon by name. - **Name** — button label text - **Prompt** — text sent to Claude when clicked - Commands can be reordered by dragging the hamburger handle - Commands are stored as JSON in the `custom_commands` settings key ### UI - Custom command buttons appear in the sidebar below the play button - Each button shows icon + left-aligned name on a colored background - Clicking a button sends the configured prompt to Claude via the channel - The completion signal (`/channel/done`) is automatically appended to all prompts ### Example Name: `Commit Changes` Prompt: `Make a commit message for the recently completed tickets, without wrapping long lines. Add all unstaged changes to the git commit. Git commit with the message you generated but don't push.` ## 12.11.1 Multiple Claude connections (HS-8460 / HS-8948) Claude Code spawns one MCP channel-server child per Claude instance, so two Claudes open in the same project mean two channel-servers. Each registers a `/channel-ports.d/.json` entry (`src/channelRegistry.ts`); the **leader** (oldest by `startedAt`) is the one triggers route to. `GET /api/channel/status` returns `aliveCount` and the client shows a **"N Claude connections active"** warning when it's >1. **Orphan problem (HS-8948).** A Claude instance can exit while its spawned channel-server child keeps running (not reaped), so its pid stays alive, `listAliveEntries` keeps counting it, and the warning never clears — with no way to fix it. Mitigation: - **Diagnostic logging:** when `aliveCount > 1`, the status route logs the roster (`multi-connection` event in `mcp.log` — pids / startedAt / leader), deduped per dataDir so the polled route logs only a real change. - **Cleanup affordance:** the warning has a **"Clean up"** button → `POST /api/channel/cleanup-connections` → `cleanupExtraConnections(dataDir)` terminates every alive channel-server EXCEPT the leader (SIGTERM) + removes their registry entries, so only the connection that actually receives triggers remains. (Root cause is orphaned MCP children outside Hot Sheet's direct lifecycle control; the cleanup is the durable mitigation.) ## 12.12 API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/api/channel/claude-check` | GET | Check if `claude` CLI is installed and meets minimum version (v2.1.80+) | | `/api/channel/status` | GET | Returns `{ enabled, alive, port, done, versionMismatch, serverName, aliveCount }` — channel state, completion flag, version check, and the count of alive channel-servers (HS-8460) | | `/api/channel/cleanup-connections` | POST | HS-8948 — terminate duplicate channel-servers (keep the leader); returns `{ ok, killed }` | | `/api/channel/trigger` | POST | Send a worklist event to Claude via the channel server | | `/api/channel/done` | POST | Called by Claude to signal it has finished processing | | `/api/channel/enable` | POST | Enable the channel and register in `.mcp.json` | | `/api/channel/disable` | POST | Disable the channel and remove from `.mcp.json` | | `/api/channel/permission` | GET | Check for pending permission requests from Claude | | `/api/channel/permission/respond` | POST | Respond to a permission request (`{ request_id, behavior }`) | | `/api/channel/permission/dismiss` | POST | Dismiss a pending permission overlay without responding | | `/api/channel/notify` | POST | Notify long-poll of channel state changes (used internally by channel server) | | `/api/channel/permission/notify` | POST | Wake the permission long-poll when a new permission request arrives (used internally by channel server) | | `/api/channel/heartbeat` | POST | Receive busy / idle / heartbeat state from the Claude Code hooks (`{ projectDir, state }`; auth-skipped — see §12.9) | | `/api/channel/heartbeat-status` | GET | Return and clear accumulated heartbeat updates (`{ updates: [{ secret, state }] }`) for the long-poll | ## 12.13 MCP Tools The channel server exposes a typed tool surface for AI agents. See [63-mcp-tools.md](63-mcp-tools.md) for the full tool reference. Tools internally proxy to the local Hot Sheet HTTP API documented in §9 — there is no duplicated handler tree. Phase 1 (HS-8346) + Phase 2 (HS-8347) shipped — 14 tools live: `hotsheet_update_ticket`, `hotsheet_create_ticket`, `hotsheet_get_ticket`, `hotsheet_delete_ticket`, `hotsheet_restore_ticket`, `hotsheet_toggle_up_next`, `hotsheet_duplicate_tickets`, `hotsheet_batch`, `hotsheet_edit_note`, `hotsheet_delete_note`, `hotsheet_query_tickets`, `hotsheet_signal_done`, `hotsheet_add_attachment`, `hotsheet_request_feedback`. Phases 3–4 land per the phasing in `docs/63-mcp-tools.md` §63.7. The REST API documented above is the universal interface and the source of truth for input validation; MCP tools are an additional access path for AI agents connected over the Claude Channel, not a replacement. ## 12.14 Requirements - Claude Code v2.1.80+ with claude.ai login - `@modelcontextprotocol/sdk` npm package (dependency of the channel server)