{"slug": "show-hn-roost-top-for-your-claude-code-sessions", "title": "Show HN: Roost – top for your Claude Code sessions", "summary": "Roost, a new open-source command-line tool for Claude Code sessions, displays live session data including model, context usage, and subagent activity, and is available via Homebrew, apt, pipx, and npm. The tool, created by developer gmhoward9289-ops, runs on macOS, Linux, and Windows with Python 3.9+, and provides a unique view of subagents spawned by each session.", "body_md": "`top`\n\nfor Claude Code. Every live session, the model it is on, how much context\nit has burned — and, unlike anything else, **the subagents it spawned**.\n\nOne file, no dependencies, Python 3.9+. Runs on macOS, Linux and Windows.\n\nThe short ambient loop below is the same program, idling:\n\n```\n  WORKER   MODEL    CTX  TOKENS  TREND  FLOW             IDLE   TASK\nNEAR LIMIT\n  demo-a1  opus-5   85%  170k    +4k       ..:-=+#+=-..  12s    refactor the parser\nPARKED + COSTLY\n  demo-b2  opus-5   61%  122k    +2k    ..............   4h10m  audit the build scripts\nWORKING NOW\n  demo-c3  fable-5  22%  44k     +22k       ...:=+*#+    3s     add integration tests\nSTARTING\n  demo-d4  -        -    -       -                       -\n\nQUIET (4)  demo-e5 . demo-f6 . demo-g7 . demo-h8\n\n8 worker(s)  |  1.2M held  |  61k last 8 turns  |  1 near limit  |  fable-5, opus-5\n\nSUBAGENTS\n  STATE    AGENT          MODEL     CTX       IDLE   TASK\n  working  Explore/a812a  opus-5    66k/200k  2s     survey the config loaders\n  idle     adaffaba4b     sonnet-5  484k/1M   1h22m  draft the migration notes\n\n  2 subagent(s), 1 working\n\nINFRA  ollama:11434 up qwen2.5-coder:14b (9.2 GB)   litellm:4000 up   openwebui:8080 DOWN\n```\n\nSessions are grouped by what it costs to ignore them, not by size: `NEAR LIMIT`\n\nis about to stop working, `PARKED + COSTLY`\n\nbills its whole context on the next\nturn, and everything quiet collapses to a single line.\n\n`TREND`\n\nis context added over the session's last few turns. It is an amount and\nnot a sparkline because context inside a session only ever rises — it drops\nsolely on `/compact`\n\n— so a shape would draw the same ramp on every row. `CTX`\n\nsays how full a session is; `TREND`\n\nsays how fast it is filling, which is how\n`demo-c3`\n\nabove shows as the one to watch at 22%. It is read back out of the\ntranscript rather than accumulated while roost runs, so it is there on the first\nframe and under `--once`\n\n. That is also what separates it from `FLOW`\n\nalongside\nit: `FLOW`\n\nsparks throughput sampled since roost started and begins empty,\n`TREND`\n\nis an amount already in the file when roost opens it.\n\nThe last line totals the fleet. Context held is what a sweep would reclaim.\n\nHomebrew (macOS and Linux):\n\n```\nbrew install gmhoward9289-ops/tap/roost\n```\n\nDebian and Ubuntu, from the repo — this also gets you `apt upgrade`\n\n:\n\n```\ncurl -fsSL https://gmhoward9289-ops.github.io/roost/roost-archive-keyring.asc | sudo gpg --dearmor -o /usr/share/keyrings/roost-archive-keyring.gpg\necho \"deb [signed-by=/usr/share/keyrings/roost-archive-keyring.gpg] https://gmhoward9289-ops.github.io/roost stable main\" | sudo tee /etc/apt/sources.list.d/roost.list\nsudo apt update\nsudo apt install roost\n```\n\nOr, without adding a repo — grab `roost_<version>_all.deb`\n\nfrom the\n[latest release](https://github.com/gmhoward9289-ops/roost/releases/latest)\nand install that file directly, e.g.:\n\n```\nsudo apt install ./roost_<version>_all.deb\n```\n\nBoth resolve `python3`\n\nexactly as a normal repo install would. The repo is\nsigned with a dedicated key (not tied to any personal identity); its public\nhalf is `packaging/apt/pubkey.asc`\n\nin this repo, published unchanged as\n`roost-archive-keyring.asc`\n\nabove.\n\nWith pipx:\n\n```\npipx install roost-top\n```\n\nPyPI holds the bare name `roost`\n\nin reserve — a prior project's name, retained\nafter deletion — so the *package* is `roost-top`\n\n; the command it installs is\nplain `roost`\n\n. Installing straight from the repo skips the index entirely:\n\n```\npipx install git+https://github.com/gmhoward9289-ops/roost\n```\n\nWith npm — the one channel that gives Windows a real `roost`\n\ncommand:\n\n```\nnpm install -g roost-top\n```\n\nOr run it without installing: `npx roost-top`\n\n. The npm package is a wrapper, not\na port: it ships the same `roost.py`\n\nand finds a Python to run it with (`py -3`\n\nfirst on Windows, `python3`\n\nelsewhere, 3.9 or newer either way). Python still has\nto be on `PATH`\n\n— npm delivers the script and puts `roost`\n\non `PATH`\n\n, it does not\nbring an interpreter. As on PyPI, the bare name `roost`\n\nwas already taken, so the\npackage is `roost-top`\n\nand the command is plain `roost`\n\n.\n\nOr just take the file. It is one script, stdlib only, no dependencies:\n\n```\ncurl -o roost https://raw.githubusercontent.com/gmhoward9289-ops/roost/main/roost.py\nchmod +x roost && ./roost\n```\n\nWith winget:\n\n```\nwinget install gmhoward9289-ops.roost\n```\n\nThat installs a frozen Windows executable — no Python required. It's built and\nattached to every tagged release alongside a `roost-<version>-windows-x64.zip`\n\non the [Releases page](https://github.com/gmhoward9289-ops/roost/releases), if\nyou'd rather grab the zip directly.\n\nOr, without any of that: save `roost.py`\n\nand run it — `.PY`\n\nis in `PATHEXT`\n\n,\nso `roost.py`\n\nworks from anywhere on `PATH`\n\n. That route and the npm one above\nboth still need a Python interpreter on `PATH`\n\n; the winget install is the one\nthat doesn't, since it ships a frozen exe.\n\nThe man page (`man roost`\n\n) ships with the Homebrew and `.deb`\n\ninstalls. A pipx\ninstall puts it under the venv's own `share/man`\n\n, which is not on the default\n`MANPATH`\n\n; read it in place with\n`man \"$(pipx environment --value PIPX_LOCAL_VENVS)/roost-top/share/man/man1/roost.1\"`\n\n.\n\n```\nroost              live, refreshing every second\nroost -w 5         slower refresh\nroost -1           one frame, then exit\nroost --json       joined records, for piping\n```\n\nWhile running: `space`\n\nrefresh now · `a`\n\nadvice panel · `s`\n\nsubagents panel · `m`\n\nlocal models panel · `u`\n\nusage panel · `g`\n\ngateway panel · `r`\n\nremote panel · `h`\n\nor `?`\n\nwhat am I looking at · `i`\n\narm interactive · `q`\n\nquit\n\nThe `FLOW`\n\ncolumn is a sparkline of each session's recent token throughput —\ncontext growth per refresh, normalised to its own busiest moment, newest at the\nright. A `.`\n\nis a sample with no flow; the ramp `:-=+*#`\n\nis increasing activity.\nIt is ASCII on purpose (block-drawing characters mojibake in the Windows\nconsole) and starts empty: history begins when roost starts, nothing persists.\n\nThe subagent `CTX`\n\ncolumn reads `48k/200k`\n\n— tokens loaded over the window.\nThe window is inferred (the smallest standard tier the observed usage fits in),\nbecause nothing in a transcript records which window the session was opened\nwith. The `AGENT`\n\ncolumn shows the agent's type (`Explore/a812a`\n\n) once the\nparent has recorded it — which only happens when the agent finishes, so a\nstill-running agent shows its hex id.\n\nExperimental, and off by default.`i`\n\narms interactive mode — the cursor,`x`\n\n,`y`\n\n, and the`EXPERIMENTAL`\n\nmarker in the top-right corner all come alive together, and it means it:`x`\n\nends a real process. Reading the dashboard has never been the risky half;`i`\n\nis the one key that draws the line. Press it again to disarm — the cursor drops and`x`\n\n/`y`\n\n/`j`\n\n/`k`\n\nstop responding until you press it once more.\n\nOnce armed, `j`\n\n/`k`\n\n(or the arrow keys) raise a cursor. Raising it expands the\n`QUIET`\n\ngroup, because a session idle for hours is exactly what a sweep is\nlooking for and it is unreachable while collapsed.\n\n| key | does |\n|---|---|\n`i` |\narm or disarm interactive mode |\n`j` `k` `↓` `↑` |\nmove the cursor (interactive mode only) |\n`x` |\nstop the selected session — confirms first, and only `y` proceeds |\n`y` |\ncopy its sessionId, for `claude --resume <id>` |\n`esc` |\ndrop the cursor, re-collapse `QUIET` |\n\nStart already armed with `roost --interactive`\n\nif you know you'll be acting on\na session right away.\n\n`x`\n\nends a process. It does not compact, save, or otherwise negotiate with the\nsession — **there is no local control channel into a running Claude Code\nsession**, so nothing gentler is available from outside it. On Unix that is a\n`SIGTERM`\n\nand the session exits on its own terms; on Windows there is no\ncross-process equivalent, so it is a `TerminateProcess`\n\nhard kill. Transcripts\nare written a turn at a time, so at most an in-flight turn is lost.\n\nBoth keys act on the row object that was on screen when you pressed them, never on an index re-resolved afterwards. Rows reorder between frames as sessions go quiet, and an index that outlived its frame would eventually stop the wrong one.\n\nroost refuses to stop its own process or its parent — run it from inside the session it is pointed at and the cursor can land on the row that owns your terminal.\n\nOnly one panel is open at a time: `a`\n\n, `s`\n\n, `m`\n\n, `u`\n\n, `g`\n\n, `r`\n\n, and `h`\n\nflip between\nADVICE, SUBAGENTS, LOCAL MODELS, USAGE, GATEWAY, REMOTE, and HELP rather than stacking. With two dozen sessions\non screen a stacked second panel lands below the bottom of the terminal, which\nis indistinguishable from the key not working. For the same reason the frame\nnow says `... N more line(s) below`\n\ninstead of quietly truncating.\n\n`h`\n\nor `?`\n\nopens a HELP panel — not a keybinding reference (the footer hint\nalready lists the keys), but a one-line-each rundown of what each screen on\nthe display means: INFRA, WORKERS, SUBAGENTS, ADVICE, LOCAL MODELS, USAGE,\nGATEWAY, REMOTE.\nroost is small enough that this is the whole manual.\n\nThe INFRA line only ever shows what Ollama currently has resident in VRAM —\n`ollama:11434 up qwen-coder-16k:latest (5.5 GB)`\n\n— because it reads\n`/api/ps`\n\n. A model that is installed but idle drops out of that line entirely,\nwhich reads as \"roost doesn't see it\" rather than \"it isn't loaded right now.\"\n\nPress `m`\n\nfor the full picture: every model `ollama list`\n\nknows about, each\nrow showing disk size, residency, VRAM when loaded, and how long until Ollama\nunloads it. It reads `/api/tags`\n\nmerged with `/api/ps`\n\n; if Ollama isn't\nrunning, the panel says so instead of showing nothing.\n\n`u`\n\nopens the USAGE panel: tokens per day per model over the last week, tallied\nfrom the transcripts on disk (input + output from each assistant turn; cache\nreads are excluded on purpose — they are billed and limited differently, and\ncounting them would swamp the number with re-reads of unchanged context).\n\n```\nUSAGE  observed transcript tokens (input+output) -- an estimate, not the Anthropic meter\n  2026-08-02  2.9M  opus-5 1.6M, sonnet-5 965k, fable-5 253k <- today\n  2026-08-01  9.1M  opus-5 4.5M, fable-5 3.3M, sonnet-5 984k\n  2026-07-29  1.9M  gemma4-32k (local) 5.3M, opus-5 1.9M\n\n  today 2.9M  |  7d 24.8M cloud  / 60.0M budget (41%)\n```\n\nTwo honesty rules bake into this panel. First, **it is an estimate, not the\nAnthropic meter** — there is no local file or API that records a plan's real\nrate-limit balance, so the budget is a number you set yourself:\n\n```\nexport ROOST_WEEKLY_BUDGET=60M    # or 850k, or a plain token count\n```\n\nRun `/usage`\n\ninside Claude Code once, pick a number that matches what it shows,\nand the panel tracks your burn against it from then on. Unset, the tallies show\nwithout the budget fraction. Second, **local models are free** — anything\nwithout a `claude-`\n\nmodel name (Ollama via LiteLLM writes transcripts too) is\nflagged `(local)`\n\nin the breakdown and excluded from the cloud totals and the\nbudget math.\n\nThe first `u`\n\nscans a week of transcripts and can pause for a moment; after\nthat only appended bytes are read, so keeping the panel open costs almost\nnothing. Day boundaries are UTC, because transcript timestamps are.\n\n`g`\n\nopens the GATEWAY panel: whether the LiteLLM proxy answers on\n`127.0.0.1:4000`\n\n, plus one row per batch-extraction run under the batch\ndirectory (`ROOST_BATCH_DIR`\n\n, default `~/litellm-server/batch`\n\n):\n\n```\nGATEWAY\n  litellm up (127.0.0.1:4000)   last request 42s ago   3 req/min\n  jobs queue: inbox 0  running 1  done 12  failed 0\n  BATCH RUN              MODEL           DONE/TOTAL  FAIL  RATE   ETA    LAST\n  results-laneB-derived  gemma4-32k      121/300     2     64/hr  2h48m  35s ago\n  results-laneA-derived  qwen-coder-16k  5/5         0     -      done   3d ago\n```\n\nThe proxy itself is asked nothing beyond \"are you up\" — a DB-less LiteLLM\nkeeps no request history (every activity endpoint 400s), so progress is\nderived from the batch pipeline's own output files: one JSON per finished\nitem means done/total, failure count, write rate, and ETA all fall out of a\ndirectory listing. Runs whose `extract.py`\n\nwrote a `_run.json`\n\nbreadcrumb show\ntheir model and worklist; older runs still appear, just with less detail.\nGreen rows are actively writing; the last-request/req-per-min figures are a\nbest-effort read of `proxy.log`\n\n's tail and disappear rather than guess when\nthe log doesn't parse. The `jobs queue`\n\nline counts the file-based job queue's\ndirs (`JOBS_ROOT`\n\n, default `~/jobs`\n\n) if present.\n\n`r`\n\nopens the REMOTE panel — other machines' roost, over ssh:\n\n```\nREMOTE\n  HOST    WORKERS  WORKING  RESIDENT MODELS  BATCH                      JOBS              AGE\n  hyrule  8        2        -                -                          in 0 run 1 fail 0  12s\n```\n\nHosts come from `ROOST_REMOTES`\n\n(comma-separated ssh aliases, default\n`hyrule`\n\n) — from the environment only, never from file contents. Each host is\nfetched with `ssh <host> roost --json`\n\non a background thread and cached: an\nunreachable host (a closed laptop lid) keeps its last good row with the AGE\ncolumn saying how old it is, instead of hanging the display. Only the very\nfirst fetch per host blocks, which is what makes `roost --remote -1`\n\nuseful.\n`ROOST_REMOTE_CMD`\n\noverrides the remote command if roost lives somewhere the\nnon-login ssh PATH can't see.\n\nEvery session stopped with `x`\n\nappends one JSON line to\n`~/.claude/logs/roost.jsonl`\n\n— same shape and the same 5000-line cap as the hook\nlogs beside it:\n\n```\n{\"ts\":\"2026-07-31T00:22:57-0400\",\"action\":\"stop\",\"ok\":true,\"host\":\"COOPER\",\n \"name\":\"models-ca\",\"pid\":4321,\"session_id\":\"abc-123\",\"model\":\"claude-opus-5\",\n \"ctx_tokens\":484030,\"idle_secs\":92500}\n```\n\nThe session's **task text is deliberately not recorded.** It is free-form prose\nout of a transcript, and an audit trail of what was stopped should not become a\ncopy of what was being worked on.\n\nBecause each record carries the context that session was holding, the log\nanswers afterwards what a sweep actually reclaimed rather than just how many\nrows you closed. `--no-log`\n\nrecords nothing. A log that cannot be written is\nignored rather than raised — losing the log is survivable, losing the display\nis not.\n\nSubagents have **no process of their own** — they run as sidechains inside the\nparent's process. Every pid-based view is structurally blind to them.\n\nThey do each get a transcript, one directory deeper than the session transcripts:\n\n```\n~/.claude/projects/<slug>/<sessionId>/subagents/agent-<id>.jsonl\n```\n\nThe short task description (\"Scout source URLs\") lives only in the *parent's*\n`toolUseResult`\n\n, keyed by `agentId`\n\n. roost joins the two, and falls back to the\nopening words of the subagent's own first message when the parent's record has\nscrolled out of reach.\n\nLocal, read-only sources. Nothing is sent anywhere; there is no network call except a localhost probe of the infra ports.\n\n| source | gives |\n|---|---|\n`~/.claude/sessions/<pid>.json` |\nClaude Code sessions: pid, sessionId, cwd, name |\n`~/.claude/projects/*/<sid>.jsonl` |\nClaude model in use, token usage |\n`~/.cursor/projects/*/agent-transcripts/*/*.jsonl` |\nCursor composers: model, usage, task text (no pid) |\n`127.0.0.1` ports |\nollama / litellm / openwebui |\n\n**Backends** default to both Claude Code and Cursor (`ROOST_BACKENDS=claude,cursor`\n\n).\nSet `ROOST_BACKENDS=claude`\n\nfor Claude-only. Cursor rows show in the `SRC`\n\ncolumn when both are present. Cursor composers are read-only in interactive\nmode — `x`\n\ncannot stop them (there is no pid), and `y`\n\ncopies the composer id.\n\nCursor transcripts are supplementary on disk; roost prefers the\n`composerHeaders`\n\ntable in Cursor's `state.vscdb`\n\nfor name and context %,\nand falls back to JSONL tails when the DB is missing. Composers older than\n`ROOST_CURSOR_MAX_IDLE_SECS`\n\n(default 24h) drop off the board. See\n`docs/cursor-on-disk.md`\n\nand epic\n[#48](https://github.com/gmhoward9289-ops/roost/issues/48).\n`python scripts/cursor_recon.py`\n\ninventories the live layout.\n\n**Context** is the last assistant turn's `input_tokens + cache_read_input_tokens\n\n- cache_creation_input_tokens`. Cross-checked against an independent tool on the same session: 77% vs 77.29%.\n\n**The context window is inferred, not recorded.** Nothing on disk states which\nwindow a session opened with, and a session on the 1M window will read 480k+\ncache tokens in a single call — scoring that against 200k yields a nonsense\n\"242%\". roost picks the smallest standard tier the usage fits and prints it in\nthe `WIN`\n\ncolumn, so the assumption is visible rather than silent. If a new tier\nships, `WINDOW_TIERS`\n\nis the one line to edit.\n\n- It reads an\n**undocumented on-disk format** that can change without warning. That is the whole foundation; treat breakage as expected, not exceptional. - The window inference above is a heuristic.\n- The\n`ADVICE`\n\npanel's thresholds are tuned to one person's usage. Read`EXPENSIVE_TOKENS`\n\nand friends before trusting the advice. - Daily-driven on macOS and Windows. CI runs the suite and a smoke frame on Linux, and it detects live sessions there — but nobody lives on it yet. Field reports welcome.\n\nroost is one person's tool with a public issue tracker. The\n[ help wanted issues](https://github.com/gmhoward9289-ops/roost/labels/help%20wanted)\nare real asks, not decoration: Linux field reports, a canary test for the\nundocumented on-disk format, adapters for other agent CLIs (gated on demand —\ncomment if you would use one), and config seams for the window tiers and the\nADVICE thresholds.\n\nQuestions or ideas? → [Discussions](https://github.com/gmhoward9289-ops/roost/discussions)\n\nOn COOPER the working copy lives at `dev/roost`\n\n(alongside `dev/heron`\n\n,\n`dev/swamp-ops`\n\n, and the rest of the estate). GitHub still hosts the remote;\nclone or copy there if you prefer `GitHub/roost`\n\n.\n\n`roost.py`\n\n— the entire program. One file, stdlib only.`bin/roost.js`\n\n— the npm wrapper; locates a Python interpreter and runs`roost.py`\n\nunder it.`packaging/`\n\n— the Homebrew formula (`roost.rb`\n\n), the`.deb`\n\nbuild (`build-deb.sh`\n\n), the version-consistency check that CI runs on every PR (`check-version-consistency.sh`\n\n), and the apt signing key (`apt/`\n\n).`tests/`\n\n— the unittest suite`ci.yml`\n\nruns on Linux, macOS and Windows.`scripts/`\n\n— maintenance utilities (`cursor_recon.py`\n\ninventories Cursor's on-disk layout for adapter work).`demo/`\n\n— the vhs tapes and GIFs in this README, plus the fleet stager that produces the staged data they record against.`.github/workflows/`\n\n—`ci.yml`\n\n(tests, packaging checks, semgrep),`release.yml`\n\n(publishes a tagged release to PyPI, npm and the Homebrew tap),`release-please.yml`\n\n(maintains the release PR), and`pr-title-lint.yml`\n\n(enforces conventional commit titles on PRs).\n\nSee [CHANGELOG.md](/gmhoward9289-ops/roost/blob/main/CHANGELOG.md).\n\nMIT — see [LICENSE](/gmhoward9289-ops/roost/blob/main/LICENSE). Contact: [dev@swamplink.com](mailto:dev@swamplink.com)\n\nBuilt in a Digital Swamp. From my swamp to yours.", "url": "https://wpnews.pro/news/show-hn-roost-top-for-your-claude-code-sessions", "canonical_source": "https://github.com/gmhoward9289-ops/roost", "published_at": "2026-08-18 22:10:08+00:00", "updated_at": "2026-08-18 22:41:10.694468+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["Roost", "Claude Code", "gmhoward9289-ops", "Homebrew", "pipx", "npm", "Python"], "alternates": {"html": "https://wpnews.pro/news/show-hn-roost-top-for-your-claude-code-sessions", "markdown": "https://wpnews.pro/news/show-hn-roost-top-for-your-claude-code-sessions.md", "text": "https://wpnews.pro/news/show-hn-roost-top-for-your-claude-code-sessions.txt", "jsonld": "https://wpnews.pro/news/show-hn-roost-top-for-your-claude-code-sessions.jsonld"}}