A multi-call Go binary (claude-status) that powers a per-workspace "Claude
activity" indicator in waybar under the
niri Wayland compositor.
The repository is agentic-db; the binary it builds is still
claude-status(as are the Go module and the on-disk DB), so every command below is unchanged.
Claude Code hooks invoke claude-status hook, which writes per-session state to a
SQLite database. A long-lived claude-status daemon reads that database plus the
niri window model and drives two renderers:
- Workspace-name glyphs. The daemon sets niri workspace names, which
waybar renders directly as glyphs:
- Working — blinking orange
● - Prompt/waiting — blinking yellow
?(Claude needs you) - Idle — a two-cell shade bar that fades over 60 minutes as the session goes stale
- Working — blinking orange
- The pwetty
claudetile. On each tick the daemon also precomputes a per-desktop payload and writes it to a tile cache.claude-status tile-data(andtile-watch) then serve that cache as the JSON a pwetty-hosted waybar tile expects — a cheap file read on the hot path, noniri msgspawn per render. Contract:~/Perso/pwetty-box-rs/tiles/claude/schema.json.
| Command | Description |
|---|---|
hook |
Hot-path hook handler. Reads a Claude Code hook JSON event from stdin, derives session state, and upserts one row in the SQLite DB. Always exits 0. |
daemon |
Long-lived reconciler. Watches the DB + niri event stream and sets/unsets workspace names. |
install |
Idempotently merge our hooks into ~/.claude/settings.json, add the claude-resume block to .zshrc, and print niri/waybar setup fragments. --no-shell skips the .zshrc edit; --zshrc <path> overrides its location. |
uninstall |
Remove only our hook entries and the .zshrc block. --purge also drops the state dir + DB; --no-shell leaves the .zshrc block. |
gc |
Run the dead-session reap pass once and report reaped rows. |
gen-waybar |
Emit paste-ready waybar format-icons JSON and style.css fragments. |
doctor |
Dump the DB schema/rows and the live niri windows list (debugging). |
events |
Print the audit log (one row per hook), newest first. --session <id>, --limit N. |
recap |
Summarize a time window of Claude sessions (topics, effort, streaks, metrics). --period day|week|quarter, --since, --json, --metrics full|none|only. |
recap-prompt |
Emit the LLM instructions that turn a recap into a written report. --period. |
resume |
Backend for the claude-resume picker: --list (fzf rows), --show <id> (preview), --init zsh (shell integration). Bare prints setup help. |
repo-backfill |
One-off, idempotent pass that backfills stored git repos for historical sessions (so old rows resolve in recap project metrics). |
tile-data |
Emit the pwetty claude-tile JSON for one niri desktop, keyed by its per-output workspace index. Reads the daemon's precomputed tile cache (falls back to a live build when no daemon is up); always prints valid JSON. --output overrides the niri connector. |
tile-watch |
Like tile-data but streams the payload for one desktop on every change (for a pwetty stream: true module) instead of printing once. |
When a crash kills your sessions, claude-resume fuzzy-picks a recent one (across
all projects, newest first) and resurrects it. claude --resume is scoped to
the directory the session started in, and a subprocess can't change your shell's
cwd — so the picker ships as a zsh function that cds into the session's dir
(in your shell, where the cd sticks) and then runs claude --resume. fzf does
the picking; the binary only serves rows + a preview.
claude-status install adds this to your .zshrc (a marker-delimited, idempotent
block — reversible via uninstall, .bak kept; if the file is chezmoi-managed it
prints a chezmoi re-add reminder). Then, in a new shell:
claude-resume # fuzzy-pick + resurrect (cr is a guarded alias, if free)
claude-resume --here # only sessions from the current repo
claude-resume --all # include still-running sessions tooTo wire it up by hand instead of via install, add to ~/.zshrc:
eval "$(claude-status resume --init zsh)"The preview pane shows each session's topic, repo/branch, cwd, and the tail of its conversation, so you can tell dead sessions apart before reviving one.
recap joins the event log (effort: active time, turns, prompts, streaks) with
the session transcripts under ~/.claude/projects (intent: Claude's ai-title,
your opening ask, project + branch) into a standup-ready markdown digest for a
time window. Active time uses a heartbeat model, so a frozen/missed-Stop turn
can't inflate it. The digest is deterministic; the narrative is left to an LLM,
which stays out of the binary so the pipeline is composable:
claude-status recap --period day | claude -p "$(claude-status recap-prompt --period day)"recap alone prints the digest (add --json for tooling); recap-prompt prints
period-tuned instructions (day = terse standup, week = themes, quarter = narrative).
The digest ends with deterministic ## Metrics (total wall-clock time,
per-session active min/avg/max, prompts sent, permission prompts) and
## Project metrics (per project: the origin remote as gh/owner/repo or
host/path, and commits landed in the window on the current branch; the branch
is noted when it isn't main/master). Every project that resolves to a git repo
is listed. Git state is read best-effort via internal/git, trying each of a
project's session cwds until one resolves — so work done from a since-removed
temp worktree still resolves via its surviving checkout, and a project that
resolves to no repo at all (a non-repo cwd like $HOME) is simply dropped. A
repo with no origin shows (no remote).
--metrics controls the whole appended block:
full (default, append it), none (omit — used to feed the LLM so it can't
restate figures), or only (just the section — the recap job appends this
verbatim after the LLM narrative).
mise run install also installs a single claude-recap-job <daily|weekly>
wrapper (in share/) plus two systemd user timers:
claude-daily-recap.timer— daily at 09:00 →~/dailies/daily-YYYYMMDD.md(from the last covered day through yesterday, so a multi-day gap is caught in one run; falls back to the previous day, or Fri–Sun on a Monday).claude-weekly-recap.timer— Mondays at 09:00 →~/weeklies/weekly-YYYYMMDD.md(the previous complete calendar week, Mon 00:00 → Sun 23:59).
Both are Persistent=true, so a run missed while the laptop was suspended/off
fires at the next power-on (unlike cron). The job waits out the wakeup resource
storm before its LLM call (the weekly waits longer so it doesn't compete with the
daily on Monday mornings), retries if the network isn't up yet, and holds a
per-mode flock so overlapping triggers can't clobber a report.
The narrative comes from a non-deterministic claude -p call, so a report can
occasionally come out weak. Iterate on it with force, which skips the wait and
the already-present guard (overwriting today's doc); trailing words become a
revision note woven into the prompt (recap-prompt --note):
claude-recap-job daily force # plain retry
claude-recap-job weekly force "keep ci-base and datadog-ci separate" # steered redomise tasks (see mise.toml):
mise run build # -> ./claude-status (static, CGO_ENABLED=0)
mise run test # go test ./...
mise run install # build + deploy binary, recap-job, timers; flip the daemon
mise run install-units # (re)install just the recap-job wrapper + systemd timers
mise run uninstall-units # disable and remove the recap timersmise run install is idempotent and safe to re-run: it builds, atomically
replaces ~/.local/bin/claude-status, deploys the recap-job wrapper and
re-enables the systemd timers (via install-units), and — if a
claude-status daemon is already running — stops it and relaunches the new
build detached (otherwise it just updates the binary and leaves startup to
niri's spawn-at-startup).
Plain go build works too; the only external dependency is
modernc.org/sqlite (pure Go, no CGO),
so the binary is statically linkable with CGO_ENABLED=0:
CGO_ENABLED=0 go build -o claude-status .First-time setup (hooks + niri/waybar config) is the binary's own
claude-status installsubcommand — distinct frommise run install, which builds and deploys the binary, recap-job, and timers.
main.go multi-call dispatch (switch os.Args[1])
internal/state shared name grammar, decay table, event->state mapping, render tables
internal/db SQLite layer (schema, Session, Open/Upsert/LoadLive/...)
internal/niri niri IPC: ListWindows + event-stream client + window/workspace model
internal/hook hook subcommand (state derivation, /proc->window resolution, audit row)
internal/clauded reads Claude Code's first-party per-session status files (busy/idle/waiting)
internal/daemon daemon subcommand (reconciler: aggregate, slots, decay, GC, tile cache)
internal/tile tile-data/tile-watch subcommands: pwetty tile payload model + cache
internal/waybar gen-waybar subcommand (format-icons + style.css generator)
internal/install install/uninstall subcommands (settings.json hook merge)
internal/doctor doctor + gc + events subcommands
internal/recap recap/recap-prompt/repo-backfill subcommands (digest + LLM instructions)
internal/transcript reads session transcripts (ai-title, opening prompt, cwd)
internal/git best-effort git CLI wrapper for recap project metrics
internal/resume resume subcommand (claude-resume rows/preview + zsh integration)