A live dashboard for every Claude Code session running on this machine — an
action queue you can't miss, status you can trust, cost, and PR state at a
glance. It reads the real session state Claude Code already writes to
~/.claude; nothing is mocked.
Built from the "Organic" Claude Design system (Caprasimo + Figtree, a warm
cream ground, a terracotta accent for needs-you and a sage accent for
running) and the Sessions Dashboard Wireframes — specifically the wireframe's
recommended option 2b: a triage queue on top of an all-sessions roster.
No dependencies, no build step. Requires Node 18+.
node server.js
# → http://localhost:4317Then open http://localhost:4317. PORT=... node server.js to change the port.
docker compose up
# → open http://localhost:4317Your host ~/.claude is mounted read-only so the container has real session
state to show. Override the port or the mounted directory without editing the
compose file:
PORT=4318 docker compose up
CLAUDE_DIR=/path/to/.claude docker compose upThe read-only dashboard, usage and history all work in the container. The write actions (Kill, Reveal folder, Resume in terminal, Open in desktop) act on host processes and macOS apps and are not available from inside the container. Live session detection also needs host processes, so in Docker sessions show as "open" but never "live". To keep those features and still start on reboot, run it natively as a service instead (below).
To run the dashboard natively and start it after a reboot, install the launchd
LaunchAgent in deploy/. This keeps live detection and the
write actions working, which Docker cannot.
| View | What it is |
|---|---|
| Sessions | The action queue (every session waiting on you, with why) over a roster of all live sessions, grouped by repo, with status, model, freshness, elapsed time, cost and PR. |
| Usage | Spend over the last 7/30 days — daily chart, by repository, by model — computed from token usage across every transcript. |
| History | Recently-ended sessions with outcome and cost. |
| Settings | Refresh controls, plus an honest explainer of how every status is derived. |
Click any session to open a drawer with its recent transcript, its live PR
state (via gh), and one-click ways back into it. Open in Claude Desktop
uses the desktop's own claude://code/continue?session=<id> scheme, and Copy
resume gives you the claude --resume command for a terminal.
Note on Open in Claude Desktop. The desktop keys its sessions by a
local_<uuid>id (stored under~/Library/Application Support/Claude/claude-code-sessions/), not the CLI transcript id — the dashboard resolves that mapping so the link targets the right session. However, on packaged (production) desktop builds the jump-to-session route is behind a remote feature gate (claudeURLHandler: code entry deep link gated off). While it's gated, the button reliably foregrounds Claude Desktop but may not navigate to the exact chat; once the gate is enabled it will, with no change here. Sessions started directly from the CLI (never registered with the desktop) have nolocal_id and can't be targeted — use Copy resume for those.
The roster shows two kinds of session, matching what Claude Desktop lists:
- Live — a Claude Code process is actually running right now
(
~/.claude/sessions/<pid>.jsonwith an alive PID). These get real running / waiting / idle status and can be killed. - Open — a recently-active session with no running process. Resumable, and what the desktop keeps in its sidebar. Because the desktop's archive flag isn't written to disk, "open" is approximated as active within the last 7 days, capped per repo, so the list matches the sidebar's breadth. Open sessions never enter the action queue (there's no process to answer).
Everything is local and read-only except two explicit, guarded actions.
- Live sessions come from
~/.claude/sessions/<pid>.json— Claude Code writes one file per running process (pid,sessionId,cwd,startedAt,name). A session is live when itspidis still a running process. - Everything else (repo, branch, model, cost, PR, status) is derived from
that session's transcript at
~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. - Repos follow the
~/.claude/worktreesconvention: a worktree's repo is the path segment before/.claude/worktrees/.
Claude Code does not persist an explicit "waiting for permission" flag to disk, so status is inferred from the tail of the transcript plus its modified time:
- Waiting on you — the last turn is an unanswered tool call that has sat idle (almost always a permission prompt), or Claude ended its turn and is awaiting your reply / answering a question.
- Running — the transcript was written to within the last 15 seconds.
- Idle — a live process with no transcript activity for over 10 minutes.
Because disk state can't tell a permission prompt from a slow tool with
certainty, approving/replying still happens in the session. Go to session
copies the exact cd <cwd> && claude --resume <id> command to get you there.
Computed from each transcript's token usage at Anthropic list prices (Opus $5/$25, Sonnet 5 $2/$10, Fable $10/$50 per 1M in/out), with cache writes at 1.25× and cache reads at 0.1× the input rate. It's an estimate of API-equivalent spend, not a bill. Priced per assistant turn at that turn's own model, so a session that switched models is costed correctly.
- Open in Claude Desktop —
open claude://code/continue?session=<id>&source=desktop_action. The session id is validated ([0-9a-f-]{8,64}) before it's put in the URL. Works for live and open sessions. - Resume in terminal (macOS) — resumes the session in your default terminal
app. The server writes a small executable
.commandfile (cd <cwd> && exec claude --resume <id>, withclauderesolved to an absolute path) andopens it, so macOS launches whatever app owns shell scripts — Terminal, iTerm2, Warp, … — rather than hard-coding Terminal.app. The drawer's Run button (next to the copied command) does the same thing. - Reveal folder — opens the session's working directory in Finder.
- Kill (live only) —
SIGTERMto a session's process. Guarded (only a pid the dashboard currently lists as live) and confirmed in a dialog.
Approving permissions or sending replies in place is intentionally not
wired: it would require driving Claude Code's undocumented per-session IPC
socket (messagingSocketPath), and getting it wrong could inject bad input into
real, running work. Instead, one click takes you into the actual session — in
the desktop app or a terminal — where you answer it yourself.
server.js zero-dependency HTTP server + JSON API + static host
lib/
sessions.js live-session discovery, transcript parsing, status heuristic
analytics.js usage aggregation + history
pricing.js model pricing + cost from token usage
git.js live PR state via `gh` (on-demand, cached)
public/
index.html app shell
app.js the SPA (Sessions / Usage / History / Settings + drawer)
dashboard.css app layout — reads only Organic tokens
organic.css the Organic design system, vendored verbatim
icons.js inlined Lucide icons (the system's icon family)
| Endpoint | Returns |
|---|---|
GET /api/state?open=1&days=7 |
live + open sessions and summary counts |
GET /api/usage?days=7|30 |
spend over time, by repo, by model |
GET /api/history |
recently-ended sessions |
GET /api/session/:id |
transcript tail |
GET /api/pr?repo=&number= |
live PR state via gh |
POST /api/action |
{action:'open-desktop', id} · {action:'open-terminal', id, cwd} (macOS) · {action:'reveal', cwd} · {action:'kill', pid} |
Transcripts are parsed once and cached on (mtime, size), so unchanged files
are never re-read. Usage aggregation only touches files modified within the
selected window and is memoised for 60s. The UI polls /api/state every 5s
(configurable in Settings) and pauses polling when the tab is hidden.