* build: vendor xterm@5 for the Terminal sidebar tab
Adds xterm@5 + xterm-addon-fit as devDependencies and a `vendor:xterm`
build step that copies the assets into `extension/lib/` at build time.
The vendored files are .gitignored so the npm version stays the source
of truth. xterm@5 is eval-free, so no MV3 CSP changes needed.
No runtime callers yet — this just stages the assets.
* feat(server): add pty-session-cookie module for the Terminal tab
Mirrors `sse-session-cookie.ts` exactly. Mints short-lived 30-min HttpOnly
cookies for authenticating the Terminal-tab WebSocket upgrade against
the terminal-agent. Same TTL, same opportunistic-pruning shape, same
"scoped tokens never valid as root" invariant. Two registries instead of
one because the cookie names are different (`gstack_sse` vs `gstack_pty`)
and the token spaces must not overlap.
No callers yet — wired up in the next commit.
* feat(server): add terminal-agent.ts (PTY for the Terminal sidebar tab)
Translates phoenix gbrowser's Go PTY (cmd/gbd/terminal.go) into a Bun
non-compiled process. Lives separately from `sidebar-agent.ts` so a
WS-framing or PTY-cleanup bug can't take down the chat path (codex
outside-voice review caught the coupling risk).
Architecture:
- Bun.serve on 127.0.0.1:0 (never tunneled).
- POST /internal/grant accepts cookie tokens from the parent server over
loopback, authenticated with a per-boot internal token.
- GET /ws upgrades require BOTH (a) Origin: chrome-extension://<id> and
(b) the gstack_pty cookie minted by /pty-session. Either gate alone is
insufficient (CSWSH defense + auth defense).
- Lazy spawn: claude PTY is not started until the WS receives its first
data frame. Idle sidebar opens cost nothing.
- Bun PTY API: `terminal: { rows, cols, data(t, chunk) }` — verified at
impl time on Bun 1.3.10. proc.terminal.write() for input,
proc.terminal.resize() for resize, proc.kill() + 3s SIGKILL fallback
on close.
- process.on('uncaughtException'|'unhandledRejection') handlers so a
framing bug logs but doesn't kill the listener loop.
Test-only `BROWSE_TERMINAL_BINARY` env override lets the integration
tests spawn /bin/bash instead of requiring claude on every CI runner.
Not yet spawned by anything — wired in the next commit.
* feat(server): wire /pty-session route + spawn terminal-agent
Server-side glue connecting the Terminal sidebar tab to the new
terminal-agent process.
server.ts:
- New POST /pty-session route. Validates AUTH_TOKEN, mints a gstack_pty
HttpOnly cookie via pty-session-cookie.ts, posts the cookie value to
the agent's loopback /internal/grant. Returns the terminalPort + Set-Cookie
to the extension.
- /health response gains `terminalPort` (just the port number — never a
shell token). Tokens flow via the cookie path, never /health, because
/health already surfaces AUTH_TOKEN to localhost callers in headed mode
(that's a separate v1.1+ TODO).
- /pty-session and /terminal/* are deliberately NOT added to TUNNEL_PATHS,
so the dual-listener tunnel surface 404s by default-deny.
- Shutdown path now also pkills terminal-agent and unlinks its state files
(terminal-port + terminal-internal-token) so a reconnect doesn't try to
hit a dead port.
cli.ts:
- After spawning sidebar-agent.ts, also spawn terminal-agent.ts. Same
pattern: pkill old instances, Bun.spawn(['bun', 'run', script]) with
BROWSE_STATE_FILE + BROWSE_SERVER_PORT env. Non-fatal if the spawn
fails — chat still works without the terminal agent.
* feat(extension): Terminal as default sidebar tab
Adds a primary tab bar (Terminal | Chat) above the existing tab-content
panes. Terminal is the default-active tab; clicking Chat returns to the
existing claude -p one-shot flow which is preserved verbatim.
manifest.json: adds ws://127.0.0.1:*/ to host_permissions so MV3 doesn't
block the WebSocket upgrade.
sidepanel.html: new primary-tabs nav, new #tab-terminal pane with a
"Press any key to start Claude Code" bootstrap card, claude-not-found
install card, xterm mount point, and "session ended" restart UI. Loads
xterm.js + xterm-addon-fit + sidepanel-terminal.js. tab-chat is no
longer the .active default.
sidepanel.js: new activePrimaryPaneId() helper that reads which primary
tab is selected. Debug-close paths now route back to whichever primary
pane is active (was hardcoded to tab-chat). Primary-tab click handler
toggles .active classes and aria-selected. window.gstackServerPort and
window.gstackAuthToken exposed so sidepanel-terminal.js can build the
/pty-session POST and the WS URL.
sidepanel-terminal.js (new): xterm.js lifecycle. Lazy-spawn — first
keystroke fires POST /pty-session, then opens
ws://127.0.0.1:<terminalPort>/ws. Origin + cookie are set automatically
by the browser. Resize observer sends {type:"resize"} text frames.
ResizeObserver, tab-switch hooks, restart button, install-card retry.
On WS close shows "Session ended, click to restart" — no auto-reconnect
(codex outside-voice flagged that as session-burning).
sidepanel.css: primary-tabs bar + Terminal pane styling (full-height
xterm container, install card, ended state).
* test: terminal-agent + cookie module + sidebar default-tab regression
Three new test files:
terminal-agent.test.ts (16 tests): pty-session-cookie mint/validate/
revoke, Set-Cookie shape (HttpOnly + SameSite=Strict + Path=/, NO Secure
since 127.0.0.1 over HTTP), source-level guards that /pty-session and
/terminal/* are NOT in TUNNEL_PATHS, /health does NOT surface ptyToken
or gstack_pty, terminal-agent binds 127.0.0.1, /ws upgrade enforces
chrome-extension:// Origin AND gstack_pty cookie, lazy-spawn invariant
(spawnClaude is called from message handler, not upgrade), uncaughtException/
unhandledRejection handlers exist, SIGINT-then-SIGKILL cleanup.
terminal-agent-integration.test.ts (7 tests): spawns the agent as a real
subprocess in a tmp state dir. Verifies /internal/grant accepts/rejects
the loopback token, /ws gates (no Origin → 403, bad Origin → 403, no
cookie → 401), real WebSocket round-trip with /bin/bash via the
BROWSE_TERMINAL_BINARY override (write 'echo hello-pty-world\n', read it
back), and resize message acceptance.
sidebar-tabs.test.ts (13 tests): structural regression suite locking the
load-bearing invariants of the default-tab change — Terminal is .active,
Chat is not, xterm assets are loaded, debug-close path no longer hardcodes
tab-chat (uses activePrimaryPaneId), primary-tab click handler exists,
chat surface is not accidentally deleted, terminal JS does NOT auto-
reconnect on close, manifest declares ws:// + http:// localhost host
permissions, no unsafe-eval.
Plan called for Playwright + extension regression; the codebase doesn't
ship Playwright extension launcher infra, so we follow the existing
extension-test pattern (source-level structural assertions). Same
load-bearing intent — locks the invariants before they regress.
* docs: Terminal flow + threat model + v1.1 follow-ups
SIDEBAR_MESSAGE_FLOW.md: new "Terminal flow" section. Documents the WS
upgrade path (/pty-session cookie mint → /ws Origin + cookie gate →
lazy claude spawn), the dual-token model (AUTH_TOKEN for /pty-session,
gstack_pty cookie for /ws, INTERNAL_TOKEN for server↔agent loopback),
and the threat-model boundary — the Terminal tab bypasses the entire
prompt-injection security stack on purpose; user keystrokes are the
trust source. That trust assumption is load-bearing on three transport
guarantees: local-only listener, Origin gate, cookie auth. Drop any
one of those three and the tab becomes unsafe.
CLAUDE.md: extends the "Sidebar architecture" note to include
terminal-agent.ts in the read-this-first list. Adds a "Terminal tab is
its own process" note so a future contributor doesn't bolt PTY logic
onto sidebar-agent.ts.
TODOS.md: three new follow-ups under a new "Sidebar Terminal" section:
- v1.1: PTY session survives sidebar reload (Issue 1C deferred).
- v1.1+: audit /health AUTH_TOKEN distribution (codex finding #2 —
a pre-existing soft leak that cc-pty-import sidesteps but doesn't
fix).
- v1.1+: apply terminal-agent's process.on exception handlers to
sidebar-agent.ts (codex finding #4 — chat path has no fatal
handlers).
* feat(extension): Terminal-only sidebar — auth fix, UX polish, chat rip
The chat queue path is gone. The Chrome side panel is now just an
interactive claude PTY in xterm.js. Activity / Refs / Inspector still
exist behind the `debug` toggle in the footer.
Three threads of change, all from dogfood iteration on top of
cc-pty-import:
1. fix(server): cross-port WS auth via Sec-WebSocket-Protocol
- Browsers can't set Authorization on a WebSocket upgrade. We had
been minting an HttpOnly gstack_pty cookie via /pty-session, but
SameSite=Strict cookies don't survive the cross-port jump from
server.ts:34567 to the agent's random port from a chrome-extension
origin. The WS opened then immediately closed → "Session ended."
- /pty-session now also returns ptySessionToken in the JSON body.
- Extension calls `new WebSocket(url, [`gstack-pty.<token>`])`.
Browser sends Sec-WebSocket-Protocol on the upgrade.
- Agent reads the protocol header, validates against validTokens,
and MUST echo the protocol back (Chromium closes the connection
immediately if a server doesn't pick one of the offered protocols).
- Cookie path is kept as a fallback for non-browser callers (curl,
integration tests).
- New integration test exercises the full protocol-auth round-trip
via raw fetch+Upgrade so a future regression of this exact class
fails in CI.
2. fix(extension): UX polish on the Terminal pane
- Eager auto-connect when the sidebar opens — no "Press any key to
start" friction every reload.
- Always-visible ↻ Restart button in the terminal toolbar (not
gated on the ENDED state) so the user can force a fresh claude
mid-session.
- MutationObserver on #tab-terminal's class attribute drives a
fitAddon.fit() + term.refresh() when the pane becomes visible
again — xterm doesn't auto-redraw after display:none → display:flex.
3. feat(extension): rip the chat tab + sidebar-agent.ts
- Sidebar is Terminal-only. No more Terminal | Chat primary nav.
- sidebar-agent.ts deleted. /sidebar-command, /sidebar-chat,
/sidebar-agent/event, /sidebar-tabs* and friends all deleted.
- The pickSidebarModel router (sonnet vs opus) is gone — the live
PTY uses whatever model the user's `claude` CLI is configured with.
- Quick-actions (🧹 Cleanup / 📸 Screenshot / 🍪 Cookies) survive
in the Terminal toolbar. Cleanup now injects its prompt into the
live PTY via window.gstackInjectToTerminal — no more
/sidebar-command POST. The Inspector "Send to Code" action uses
the same injection path.
- clear-chat button removed from the footer.
- sidepanel.js shed ~900 lines of chat polling, optimistic UI,
stop-agent, etc.
Net diff: -3.4k lines across 16 files. CLAUDE.md, TODOS.md, and
docs/designs/SIDEBAR_MESSAGE_FLOW.md rewritten to match. The sidebar
regression test (browse/test/sidebar-tabs.test.ts) is rewritten as 27
structural assertions locking the new layout — Terminal sole pane,
no chat input, quick-actions in toolbar, eager-connect, MutationObserver
repaint, restart helper.
* feat: live tab awareness for the Terminal pane
claude in the PTY now has continuous tab-aware context. Three pieces:
1. Live state files. background.js listens to chrome.tabs.onActivated /
onCreated / onRemoved / onUpdated (throttled to URL/title/status==
complete so loading spinners don't spam) and pushes a snapshot. The
sidepanel relays it as a custom event; sidepanel-terminal.js sends
{type:"tabState"} text frames over the live PTY WebSocket.
terminal-agent.ts writes:
<stateDir>/tabs.json all open tabs (id, url, title, active,
pinned, audible, windowId)
<stateDir>/active-tab.json current active tab (skips chrome:// and
chrome-extension:// internal pages)
Atomic write via tmp + rename so claude never reads a half-written
document. A fresh snapshot is pushed on WS open so the files exist by
the time claude finishes booting.
2. New $B tab-each <command> [args...] meta-command. Fans out a single
command across every open tab, returns
{command, args, total, results: [{tabId, url, title, status, output}]}.
Skips chrome:// pages; restores the originally active tab in a finally
block (so a mid-batch error doesn't leave the user looking at a
different tab); uses bringToFront: false so the OS window doesn't
jump on every fanout. Scope-checks the inner command BEFORE the loop.
3. --append-system-prompt hint at spawn time. Claude is told about both
the state files and the $B tab-each command up front, so it doesn't
have to discover the surface by trial. Passed via the --append-system-
prompt CLI flag, NOT as a leading PTY write — the hint stays out of
the visible transcript.
Tests:
- browse/test/tab-each.test.ts (new) — registration + source-level
invariants (scope check before loop, finally-restore, bringToFront:false,
chrome:// skip) + behavior tests with a mock BrowserManager that verify
iteration order, JSON shape, error handling, and active-tab restore.
- browse/test/terminal-agent.test.ts — three new assertions for
tabState handler shape, atomic-write pattern, and the
--append-system-prompt wiring at spawn.
Verified live: opened 5 tabs, ran $B tab-each url against the live
server, got per-tab JSON results back, original active tab restored
without OS focus stealing.
* chore: drop sidebar-agent test refs after chat rip
Five test files / describe blocks targeted the deleted chat path:
- browse/test/security-e2e-fullstack.test.ts (full-stack chat-pipeline E2E
with mock claude — whole file gone)
- browse/test/security-review-fullstack.test.ts (review-flow E2E with real
classifier — whole file gone)
- browse/test/security-review-sidepanel-e2e.test.ts (Playwright E2E for
the security event banner that was ripped from sidepanel.html)
- browse/test/security-audit-r2.test.ts (5 describe blocks: agent queue
permissions, isValidQueueEntry stateFile traversal, loadSession session-ID
validation, switchChatTab DocumentFragment, pollChat reentrancy guard,
/sidebar-tabs URL sanitization, sidebar-agent SIGTERM→SIGKILL escalation,
AGENT_SRC top-level read converted to graceful fallback)
- browse/test/security-adversarial-fixes.test.ts (canary stream-chunk split
detection on detectCanaryLeak; one tool-output test on sidebar-agent)
- test/skill-validation.test.ts (sidebar agent #584 describe block)
These all assumed sidebar-agent.ts existed and tested chat-queue plumbing,
chat-tab DOM round-trip, chat-polling reentrancy, or per-message classifier
canary detection. With the live PTY there is no chat queue, no chat tab,
no LLM stream to canary-scan, and no per-message subprocess. The Terminal
pane's invariants are covered by the new browse/test/sidebar-tabs.test.ts
(27 structural assertions), browse/test/terminal-agent.test.ts, and
browse/test/terminal-agent-integration.test.ts.
bun test → exit 0, 0 failures.
* chore: bump version and changelog (v1.14.0.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(extension): xterm fills the full Terminal panel height
The Terminal pane only rendered into the top portion of the panel — most
of the panel below the prompt was an empty black gap. Three layered
issues, all about xterm.js measuring dimensions during a layout state
that wasn't ready yet:
1. order-of-operations in connect(): ensureXterm() ran BEFORE
setState(LIVE), so term.open() measured els.mount while it was still
display:none. xterm caches a 0-size viewport synchronously inside
open() and never auto-recovers when the container goes visible.
Flipped: setState(LIVE) → ensureXterm.
2. first fit() ran synchronously before the browser had applied the
.active class transition. Wrapped in requestAnimationFrame so layout
has settled before fit() reads clientHeight.
3. CSS flex-overflow trap: .terminal-mount has flex:1 inside the
flex-column #tab-terminal, but .tab-content's `overflow-y: auto` and
the lack of `min-height: 0` on .terminal-mount meant the item
couldn't shrink below content size. flex:1 then refused to expand
into available space and xterm rendered into whatever its initial
2x2 measurement happened to be.
Fixes:
- extension/sidepanel-terminal.js: reorder + RAF fit
- extension/sidepanel.css: .terminal-mount gets `flex: 1 1 0` +
`min-height: 0` + `position: relative`. #tab-terminal overrides
.tab-content's `overflow-y: auto` to `overflow: hidden` (xterm has
its own viewport scroll; the parent shouldn't compete) and explicitly
re-declares `display: flex; flex-direction: column` for #tab-terminal.active.
bun test browse/test/sidebar-tabs.test.ts → 27/27 pass.
Manually verified: side panel opens → Terminal fills full panel height,
xterm scrollback works, debug-tab toggle still repaints correctly.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
Sidebar Flow
How the GStack Browser sidebar actually works. Read this before touching
sidepanel.js, background.js, content.js, terminal-agent.ts, or
sidebar-related server endpoints.
The sidebar has one primary surface — the Terminal pane, an interactive
claude PTY. Activity / Refs / Inspector survive as debug overlays behind
the debug toggle in the footer. The chat queue path (one-shot claude -p,
sidebar-agent.ts) was ripped once the PTY proved out — the Terminal pane is
strictly more capable.
Components
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ sidepanel.js + │────▶│ server.ts │────▶│terminal-agent.ts │
│ -terminal.js │ │ (compiled) │ │ (non-compiled) │
│ (xterm.js) │ │ │ │ PTY listener │
└─────────────────┘ └──────────────┘ └──────────────────┘
▲ │ │
│ ws://127.0.0.1:<termPort>/ws (Sec-WebSocket-Protocol auth)
└───────────────────────┼──────────────────────▶│ Bun.spawn(claude)
│ │ terminal: {data}
│ ▼
│ ┌──────────────────┐
│ │ claude PTY │
│ └──────────────────┘
POST /pty-session │
(Bearer AUTH_TOKEN) │
▼
┌──────────────────┐
│ pty-session- │
│ cookie.ts │
│ (in-memory token │
│ registry) │
└──────────────────┘
│
│ POST /internal/grant (loopback)
▼
┌──────────────────┐
│ validTokens Set │
│ in agent memory │
└──────────────────┘
The compiled browse server can't posix_spawn external executables —
terminal-agent.ts runs as a separate non-compiled bun run process and
owns the claude subprocess.
Startup + first-keystroke timeline
T+0ms CLI runs `$B connect`
├── Server starts (compiled)
└── Spawns terminal-agent.ts via `bun run`
T+500ms terminal-agent.ts boots
├── Bun.serve on 127.0.0.1:0 (random port)
├── Writes <stateDir>/terminal-port (server reads it for /health)
├── Writes <stateDir>/terminal-internal-token (loopback handshake)
└── Probes claude → writes claude-available.json
T+1-3s Extension loads, sidebar opens
├── sidepanel-terminal.js: setState(IDLE), shows "Starting Claude Code..."
└── tryAutoConnect() polls until window.gstackServerPort + token are set
T+ready tryAutoConnect calls connect()
├── POST /pty-session (Authorization: Bearer AUTH_TOKEN)
│ └── server mints session token, posts /internal/grant to agent
│ └── responds with {terminalPort, ptySessionToken}
├── GET /claude-available (preflight)
├── new WebSocket(`ws://127.0.0.1:<terminalPort>/ws`,
│ [`gstack-pty.<token>`])
│ └── Browser sends Sec-WebSocket-Protocol + Origin
│ └── Agent validates Origin AND token BEFORE upgrading
│ └── Agent echoes the protocol back (REQUIRED — browser
│ closes the connection without it)
├── On open: send {type:"resize"} then a single \n byte
└── Agent message handler sees the byte → spawnClaude()
Auth: WebSocket can't send Authorization headers
Browser WebSocket clients can't set Authorization. They CAN set
Sec-WebSocket-Protocol via the second arg of new WebSocket(url, protocols). We exploit that:
POST /pty-session(auth: Bearer AUTH_TOKEN) → server mints a short-lived session token, pushes it to the agent over loopback, returns it in the JSON body.- Extension calls
new WebSocket(url, ['gstack-pty.<token>']). - Agent reads
Sec-WebSocket-Protocol, stripsgstack-pty., validates againstvalidTokens, echoes the protocol back. Echo is mandatory — without it Chromium closes the connection on receipt of the upgrade response.
A Set-Cookie: gstack_pty=... header is also returned for non-browser
callers (curl, integration tests). The cookie path was the original v1
design but SameSite=Strict cookies don't survive the cross-port jump
from server.ts:34567 → agent: from a chrome-extension origin.
The protocol-token path is what the browser actually uses.
Dual-token model
| Token | Lives in | Used for | Lifetime |
|---|---|---|---|
AUTH_TOKEN |
<stateDir>/browse.json; in-memory in server.ts |
/pty-session POST (mint cookie + token) |
server lifetime |
gstack-pty.<...> (Sec-WebSocket-Protocol) |
Browser memory only; agent validTokens Set |
/ws upgrade auth |
30 min, auto-revoked on WS close |
INTERNAL_TOKEN |
<stateDir>/terminal-internal-token; in agent memory |
server → agent loopback /internal/grant |
agent lifetime |
AUTH_TOKEN is never valid for /ws directly. The session token is
never valid for /pty-session or /command. Strict separation
prevents an SSE or page-content token leak from escalating into shell
access.
Threat model
The Terminal pane bypasses the prompt-injection security stack on purpose — the user is typing directly to claude, there's no untrusted page content in the loop. Trust source is the keyboard, same as any local terminal.
That trust assumption is load-bearing on three transport guarantees:
- Local-only listener. terminal-agent.ts binds
127.0.0.1only. The dual-listener tunnel surface (server.tsTUNNEL_PATHS) does not include/pty-sessionor/terminal/*, so the tunnel returns 404 by default-deny. - Origin gate.
/wsupgrades requireOrigin: chrome-extension://<id>. A localhost web page can't mount a cross-site WebSocket hijack against the shell because its Origin is a regularhttp(s)://.... - Session token auth. Minted only by an authenticated
/pty-sessionPOST, scoped to one WS, auto-revoked on close.
Drop any one of those three and the whole tab becomes unsafe.
Lifecycle
- Eager auto-connect. Sidebar opens → tryAutoConnect polls for the bootstrap globals and connects as soon as they're set. No keypress required.
- One PTY per WS. Closing the WebSocket SIGINTs claude, then SIGKILLs after 3s. The session token is revoked so a stolen token can't be replayed.
- No auto-reconnect on close. The user sees "Session ended, click to start a new session." Auto-reconnect would burn a fresh claude session on every reload. v1.1 may add session resumption keyed on tab/session id (see TODOS).
- Manual restart anytime. A
↻ Restartbutton lives in the always- visible terminal toolbar — works mid-session, not just from the ENDED state.
Quick-action toolbar
Three browser-action buttons live next to the Restart button at the top of the Terminal pane:
| Button | Behavior |
|---|---|
| 🧹 Cleanup | window.gstackInjectToTerminal(prompt) — pipes a "remove ads/banners" instruction into the live PTY. claude in the terminal sees it and acts. |
| 📸 Screenshot | POST /command screenshot — direct browse-server call, no PTY involvement. |
| 🍪 Cookies | Navigates to the /cookie-picker page. |
The Inspector's "Send to Code" button uses the same gstackInjectToTerminal
path to forward CSS inspector data into claude.
Debug surfaces (Activity / Refs / Inspector)
Behind the debug toggle in the footer. SSE-driven, independent of the
Terminal pane:
- Activity — streams every browse command via
/activity/streamSSE. - Refs — REST:
GET /refs— current page's@refelement labels. - Inspector — CDP-based element picker; SSE on
/inspector/events.
When the debug strip closes, the Terminal pane re-becomes visible.
xterm.js doesn't auto-redraw when its container flips from display:none
to display:flex, so sidepanel-terminal.js runs a MutationObserver on
#tab-terminal's class attribute and forces a fit + refresh when
.active returns.
Files
| Component | File | Runs in |
|---|---|---|
| Sidebar UI shell | extension/sidepanel.html + sidepanel.js + sidepanel.css |
Chrome side panel |
| Terminal UI | extension/sidepanel-terminal.js + extension/lib/xterm.js |
Chrome side panel |
| Service worker | extension/background.js |
Chrome background |
| Content script | extension/content.js |
Page context |
| HTTP server | browse/src/server.ts |
Bun (compiled binary) |
| PTY agent | browse/src/terminal-agent.ts |
Bun (non-compiled) |
| PTY token store | browse/src/pty-session-cookie.ts |
Bun (compiled, in server.ts) |
| CLI entry | browse/src/cli.ts |
Bun (compiled binary) |
| State file | <stateDir>/browse.json |
Filesystem |
| Terminal port | <stateDir>/terminal-port |
Filesystem |
| Internal token | <stateDir>/terminal-internal-token |
Filesystem |
| Claude probe | <stateDir>/claude-available.json |
Filesystem |
| Active tab | <stateDir>/active-tab.json |
Filesystem (claude reads) |