@chrodriguez-rts/claudetop
v0.3.4
Published
Real-time terminal dashboard for Claude workflows, agents, and sessions
Maintainers
Readme
claudetop
Real-time terminal dashboard for Claude workflows, agents, and sessions.
claudetop is an interactive terminal UI (TUI) — think htop/btop, but for your Claude API
activity. A local daemon aggregates events from every process that calls Claude, and a
keyboard-driven Ink dashboard renders live sessions, multi-step workflows, autonomous agent
tool-call chains, and background jobs from a single terminal pane.
╔═══════════════════════════════════════════════════════════════════╗
║ claudetop v0.1.0 [?] help [q] quit ║
╠══════════════════╦══════════════════════════╦═════════════════════╣
║ SESSIONS (3) ║ DETAIL ║ BACKGROUND JOBS ║
║ ● research-ag.. ║ Agent: research-agent ║ batch-embed-001 ║
║ ● chat-session ║ Status: ⟳ RUNNING ║ [████████░░] 80% ║
║ ○ idle-worker ║ Tokens: ↑ 2,341 ↓ 891 ║ custom-scrape-002 ║
║ ║ ✓ web_search (241ms) ║ [████░░░░░░] 41% ║
╠══════════════════╩══════════════════════════╩═════════════════════╣
║ ● 3 active │ Tokens: 124,302 in / 89,441 out │ Cost: $0.43 ║
╚═══════════════════════════════════════════════════════════════════╝Features
- Zero-friction observability — swap one import for the drop-in SDK wrapper and your Claude activity shows up automatically, no instrumentation code required.
- Cross-process visibility — a local IPC daemon aggregates activity from many processes at
once, so you can watch
claudetopin one terminal while several services call Claude in others. - Live sessions — token counts (in/out), cost estimates, latency, turn count, and streaming state, updated in real time.
- Workflows — multi-step pipelines rendered as a step ladder with per-step status.
- Agents — autonomous agent iterations with a full tool-call chain (input, status, timing).
- Background jobs — batch / embedding / fine-tune / custom jobs with animated progress bars.
- Real-time log stream — scroll-lockable event feed across every source.
- Beautiful, keyboard-driven UX — built with Ink v4 (React for the terminal) and Zustand,
inspired by
btopandlazygit.
Install
npm install -g @chrodriguez-rts/claudetopThis installs the claudetop (and ctop) commands globally. Or run it without installing:
npx @chrodriguez-rts/claudetopRequires Node.js 22.13 or newer (history persistence uses the built-in
node:sqlite module).
The CLI installs two equivalent binaries: claudetop and the shorthand alias ctop.
Quickstart
Start the daemon (the central state aggregator). It runs in the foreground and keeps the process alive:
claudetop daemon startInstrument your app so its Claude activity is reported to the daemon. The fastest path is the drop-in SDK wrapper — change one import:
import Anthropic from "@chrodriguez-rts/claudetop/sdk"; const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const message = await client.messages.create({ model: "claude-opus-4-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], });Open the dashboard in another terminal:
claudetopYour app's session appears in the SESSIONS pane with live token counts and cost.
Just want to see what it looks like first? Run
CLAUDETOP_DEMO=1 claudetopto launch the TUI pre-seeded with mock data — no daemon or API key needed. See Demo mode.
Integration paths
There are two ways to feed activity into claudetop. Use the SDK wrapper for automatic session
tracking, or the manual client when you need explicit control over workflows, agents, and jobs.
(a) Drop-in SDK wrapper — @chrodriguez-rts/claudetop/sdk
@chrodriguez-rts/claudetop/sdk is a transparent proxy over @anthropic-ai/sdk. Its default export is a drop-in
replacement for the real Anthropic client — the API surface and call signatures are identical, so
the only change to your code is the import line.
// Before
import Anthropic from "@anthropic-ai/sdk";
// After — identical API, automatic session tracking in claudetop
import Anthropic from "@chrodriguez-rts/claudetop/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });Under the hood it registers a session with the daemon on the first call, counts tokens in real
time, estimates cost, and updates session status to error if a call throws. Emits are
fire-and-forget, so the happy path adds no latency.
You can name the session with the extra claudetopName option (everything else is forwarded
to the real Anthropic constructor untouched):
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
claudetopName: "research-agent",
});@anthropic-ai/sdk is an optional peer dependency
The real SDK is not bundled with @chrodriguez-rts/claudetop — it is an optional peer
dependency, loaded lazily so that installing the package for the TUI alone never breaks. If you use
@chrodriguez-rts/claudetop/sdk, install it yourself:
npm install @anthropic-ai/sdkIf it's missing, constructing the wrapped client throws:
@chrodriguez-rts/claudetop/sdk requires @anthropic-ai/sdk to be installed. Run `npm install @anthropic-ai/sdk`.Phase-2.x streaming caveat
Both non-streaming messages.create(...) and async-iteration streaming are fully instrumented:
// ✅ Instrumented — async iteration over the stream
const stream = client.messages.stream({
model: "claude-opus-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku." }],
});
for await (const event of stream) {
// token counts update live in claudetop as events arrive
}// ✅ Instrumented — create() with stream: true (async iterable)
const stream = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Write a haiku." }],
});
for await (const event of stream) {
// ...
}The interceptor taps the stream's async iterator. Callback-style streaming via
stream.on("text", ...) and other event-emitter handlers is NOT yet instrumented in Phase 2.x —
those calls work exactly as the real SDK does, but their token deltas will not be reflected in
claudetop. Use async iteration if you want live streaming metrics in the dashboard.
(b) Manual client — @chrodriguez-rts/claudetop/client
For apps that keep their own Anthropic SDK instance, or that need to model workflows, agents, and background jobs explicitly, use the manual registration API. Each helper returns the object it created or updated; you pass that object back in to advance it, so there is no hidden client state to manage.
import { claudetop } from "@chrodriguez-rts/claudetop/client";Sessions
// register(input: { name: string; model: string; tags?: string[] }): Promise<ClaudeSession>
const session = await claudetop.session.register({
name: "research-agent",
model: "claude-opus-4-5",
tags: ["batch", "internal"],
});
// update(session: ClaudeSession): Promise<void>
await claudetop.session.update({ ...session, tokensIn: 2341, tokensOut: 891 });
// complete(session: ClaudeSession, status?: Status): Promise<void> — status defaults to "completed"
await claudetop.session.complete(session);Workflows
// create(input: { sessionId: string; name: string; steps: string[] }): Promise<ClaudeWorkflow>
const workflow = await claudetop.workflow.create({
sessionId: session.id,
name: "Research Pipeline",
steps: ["fetch", "summarize", "synthesize", "write"],
});
// step.advance(workflow: ClaudeWorkflow): Promise<ClaudeWorkflow>
// advance() returns the updated workflow — feed it back in to advance again.
let wf = await claudetop.workflow.step.advance(workflow); // fetch -> summarize
wf = await claudetop.workflow.step.advance(wf); // summarize -> synthesize
// once the final step is passed, the workflow is marked "completed" automatically.Agents
// register(input: {
// sessionId: string; name: string; model: string;
// availableTools?: string[]; maxIterations?: number;
// }): Promise<ClaudeAgent>
const agent = await claudetop.agent.register({
sessionId: session.id,
name: "researcher",
model: "claude-opus-4-5",
availableTools: ["web_search", "read_file", "write_file"],
maxIterations: 10,
});
// toolCall(agent: ClaudeAgent, input: { tool: string; input?: Record<string, unknown> }):
// Promise<{ agent: ClaudeAgent; callId: string }>
const { agent: a1, callId } = await claudetop.agent.toolCall(agent, {
tool: "web_search",
input: { query: "claudetop release notes" },
});
// toolResult(agent: ClaudeAgent, callId: string,
// result: { status: "success" | "error"; output?: Record<string, unknown>; durationMs?: number }):
// Promise<ClaudeAgent>
const a2 = await claudetop.agent.toolResult(a1, callId, {
status: "success",
output: { hits: 5 },
durationMs: 241,
});Note: there is no dedicated agent-registered event.
agent.registerandagent.toolCallboth emit on the same channel, and the daemon upserts the full agent payload either way.
Background jobs
// register(input: {
// type: "batch" | "embedding" | "fine-tune" | "custom";
// label: string; metadata?: Record<string, unknown>;
// }): Promise<BackgroundJob>
const job = await claudetop.job.register({
type: "batch",
label: "embed-corpus-001",
metadata: { source: "docs/" },
});
// progress(job: BackgroundJob, progressPct: number): Promise<BackgroundJob>
// progressPct is a fraction in the range 0.0 – 1.0. Returns the updated job.
let j = await claudetop.job.progress(job, 0.45);
j = await claudetop.job.progress(j, 0.9);
// complete(job: BackgroundJob): Promise<void> — marks status "completed", progress 1.0
await claudetop.job.complete(j);The client connects to the daemon lazily on the first call and shares a single socket connection across every helper.
Daemon CLI
| Command | Description |
|---|---|
| claudetop | Launch the TUI dashboard. |
| claudetop daemon start | Start the daemon as a detached background process that outlives the launching terminal, then return. Its stdout/stderr are redirected to ~/.claudetop/daemon.log. Add -f / --foreground to run it in the foreground (blocks, logs to the terminal; for use under a process supervisor). No-op if one is already running. |
| claudetop daemon status | Report whether a daemon is reachable. Prints daemon: running / daemon: not running and exits 0 / 1. |
| claudetop daemon stop | Stop the running daemon by signalling the pid recorded in ~/.claudetop/daemon.pid (SIGTERM). Prints daemon: stopped (pid N) / daemon: not running; a stale pidfile is cleaned up. |
| claudetop capture enable / disable / status | Toggle/report auto-capture of Claude Code CLI sessions (transcript tailer). |
| claudetop hooks install / uninstall / status | Add/remove user-scope hooks in ~/.claude/settings.json for low-latency capture. |
| claudetop --version / claudetop -v | Print the version. |
The ctop alias works identically (e.g. ctop daemon start).
Capture Claude Code CLI activity
By default claudetop only sees apps you instrument with the /sdk wrapper or
the /client API. It can also auto-capture every Claude Code CLI session
user-wide — no per-app changes and no Anthropic API key — by reading the transcripts and
hook events Claude Code already produces locally. Nothing is sent to Anthropic; token counts come
straight from the local transcripts and cost is derived with the same pricing.ts table.
claudetop capture enable # turn on the transcript tailer
claudetop hooks install # add low-latency lifecycle hooks (merges into settings.json)
claudetop daemon start # (re)start the daemon to apply
claudetop # open the TUI — your Claude Code sessions appear automaticallyTwo cooperating sources feed the daemon:
- Transcript tailer (authoritative) — backfills recent sessions (last
capture.backfillDaysdays) and tails~/.claude/projects/**/*.jsonllive for model, token/cache usage, derived cost, cwd, and git branch. Subagent transcripts roll into their parent session (taggedsubagents). - User-scope hooks (low-latency) —
claudetop hooks installmerges command hooks (SessionStart/PreToolUse/PostToolUse/Stop/…) into~/.claude/settings.jsonfor instant lifecycle/status updates. It appends to your existing hooks (backing the file up first) andhooks uninstallremoves only claudetop's entries.
To undo: claudetop hooks uninstall and claudetop capture disable. Honor a non-default Claude
data dir via CLAUDE_CONFIG_DIR / CLAUDE_CONFIG_DIRS or capture.claudeConfigDir.
Troubleshooting
Nothing shows up / the dashboard is empty. Read the status bar first:
● daemon disconnected— no daemon is running. Start one withclaudetop daemon start, then pressrin the TUI to reconnect.● daemon connectedbut no sessions — the daemon is up and idle. The detail pane shows first-run guidance: instrument an app with the/sdkwrapper or/clientAPI, or turn on Claude Code capture.
A cancelled session reappears as active. Fixed — terminal states (cancelled / completed) are now sticky and survive the capture tailer's mtime sweep. If you still see it, restart the daemon to clear stale state.
EADDRINUSE when starting. A daemon already owns the socket. claudetop daemon start pre-checks and prints daemon: already running instead of a stack trace. If a crash left things wedged, claudetop daemon stop (SIGTERM via the pidfile, clears a stale pidfile) then start again.
The daemon dies when I close the terminal. It shouldn't — claudetop daemon start runs detached and outlives the launching terminal. Only -f / --foreground ties it to the terminal. Its output lands in ~/.claudetop/daemon.log; check there if it exits unexpectedly.
Capture isn't working. It needs all three: claudetop capture enable, claudetop hooks install, and a daemon restart to apply. Confirm with claudetop capture status (transcripts on/off + hooks installed count). For a non-default Claude data dir, set CLAUDE_CONFIG_DIR.
Raw mode is not supported / TTY error. The TUI needs a real interactive terminal; it can't run piped or under CI. The same applies to CLAUDETOP_DEMO=1 claudetop.
Where do things live? Config ~/.claudetop/config.json, history ~/.claudetop/history.db, daemon log ~/.claudetop/daemon.log, pidfile ~/.claudetop/daemon.pid, socket ~/.claudetop/daemon.sock (a named pipe on Windows).
Keyboard shortcuts
Press ? inside the TUI to toggle this overlay at any time.
| Key | Action |
|---|---|
| tab / shift+tab | Cycle panel focus |
| ↑ / ↓ | Navigate list items |
| k | Kill / cancel selected |
| r | Reconnect to daemon |
| t | Cycle color theme |
| d | Toggle detail panel mode |
| l | Toggle log scroll-lock |
| ? | Toggle this help |
| q | Quit claudetop |
Configuration
claudetop reads ~/.claudetop/config.json on startup. Every key is optional; the defaults are
shown below.
{
"theme": "nord",
"daemon": {
"autoStart": true,
"socketPath": "~/.claudetop/daemon.sock",
"historyRetentionHours": 24,
"port": null
},
"display": {
"showCost": true,
"defaultPanel": "sessions",
"logLevel": "info",
"maxLogLines": 500,
"scrollLock": false
},
"pricing": {
"overrides": {}
},
"capture": {
"transcripts": false,
"hooks": false,
"backfillDays": 7,
"maxBackfillFiles": 200,
"claudeConfigDir": null
}
}| Key | Default | Description |
|---|---|---|
| theme | "nord" | Color theme. One of nord, dracula, one-dark, tokyo-night. |
| daemon.autoStart | true | Reserved — not yet wired; start the daemon manually with claudetop daemon start. |
| daemon.socketPath | "~/.claudetop/daemon.sock" | Unix socket path for the daemon (see Platform support for Windows). Honored, after CLAUDETOP_SOCKET. |
| daemon.historyRetentionHours | 24 | How many hours of session/metric history to persist via Node's built-in node:sqlite (needs Node ≥ 22.13; otherwise history is in-memory only). |
| daemon.port | null | Reserved for future TCP transport; unused by the local Unix-socket daemon. |
| display.showCost | true | Show the estimated cost segment in the status bar. |
| display.defaultPanel | "sessions" | Panel focused on launch. |
| display.logLevel | "info" | Reserved — not yet wired. Intended minimum log level for the stream. |
| display.maxLogLines | 500 | Reserved — not yet wired; the buffer is currently fixed at 500 lines. |
| display.scrollLock | false | Start with the log stream scroll-locked. |
| pricing.overrides | {} | Per-model price overrides so teams on custom-contracted pricing get accurate cost estimates without editing source. |
| capture.transcripts | false | Auto-capture Claude Code CLI sessions by tailing ~/.claude/projects. Toggle with claudetop capture enable/disable; restart the daemon to apply. |
| capture.hooks | false | Reserved flag for the hooks source; hooks are enabled by claudetop hooks install (which edits ~/.claude/settings.json) and forward over the socket regardless. |
| capture.backfillDays | 7 | On daemon start, backfill transcripts modified within this many days, then tail live. 0 = live only (clamped to 0–90). |
| capture.maxBackfillFiles | 200 | Cap on transcripts scanned during backfill (most-recent first; clamped to 1–5000). |
| capture.claudeConfigDir | null | Override the Claude data dir. Precedence: CLAUDE_CONFIG_DIR / first of CLAUDE_CONFIG_DIRS env > this > ~/.claude. |
The socket path can also be overridden at runtime with the CLAUDETOP_SOCKET environment variable
(useful for tests or custom layouts).
Themes
claudetop ships four themes:
- Nord (default)
- Dracula
- One Dark
- Tokyo Night
Set the active theme via the theme key in ~/.claudetop/config.json.
Demo mode
To explore the dashboard without a daemon or an API key, set CLAUDETOP_DEMO to any non-empty
value. The store is pre-seeded with mock sessions, workflows, agents, jobs, and log events so you
can eyeball the full layout and navigation:
CLAUDETOP_DEMO=1 claudetopPlatform support
| Platform | Transport | Status |
|---|---|---|
| macOS | Unix domain socket (~/.claudetop/daemon.sock) | Fully supported |
| Linux | Unix domain socket (~/.claudetop/daemon.sock) | Fully supported |
| Windows | Named pipe (\\.\pipe\claudetop-daemon) | Supported via named-pipe fallback |
Node does not support Unix domain sockets on Windows, so claudetop transparently uses a named
pipe there instead. The socket/pipe address is resolved automatically per platform and can be
overridden with the CLAUDETOP_SOCKET environment variable.
Architecture
claudetop is two independent layers that communicate over a local socket using a line-delimited
JSON protocol. Each message is a typed envelope: { type, payload, timestamp }.
Your application(s)
import Anthropic from "@chrodriguez-rts/claudetop/sdk" (wrapper)
── or ──
import { claudetop } from "@chrodriguez-rts/claudetop/client" (manual)
│
│ IPC (Unix socket / named pipe)
▼
┌───────────────────────┐
│ claudetop daemon │ aggregates state (sessions / workflows /
│ │ agents / jobs + metrics), broadcasts deltas,
│ │ persists history (optional SQLite)
└───────────┬───────────┘
│ IPC (Unix socket / named pipe)
▼
┌───────────────────────┐
│ claudetop TUI │ your terminal dashboard (one or many)
└───────────────────────┘- Daemon — a singleton background process and central state aggregator. It owns authoritative in-memory state for all sessions, workflows, agents, and jobs, broadcasts delta updates to every connected client, and persists a rolling history to SQLite.
- TUI client — an Ink/React terminal application that connects to the daemon on startup, subscribes to state updates via Zustand store slices, and renders the live dashboard. Multiple TUI instances can connect to the same daemon at once.
Package entry points
| Import path | Use case |
|---|---|
| @chrodriguez-rts/claudetop | Reserved for the future programmatic API. |
| @chrodriguez-rts/claudetop/sdk | Drop-in Anthropic SDK wrapper. |
| @chrodriguez-rts/claudetop/client | Manual session / workflow / agent / job registration. |
| @chrodriguez-rts/claudetop/daemon | Daemon lifecycle management (startDaemon, pingDaemon). |
License
MIT
