Live-stream your Claude Code session to a web page. Viewers watch your prompts and tool activity scroll by in real time — an over-the-shoulder feed, not a raw terminal.
Claude Code ──hooks──▶ emit.mjs ──HTTP POST──▶ relay server ──SSE──▶ browser viewers
(your machine) (Node, zero deps) (read-only)
- Server-Sent Events, not WebSocket — viewers are read-only, so we only push server→client. No dependencies, auto-reconnects.
- Redacted by default — the hook scrubs API keys, tokens, JWTs, and private keys before anything leaves your machine, and never sends command output or file contents (just prompts + which tool ran + its target).
# 1. start the relay (viewer page + ingest endpoint)
npm start # -> http://localhost:8787
# 2. wire the hooks into ~/.claude/settings.json (backs up first, reversible)
npm run install-hooks
# 3. restart Claude Code, open http://localhost:8787 — go.Undo the hooks anytime:
npm run uninstall-hooksBy default the feed is read-only. To let the web page send prompts into Claude, launch Claude through the wrapper instead of your normal terminal:
node server/run.mjs # runs `claude` (claude.cmd on Windows) inside a PTY
# node server/run.mjs -- --model opus # forward flags to claudeClaude looks and behaves exactly as normal locally. Now anything typed in the composer box at the bottom of the viewer is written into Claude's input — and shows up in the feed via the prompt hook. The tab you have selected decides which session receives it ("All" broadcasts to every wrapped session).
browser composer ──POST /input──▶ relay ──SSE /control──▶ wrapper ──PTY──▶ Claude
Anyone who can open the page can type into your session. For a public/tunnel URL, gate it with a shared token:
CLAUDE_LIVE_TOKEN=some-secret node server/run.mjs # wrapper enforces it
# viewers must open http://…/?token=some-secret to send inputWithout a token, input is open to anyone with the link (fine for local/private).
The relay is a plain HTTP server, so any tunnel or host works:
# quickest: expose your local relay
cloudflared tunnel --url http://localhost:8787
# or: ngrok http 8787Point your machine's hook at a remote relay instead of localhost:
export CLAUDE_LIVE_URL="https://your-relay.example.com/ingest"Then deploy server/ to Fly.io / Railway / a VPS (it's a single zero-dep file).
| Event | Shown |
|---|---|
SessionStart |
session started |
UserPromptSubmit |
your prompt text (redacted, truncated) |
PreToolUse |
tool name + target (file path, command, pattern, url) |
Stop |
Claude's reply — read from the session transcript (redacted, truncated) |
Tune fidelity or redaction in plugin/scripts/emit.mjs.
plugin/ is a self-contained plugin (.claude-plugin/plugin.json +
hooks/hooks.json). Add it via a plugin marketplace, or just use
npm run install-hooks above, which writes the same hooks into your settings.
- File paths are shown as-is — they can reveal repo/client names. Extend
redact()inemit.mjsif that matters for your streams. - The public relay URL is read-only; viewers can't type into your session.
MIT