This file provides guidance to Claude Code & Codex when working with code in this repository.
The Companion — a web UI for Claude Code & Codex.
It reverse-engineers the undocumented --sdk-url WebSocket protocol in the Claude Code CLI to provide a browser-based interface for running multiple Claude Code sessions with streaming, tool call visibility, and permission control.
# Dev server (Hono backend on :3456 + Vite HMR on :5174)
cd web && bun install && bun run dev
# Or from repo root
make dev
# Type checking
cd web && bun run typecheck
# Production build + serve
cd web && bun run build && bun run start
# Auth token management
cd web && bun run generate-token # show current token
cd web && bun run generate-token --force # regenerate a new token
# Landing page (thecompanion.sh) — idempotent: starts if down, no-op if up
# IMPORTANT: Always use this script to run the landing page. Never cd into landing/ and run bun/vite manually.
./scripts/landing-start.sh # start
./scripts/landing-start.sh --stop # stop# Run tests
cd web && bun run test
# Watch mode
cd web && bun run test:watch- All new backend (
web/server/) and frontend (web/src/) code must include tests when possible. - Every new or modified frontend component (
web/src/components/) must have an accompanying.test.tsxfile with at minimum: a render test, an axe accessibility scan (toHaveNoViolations()), and tests for any interactive behavior (clicks, keyboard shortcuts, state changes). - Tests use Vitest. Server tests live alongside source files (e.g.
routes.test.tsnext toroutes.ts). - A husky pre-commit hook runs typecheck and tests automatically before each commit.
- Never remove or delete existing tests. If a test is failing, fix the code or the test. If you believe a test should be removed, you must first explain to the user why and get explicit approval before removing it.
- When creating test, make sure to document what the test is validating, and any important context or edge cases in comments within the test code.
All UI components used in the message/chat flow must be represented in the Playground page (web/src/components/Playground.tsx, accessible at #/playground). When adding or modifying a message-related component (e.g. MessageBubble, ToolBlock, PermissionBanner, Composer, streaming indicators, tool groups, subagent groups), update the Playground to include a mock of the new or changed state.
Browser (React) ←→ WebSocket ←→ Hono Server (Bun) ←→ WebSocket (NDJSON) ←→ Claude Code CLI
:5174 /ws/browser/:id :3456 /ws/cli/:id (--sdk-url)
- Browser sends a "create session" REST call to the server
- Server spawns
claude --sdk-url ws://localhost:3456/ws/cli/SESSION_IDas a subprocess - CLI connects back to the server over WebSocket using NDJSON protocol
- Server bridges messages between CLI WebSocket and browser WebSocket
- Tool calls arrive as
control_request(subtypecan_use_tool) — browser renders approval UI, server relayscontrol_responseback
-
web/server/— Hono + Bun backend (runs on port 3456)index.ts— Server bootstrap, Bun.serve with dual WebSocket upgrade (CLI vs browser)ws-bridge.ts— Core message router. Maintains per-session state (CLI socket, browser sockets, message history, pending permissions). Parses NDJSON from CLI, translates to typed JSON for browsers.cli-launcher.ts— Spawns/kills/relaunches Claude Code CLI processes. Handles--resumefor session recovery. Persists session state across server restarts.session-store.ts— JSON file persistence to$TMPDIR/vibe-sessions/. Debounced writes.session-types.ts— All TypeScript types for CLI messages (NDJSON), browser messages, session state, permissions.routes.ts— REST API: session CRUD, filesystem browsing, environment management.env-manager.ts— CRUD for environment profiles stored in~/.companion/envs/.
-
web/src/— React 19 frontendstore.ts— Zustand store. All state keyed by session ID (messages, streaming text, permissions, tasks, connection status).ws.ts— Browser WebSocket client. Connects per-session, handles all incoming message types, auto-reconnects. Extracts task items fromTaskCreate/TaskUpdate/TodoWritetool calls.types.ts— Re-exports server types + client-only types (ChatMessage,TaskItem,SdkSessionInfo).api.ts— REST client for session management.App.tsx— Root layout with sidebar, chat view, task panel. Hash routing (#/playground).components/— UI:ChatView,MessageFeed,MessageBubble,ToolBlock,Composer,Sidebar,TopBar,HomePage,TaskPanel,PermissionBanner,EnvManager,Playground.
-
web/bin/cli.ts— CLI entry point (bunx the-companion). Sets__COMPANION_PACKAGE_ROOTand imports the server.
The CLI uses NDJSON (newline-delimited JSON). Key message types from CLI: system (init/status), assistant, result, stream_event, control_request, tool_progress, tool_use_summary, keep_alive. Messages to CLI: user, control_response, control_request (for interrupt/set_model/set_permission_mode).
Full protocol documentation is in WEBSOCKET_PROTOCOL_REVERSED.md.
Sessions persist to disk ($TMPDIR/vibe-sessions/) and survive server restarts. On restart, live CLI processes are detected by PID and given a grace period to reconnect their WebSocket. If they don't, they're killed and relaunched with --resume using the CLI's internal session ID.
The server automatically records all raw protocol messages (both Claude Code NDJSON and Codex JSON-RPC) to JSONL files. This is useful for debugging, understanding the protocol, and building replay-based tests.
- Location:
~/.companion/recordings/(override withCOMPANION_RECORDINGS_DIR) - Format: JSONL — one JSON object per line. First line is a header with session metadata, subsequent lines are raw message entries.
- File naming:
{sessionId}_{backendType}_{ISO-timestamp}_{randomSuffix}.jsonl - Disable: set
COMPANION_RECORD=0orCOMPANION_RECORD=false - Rotation: automatic cleanup when total lines exceed 1M (configurable via
COMPANION_RECORDINGS_MAX_LINES)
Each entry captures:
{"ts": 1771153996875, "dir": "in", "raw": "{\"type\":\"system\",...}", "ch": "cli"}dir:"in"(received by server) or"out"(sent by server)ch:"cli"(Claude Code / Codex process) or"browser"(frontend WebSocket)raw: the exact original string — never re-serialized, preserving the true protocol payload
REST API:
GET /api/recordings— list all recording files with metadataGET /api/sessions/:id/recording/status— check if a session is recording + file pathPOST /api/sessions/:id/recording/start/stop— enable/disable per session
Code: web/server/recorder.ts (recorder + manager), web/server/replay.ts (load & filter utilities).
Always use agent-browser CLI command to explore the browser. Never use playwright or other browser automation libraries.
When submitting a pull request:
- use commitzen to format the commit message and the PR title
- Add a screenshot of the changes in the PR description if its a visual change
- Explain simply what the PR does and why it's needed
- Tell me if the code was reviewed by a human or simply generated directly by an AI.
When creating or updating Linear issues:
- do not use commitzen-style titles in Linear
- use clear product-style titles that describe user value/outcome
Use this flow from the repository root:
# 1) Create a branch
git checkout -b fix/short-description (commitzen)
# 2) Commit using commitzen format
git add <files>
git commit -m "fix(scope): short summary" (commitzen)
# 3) Push and set upstream
git push -u origin fix/short-description
# 4) Create PR (title should follow commitzen style)
gh pr create --base main --head fix/short-description --title "fix(scope): short summary"For multi-line PR descriptions, prefer a body file to avoid shell quoting issues:
cat > /tmp/pr_body.md <<'EOF'
## Summary
- what changed
## Why
- why this is needed
## Testing
- what was run
## Review provenance
- Implemented by AI agent / Human
- Human review: yes/no
EOF
gh pr edit --body-file /tmp/pr_body.md- All features must be compatible with both Codex and Claude Code. If a feature is only compatible with one, it must be gated behind a clear UI affordance (e.g. "This feature requires Claude Code") and the incompatible option should be hidden or disabled.
- When implementing a new feature, always consider how it will work with both models and test with both if possible. If a feature is only implemented for one model, document that clearly in the code and in the UI.
- Hono backend (port 3457 in dev):
cd web && bun run dev:apior via./scripts/dev-start.sh - Vite frontend (port 5174 in dev):
cd web && bun run dev:viteor via./scripts/dev-start.sh - Both start together with
cd web && bun run dev(ormake dev), but that runs in foreground. Use./scripts/dev-start.shfor background mode.
./scripts/dev-start.shhealth-checks the backend on/which returns 404. If the script times out, the backend is still running — verify withcurl http://localhost:3457/api/sessions. You can start the servers manually as background processes instead.- The app requires Claude Code CLI or Codex CLI to create functional sessions. Without them, the UI loads but session creation will fail. The component playground at
#/playgroundworks without any CLI. - No external databases or services are needed. Session state persists to
$TMPDIR/vibe-sessions/as JSON files. - The pre-commit hook (
.husky/pre-commit) runscd web && bun run typecheck && bun run test -- --coverage. Run these before committing. - Two blocked postinstalls (
core-js,protobufjs) are harmless and do not affect functionality.