The stackunderflow command covers dashboard launch, usage reports, data export,
config management, session backups, and the agent-facing memory namespace. All
persistent state lives under ~/.stackunderflow/ (config at
~/.stackunderflow/config.json, session data at ~/.stackunderflow/store.db). Every
command accepts --help for a quick reminder.
# Dashboard
stackunderflow init [--port N] [--host H] [--no-browser] [--clear-cache] [--install-skills] [--skills-dest DIR] [--skills-force]
stackunderflow start [-p N] [-H H] [--headless] [--fresh] [--no-watcher] [--no-lock]
stackunderflow reindex
stackunderflow clear-cache [PROJECT]
# Reports (all eight also accept --ingest / --no-auto-ingest — see "Reading freshness")
stackunderflow status [--format text|json]
stackunderflow today [--format text|json] [--project P] [--exclude P]
stackunderflow month [--format text|json] [--project P] [--exclude P]
stackunderflow report [-p PERIOD] [--format text|json] [--project P] [--exclude P] [--provider PROV]
stackunderflow export -f csv|json -o PATH [-p today|week|month|all] [--provider X] [--project P] [--exclude P] [--force]
stackunderflow optimize [-p PERIOD] [--format text|json] [--project P] [--exclude P]
stackunderflow compare [-p today|week|month|all] [--provider X] [--project P] [--format text|json]
stackunderflow yield [-p PERIOD] [--format text|json] [--project SLUG]
stackunderflow context-budget [--project DIR] [--global] [--format text|json]
# Memory — the agent-facing namespace (stable JSON envelope; see "Memory Commands")
stackunderflow memory decisions QUERY [--project SLUG] [--since X] [--limit N] [--context-budget N] [--format text|json] [--json]
stackunderflow memory file PATH [--project SLUG] [--since X] [--limit N] [--context-budget N] [--format text|json] [--json]
stackunderflow memory worked ACTION [--project SLUG] [--since X] [--limit N] [--context-budget N] [--format text|json] [--json]
stackunderflow memory sessions [PATH] [--project SLUG] [--since X] [--limit N] [--context-budget N] [--format text|json] [--json]
stackunderflow memory ask QUESTION [--project SLUG] [--since X] [--limit N] [--context-budget N] [--format text|json] [--json]
# Discovery — lower-level self-referential queries (the `memory` namespace wraps these)
stackunderflow find-sessions-in-path PATH [--since X] [--limit N] [--provider P] [--format text|json] [--context-budget N]
stackunderflow find-sessions-touching-file FILE [--limit N] [--mode read|write|any] [--format text|json] [--context-budget N]
stackunderflow search-past-decisions QUERY [--project P] [--since X] [--limit N] [--use-embeddings] [--embed-model M] [--format text|json] [--context-budget N]
stackunderflow find-sessions-where-action-worked ACTION [--project P] [--file F] [--since X] [--limit N] [--min-confidence F] [-v] [--format text|json]
stackunderflow find-failure-modes-for-file FILE [--since X] [--limit N] [--min-confidence F] [-v] [--format text|json]
stackunderflow risk file PATH [--since X] [--format text|json]
# Auto-generated skills (mined from the local store — see docs/skills.md)
stackunderflow skills generate [--project P] [--projects A,B] [--scope project|user] [--min-occurrences N] [--kind K] [--window W] [--out DIR] [--dry-run] [--format text|json]
stackunderflow skills list [--scope project|user] [--out DIR] [--format text|json]
stackunderflow skills clean [--scope project|user] [--out DIR] [--older-than X] [--dry-run] [--yes]
# Recommendations (mined from the local store — read-only, never auto-applied)
stackunderflow recommend skills [--project P] [--threshold N] [--window-days N] [--no-cache] [--format text|json]
stackunderflow recommend mode --prompt TEXT [--current-model M] [--no-cache] [--format text|json]
# Discovery citation-feedback telemetry
stackunderflow discovery telemetry [--command C] [--session S] [--limit N] [--format text|json]
stackunderflow discovery demote-uncited [--dry-run] [--min-loads N] [--min-age-days N] [--format text|json]
# Config (legacy: config show/set/unset still works as hidden aliases for cfg ls/set/rm)
stackunderflow cfg ls [--json]
stackunderflow cfg set KEY VALUE
stackunderflow cfg rm KEY
stackunderflow cfg model-alias set FROM TO
stackunderflow cfg model-alias rm FROM
stackunderflow cfg model-alias ls [--json]
# Plan budgets
stackunderflow plan show [--format text|json]
stackunderflow plan set NAME [--monthly-usd N] [--reset-day D]
stackunderflow plan reset
stackunderflow plan thresholds show [--format text|json]
stackunderflow plan thresholds set N [N ...]
stackunderflow plan thresholds reset
# ETL pipeline
stackunderflow etl backfill [--force]
stackunderflow etl status [--format text|json]
# Hooks (opt-in Claude Code lifecycle hooks — see docs/hooks.md)
stackunderflow hooks install [--scope project|user] [--dry-run] [--capture-content]
stackunderflow hooks uninstall [--scope project|user]
stackunderflow hooks status [--scope project|user] [--format text|json]
stackunderflow hooks repair [--scope project|user|all] [--dry-run]
stackunderflow hooks run <hook-id> [--capture-content] # internal — invoked by Claude Code
# Backup
stackunderflow backup create [--label TEXT] [--keep N]
stackunderflow backup list
stackunderflow backup restore NAME [--dry-run]
stackunderflow backup auto [--enable|--disable]
# PR / CI ingest (opt-in — REST backfill + webhook receiver)
stackunderflow ingest github --repo OWNER/REPO [--token T] [--state all|open|closed] [--max-pages N] [--no-ci] [--format text|json]
stackunderflow ingest webhook serve [--port N] [--host H]
Launch the StackUnderflow dashboard.
Usage: stackunderflow start [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-p, --port |
INTEGER | from config | Server port |
-H, --host |
TEXT | from config | Bind address |
--headless |
flag | false | Don't open the browser |
--fresh |
flag | false | Clear disk cache before starting |
--no-watcher |
flag | false | Disable the ETL filesystem watcher (sets STACKUNDERFLOW_DISABLE_WATCHER=1); headless / debugging |
--no-lock |
flag | false | Skip the singleton watcher lock at ~/.stackunderflow/server.lock (sets STACKUNDERFLOW_DISABLE_LOCK=1). Headless / test only — two instances running watchers against the same store race on ingest and mart refresh |
Examples:
$ stackunderflow start
StackUnderflow is live at http://127.0.0.1:8081
Ctrl+C to stop
$ stackunderflow start -p 9000 --headless
StackUnderflow is live at http://127.0.0.1:9000
Ctrl+C to stop
$ stackunderflow start --fresh
cache cleared: /Users/you/.stackunderflow/cache
StackUnderflow is live at http://127.0.0.1:8081
Ctrl+C to stop
--no-watcher starts the server without the ETL filesystem watcher;
dashboard data then refreshes only on a manual reindex or restart.
--no-lock skips the singleton watcher lock — a headless / test escape
hatch, not a way to run two servers against one store (their watchers
race). Both print the same two-line banner as a normal start.
Network exposure. Binding to any host other than
127.0.0.1,localhost, or::1prints a yellow warning to stderr: the API has no authentication, so a non-loopback bind serves session data, tokens, and cost figures to anyone who can reach that interface. The server still starts — the warning does not block it.
Filesystem watcher. By default
startspawns a daemon thread that watches every registered adapter's source paths (for the four default-on adapters:~/.claude/projects,~/.codex/sessions, the Cursorstate.vscdb, and the Cline task store under VS Code'sglobalStorage). On any change it ingests the new bytes and refreshes the marts. Pass--no-watcher(or setSTACKUNDERFLOW_DISABLE_WATCHER=1) to skip the spawn and rely on the periodic-ingest path instead.
Restart after upgrades.
stackunderflow startis a long-lived process that loads the application code into memory once at boot. New releases (pip install -U stackunderflow) and any locally installed code changes only take effect after the server restarts — stop with Ctrl+C and re-runstackunderflow start. Routes added in a new version will return404until then.
Start the dashboard. A backward-compatible alias for start with
slightly different flag names. With --install-skills, it also copies
the three shipped Claude Code skills into place before the server boots.
Usage: stackunderflow init [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--port |
INTEGER | from config | Server port |
--host |
TEXT | from config | Bind address |
--no-browser |
flag | false | Don't open the browser (maps to start --headless) |
--clear-cache |
flag | false | Clear disk cache first (maps to start --fresh) |
--install-skills |
flag | false | Copy the three shipped Claude Code skills (check-prior-work, find-related-sessions, recall-past-decisions) into the skills destination before starting. Idempotent: byte-identical files are skipped silently |
--skills-dest |
PATH | ~/.claude/skills/ |
Destination directory for --install-skills |
--skills-force |
flag | false | With --install-skills, overwrite destination SKILL.md files that differ from the shipped copy. Default behaviour preserves local edits — a modified destination is skipped with a warning |
Skill install. --install-skills runs first, then the dashboard
starts as usual. A missing destination is created; one whose bytes
match the shipped copy is reported as already current; one that differs
is skipped with a warning unless --skills-force is set. See
docs/skills.md.
Examples:
$ stackunderflow init
$ stackunderflow init --port 9000 --no-browser
$ stackunderflow init --clear-cache
$ stackunderflow init --install-skills
+ installed skill: check-prior-work → /Users/you/.claude/skills/check-prior-work/SKILL.md
+ installed skill: find-related-sessions → /Users/you/.claude/skills/find-related-sessions/SKILL.md
+ installed skill: recall-past-decisions → /Users/you/.claude/skills/recall-past-decisions/SKILL.md
Re-ingest every registered adapter source into ~/.stackunderflow/store.db,
applying the current schema first. Ingest is incremental — each source
file is read from its stored watermark and rows land via INSERT OR IGNORE — so re-running is safe and cheap. Run it after an upgrade (a
new release may add adapters or schema) or to force a catch-up when
start and its watcher are not running.
Usage: stackunderflow reindex [OPTIONS]
No options beyond --help.
Example:
$ stackunderflow reindex
Reindexing into /Users/you/.stackunderflow/store.db
Done: {'claude': 58203, 'codex': 1171, 'cursor': 1035, 'cline': 860}
The Done: line is the per-provider count of messages ingested on this
pass (run_ingest returns a {provider: messages_added} dict).
Wipe the on-disk Cursor parse cache (~/.stackunderflow/cache/cursor-results.json)
and print guidance on clearing the rest. The Cursor parse cache is a
fingerprint-keyed snapshot of the parsed state.vscdb records; deleting it
forces the next ingest to re-read SQLite from scratch (slow but always
correct). The in-memory cache is always cleared on restart; pass --fresh
to start to also wipe the broader disk cache.
Usage: stackunderflow clear-cache [OPTIONS] [PROJECT]
| Argument | Required | Description |
|---|---|---|
PROJECT |
no | (reserved, currently unused) |
Example:
$ stackunderflow clear-cache
cursor parse cache cleared.
in-memory cache is cleared on restart.
use `stackunderflow start --fresh` to also wipe the disk cache.
To actually wipe the disk cache:
stackunderflow start --fresh
The eight read-only data commands listed in this section
(status, today, month, report, compare, yield,
optimize, export) all read from the SQLite store at
~/.stackunderflow/store.db. That store is normally kept fresh by
the filesystem watcher that stackunderflow start spawns. When
start is not running, the store reflects whatever the last
watcher snapshot wrote — which can be hours or days stale.
Two flags control how much the read commands try to refresh the store before they run:
| Flag | Default | Behaviour |
|---|---|---|
--ingest |
off | Force a fresh ingest + incremental backfill pass before the command's query. Use when stackunderflow start is not running and you need authoritative numbers right now. |
--auto-ingest / --no-auto-ingest |
--auto-ingest (on) |
Refresh automatically when the store's newest event is older than 6 hours. A one-line [stale data — ingesting...] notice goes to stderr. Disable with --no-auto-ingest (e.g. inside scripts that don't want surprise latency). |
--ingest overrides --no-auto-ingest: an explicit force always
runs the pass. The refresh runs synchronously in the CLI's
lifetime — it blocks the command until the pass completes.
The refresh path is identical to the one
stackunderflow start runs at boot: enumerate every registered
adapter, ingest new bytes via the per-file watermark, then call
backfill(conn, force=False) to materialise any newly-inserted
messages into usage_events and refresh the mart watermarks.
When the start server is running in another terminal it owns the
watcher lock. The CLI ingest path runs anyway — it shares the same
ETL transactions and INSERT OR IGNORE dedup, so concurrent passes
do not corrupt the store, though the second pass will see most rows
as already-ingested.
Compact one-liner showing today's and this month's cost and message counts.
Equivalent to running today and month together and condensing to a single line.
Usage: stackunderflow status [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--format |
text|json |
text | Output format |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Example:
$ stackunderflow status
today: $34.61 (558 msg) | month: $558.65 (22681 msg)
$ stackunderflow status --format json
{
"today": { ... },
"month": { ... }
}
See also:
todayandmonthfor full per-project tables.
Today's usage broken down by project.
Usage: stackunderflow today [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--format |
text|json |
text | Output format |
--project |
TEXT | (all) | Include only this project dir name (repeatable) |
--exclude |
TEXT | (none) | Exclude this project dir name (repeatable) |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Example:
$ stackunderflow today
StackUnderflow — today
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Project ┃ Cost ┃ Messages ┃ Sessions ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ -Users-you-dev-my-api │ $15.21 │ 116 │ 1 │
│ -Users-you-dev-my-app │ $2.95 │ 125 │ 1 │
└───────────────────────────────────────────────┴────────┴──────────┴──────────┘
Total: $18.16 241 messages 2 sessions
$ stackunderflow today --project my-api --format json
This month's usage broken down by project.
Usage: stackunderflow month [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--format |
text|json |
text | Output format |
--project |
TEXT | (all) | Include only this project dir name (repeatable) |
--exclude |
TEXT | (none) | Exclude this project dir name (repeatable) |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Example:
$ stackunderflow month
StackUnderflow — this month
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Project ┃ Cost ┃ Messages ┃ Sessions ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ -Users-you-dev-StackUnderflow │ $138.56 │ 5,665 │ 10 │
│ -Users-you-dev-my-api │ $91.91 │ 2,939 │ 3 │
└──────────────────────────────────────────────┴─────────┴──────────┴──────────┘
Total: $230.47 8,604 messages 13 sessions
$ stackunderflow month --exclude StackUnderflow
Dashboard-style summary over a configurable date range.
Usage: stackunderflow report [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-p, --period |
TEXT | 7days |
Period: today, 7days, 30days, month, all |
--format |
text|json |
text | Output format |
--project |
TEXT | (all) | Include only this project dir name (repeatable) |
--exclude |
TEXT | (none) | Exclude this project dir name (repeatable) |
--provider |
all|claude|codex|cursor|opencode|pi|copilot |
all |
Provider filter. Accepted for forward-compatibility but not yet applied — the report aggregates every provider regardless of this value |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Valid period strings: today, 7days, 30days, month, all. Any other value
exits with code 1 and prints Error: Unknown period '<value>'. Valid: today, 7days, 30days, month, all.
Examples:
$ stackunderflow report
StackUnderflow — last 7 days
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Project ┃ Cost ┃ Messages ┃ Sessions ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ -Users-you-dev-StackUnderflow │ $138.56 │ 5,665 │ 10 │
│ -Users-you-dev-chimera │ $91.91 │ 2,939 │ 3 │
└──────────────────────────────────────────────┴─────────┴──────────┴──────────┘
Total: $453.88 14,782 messages 48 sessions
$ stackunderflow report -p 30days --project StackUnderflow
$ stackunderflow report -p all --format json
$ stackunderflow report -p today --exclude sandbox
Export aggregated, cross-project usage data to a file. Both --format
and --output are required. Designed for spreadsheets, BI tools, and
downstream automation.
Usage: stackunderflow export [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-f, --format |
csv|json |
(required) | Output format |
-o, --output |
PATH | (required) | File path to write |
-p, --period |
today|week|month|all |
(multi-period rollup) | Single window. Omit for today + 7 days + 30 days in one file. |
--provider |
TEXT | (all) | Filter by provider (e.g. claude, codex, cursor) |
--project |
TEXT | (all) | Include only this project slug (repeatable) |
--exclude |
TEXT | (none) | Exclude this project slug (repeatable) |
--force |
flag | false | Overwrite the output file if it already exists |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
CSV layout. Each period section starts with a # period: <label>
comment row, followed by the daily-rows header
(date, provider, project, cost_usd, calls, sessions, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens) and the rows.
A blank line separates each period from the activity-breakdown section
(# activity — <label>, then activity, calls, share_pct). With no
--period, three pairs of sections are emitted (today / last 7 days /
last 30 days) in the same file.
JSON layout. With --period, the file is one period dict with
label, since, until, totals, daily, projects, models,
activities, tools, mcp, shell. Without --period, a top-level
{schema, generated, filters, today, last_7d, last_30d} envelope wraps
three of those dicts so a single file is enough for short / medium /
long windows side-by-side.
File-write safety. The command refuses to overwrite an existing
file unless --force is set, refuses to follow symlinks at the output
path, and writes atomically via a .tmp file that is renamed into
place. Parent directories are created if missing.
Examples:
$ stackunderflow export --format csv --output ~/usage-week.csv --period week
wrote /Users/you/usage-week.csv
$ stackunderflow export -f json -o ~/usage-rollup.json
wrote /Users/you/usage-rollup.json
$ stackunderflow export -f csv -o ~/claude-only.csv --provider claude --period month
$ stackunderflow export -f csv -o ~/big.csv -p all --exclude sandbox --force
$ jq '.last_30d.projects[] | select(.cost_usd > 10)' < ~/usage-rollup.json
The same data is available over HTTP at GET /api/export with the
matching query parameters (format, period, provider, project,
exclude) — used by the dashboard's "Download" button.
Surface wasted spend in two blocks. Q&A loops lists projects where
the assistant retried the same question repeatedly. Structural
patterns reports waste detected from filesystem state and tool-call
history — seven detectors: bloated CLAUDE.md, unused MCP servers,
ghost agents, exploration-only sessions, files re-read excessively,
cache thrash, and oversized bash output.
Usage: stackunderflow optimize [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-p, --period |
TEXT | 30days |
Period: today, 7days, 30days, month, all |
--format |
text|json |
text | Output format |
--project |
TEXT | (all) | Include only this project dir name (repeatable) |
--exclude |
TEXT | (none) | Exclude this project dir name (repeatable) |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Examples:
$ stackunderflow optimize --period 7days
No waste or structural patterns found in last 7 days.
$ stackunderflow optimize --period 30days
Waste report — last 30 days
Q&A loops:
my-api: 3 looped pair(s)
- How do I fix the auth middleware?
- Why does the test keep failing?
Structural patterns:
[MEDIUM] bloated_claude_md: 2 bloated CLAUDE.md file(s)
2 CLAUDE.md file(s) exceed 5,000 tokens and are loaded into every session's context.
~7,400 wasted tokens
fix: Trim CLAUDE.md to the bare essentials — move long-form notes to project-local docs and reference them on demand.
$ stackunderflow optimize --period all --format json
JSON output is {"scope": "<label>", "waste": [...], "patterns": [...]}.
Each patterns entry carries pattern_id, severity, title,
description, estimated_waste_tokens, suggested_fix, and a
details payload. estimated_waste_tokens is null for detectors
that can't put a token figure on the waste.
Side-by-side per-model comparison over a time window — answers "is it worth using Opus for this kind of work?" by surfacing one-shot rate, retry rate, cache hit rate, and unit economics (
Usage: stackunderflow compare [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-p, --period |
today|week|month|all |
month |
Window over which to compare |
--provider |
TEXT | (all) | Filter by provider id (claude, codex, cursor, …) |
--project |
TEXT | (all) | Restrict to this project slug (repeatable) |
--format |
text|json |
text | Output format |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Metrics:
- 1-shot % — fraction of sessions where the user asked once, the assistant answered once, and that was it (heuristic: exactly 1 user + 1 assistant message).
-
Retry rate —
(assistant_messages / sessions) - 1, i.e. the average number of extra assistant turns per session beyond the first answer. -
Cache % —
cache_read / (cache_read + cache_create), the prompt-cache hit rate; high is good (most prompt context was reused). -
$/call —
total_cost / calls(per assistant message). -
$/session —
total_cost / sessions(sessions are attributed to whichever model dominated the session — most assistant messages wins, ties broken alphabetically). -
Total $ — sum of
compute_costover every assistant message in the window.
Examples:
$ stackunderflow compare
Compare — month
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓
┃ Model ┃ Sessions ┃ Calls ┃ 1-shot% ┃ Retry ┃ Cache% ┃ $/call ┃ $/session ┃ Total$ ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩
│ claude-opus-4-6 │ 12 │ 480 │ 16.7% │ 39.00 │ 92.4% │ $0.0210 │ $0.84 │ $10.07 │
│ claude-sonnet-4-6 │ 38 │ 1,240 │ 34.2% │ 31.63 │ 88.1% │ $0.0034 │ $0.11 │ $4.21 │
│ gpt-5 │ 4 │ 18 │ 75.0% │ 3.50 │ 0.0% │ $0.0040 │ $0.02 │ $0.07 │
└────────────────────────────┴──────────┴───────┴─────────┴───────┴────────┴─────────┴───────────┴────────┘
$ stackunderflow compare --period week --provider claude --format json
{
"generated": 1746125443.117,
"models": [
{
"cache_hit_rate": 0.94,
"calls": 124,
"cost_per_call": 0.018,
"cost_per_session": 0.74,
"model": "claude-opus-4-6",
"one_shot_pct": 0.0,
"provider": "claude",
"retry_rate": 40.33,
"sessions": 3,
"total_cost": 2.21,
"total_tokens": 4120000
}
],
"period": "week"
}
The payload is json.dumps(..., indent=2, sort_keys=True), so keys are
alphabetical. The same data is available via GET /api/compare — see
docs/api-reference.md.
Yield analysis — correlate AI sessions with the git commit history of their cwd.
For each session in the window, the command resolves the session's cwd to a
git repo, runs git log over the 24h after the session started, and
classifies the result:
| Class | Meaning |
|---|---|
productive |
A commit landed within 24h and is still reachable from HEAD. |
reverted |
A commit landed but was later reverted (by git revert or by being wiped from HEAD via reset / force push). |
abandoned |
No commit landed within the window. |
no_repo |
The session's cwd is missing or isn't a git repository. |
Heuristic warning. This correlates by time, not by content. A commit inside the 24h window is credited to the session even if it was about something else. Treat the breakdown as a smoke signal, not a verdict. Sessions and commits don't have to share a topic to get matched up.
Usage: stackunderflow yield [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
-p, --period |
TEXT | month |
Period: today, week, month, all, 7days, 30days |
--project |
TEXT | (all) | Filter by project slug (repeatable) |
--format |
text|json |
text | Output format |
--ingest |
flag | off | Force a fresh ingest pass — see Reading freshness above |
--auto-ingest / --no-auto-ingest |
flag | on | Auto-refresh when the store is > 6 h stale |
Examples:
$ stackunderflow yield -p week
Yield analysis — period: week
productive: 13 ($724.77)
reverted: 0 ($0.00)
abandoned: 8 ($1080.25)
no_repo: 3 ($0.92)
total: 24 ($1805.94)
Top sessions by cost:
CLASS COST PROJECT SESSION
abandoned $ 266.39 -Users-yadkonrad-dev-dev-yea 910a9d68-...
productive $ 247.88 -Users-yadkonrad-dev-dev-yea ada0010e-...
...
note: yield is correlated by time, not by content — a commit within 24h is credited to the session even if unrelated.
$ stackunderflow yield -p month --format json
The same data is available over HTTP at GET /api/yield with the matching
query parameters (period, project).
Limits:
- Sessions whose
cwdlives on a path you've since deleted, renamed, or moved outside its original git work tree are reported asno_repo. - The 24h window is fixed in v1. A multi-session day in one repo will share the same follow-up commit attribution across every session that ran first.
- Each git invocation has a 5-second timeout; a hung repo (e.g. NFS lock)
falls through to
no_reporather than stalling the report.
Estimate the per-session "context tax" — the tokens every Claude Code turn pays before the user types: system prompt + registered MCP servers
- available skills + agent definitions + memory files (project
CLAUDE.md, global~/.claude/CLAUDE.md).
Usage: stackunderflow context-budget [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--project |
PATH | cwd | Project directory to inspect |
--global |
flag | false | Estimate the global budget only (~/.claude); ignore project files |
--format |
text|json |
text | Output format |
Estimation heuristic — explicit and approximate.
| Source | Cost |
|---|---|
| System prompt | Fixed DEFAULT_SYSTEM_PROMPT_TOKENS=3000 (Claude Code default, public scratch count) |
CLAUDE.md files |
len(content) // 4 — 1 token ≈ 4 characters of English |
| MCP server | MCP_BASE_TOKENS=200 + MCP_PER_TOOL_TOKENS=50 × declared tools, or MCP_UNKNOWN_TOOLS_FALLBACK=200 flat when tool counts aren't statically known (the common case) |
Skill (SKILL.md) |
len(content) // 4 |
Subagent (*.md) |
len(content) // 4 |
| Cost projection | Total tokens × $3/M (current Sonnet input rate) per session, × 100 sessions/month |
Underestimates code-heavy or non-Latin content; the full heuristic
string is echoed in every output payload (heuristic field in JSON).
Useful for spotting bloat — not for billing.
A budget over 20k tokens triggers a yellow warning in the text output
and a context_budget_bloat finding (severity: medium) when consumed
by the optimize report.
Examples:
$ stackunderflow context-budget
Context budget (per-session estimate)
heuristic: len(text) // 4; per-MCP-server 200 + 50/tool
system_prompt 3,000 tok (fixed)
memory:project_CLAUDE.md 512 tok /path/to/project/CLAUDE.md
memory:global_CLAUDE.md 876 tok /Users/you/.claude/CLAUDE.md
mcp:filesystem 400 tok /Users/you/.claude.json
mcp:tavily 400 tok /Users/you/.claude.json
skill:anti-slop-guide 204 tok /Users/you/.claude/skills/anti-slop-guide/SKILL.md
...
total: 8,142 tokens
cost per session: $0.0244
estimated monthly cost: $2.44
$ stackunderflow context-budget --global --format json
$ stackunderflow context-budget --project ~/code/my-app
The same data is available over HTTP at GET /api/context-budget with
a project=<slug> query parameter (omit for the global budget).
Show all settings with their sources (default, file, or env).
Usage: stackunderflow cfg ls [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--json |
flag | false | JSON output instead of table |
Each row's [source] tag is default (built-in), file (set in
config.json), or env (an environment variable currently overrides it).
The text table is sorted alphabetically; the JSON keeps declaration order.
Examples (fresh install — every key at its built-in default):
$ stackunderflow cfg ls
Settings:
auto_browser True [default]
auto_reindex_on_ingest True [default]
currency USD [default]
discovery_budget_tokens 2000 [default]
discovery_rank_weights 0.5,0.2,0.3 [default]
host 127.0.0.1 [default]
log_level INFO [default]
max_date_range_days 30 [default]
messages_initial_load 500 [default]
model_aliases {} [default]
plan_alert_thresholds [50, 75, 90] [default]
plan_monthly_usd None [default]
plan_name None [default]
plan_reset_day 1 [default]
port 8081 [default]
$ stackunderflow cfg ls --json
{
"port": 8081,
"host": "127.0.0.1",
"auto_browser": true,
"max_date_range_days": 30,
"messages_initial_load": 500,
"log_level": "INFO",
"auto_reindex_on_ingest": true,
"currency": "USD",
"model_aliases": {},
"plan_name": null,
"plan_monthly_usd": null,
"plan_reset_day": 1,
"plan_alert_thresholds": [
50,
75,
90
],
"discovery_budget_tokens": 2000,
"discovery_rank_weights": "0.5,0.2,0.3"
}
Legacy alias:
stackunderflow config show [--json]
Write a key-value pair to the config file (~/.stackunderflow/config.json).
Usage: stackunderflow cfg set [OPTIONS] KEY VALUE
No options beyond --help.
Examples:
$ stackunderflow cfg set port 9000
port = 9000
$ stackunderflow cfg set auto_browser false
auto_browser = False
$ stackunderflow cfg set log_level DEBUG
log_level = DEBUG
Valid keys: port, host, auto_browser, max_date_range_days,
messages_initial_load, log_level, auto_reindex_on_ingest, currency,
discovery_budget_tokens, discovery_rank_weights (full list in
Config Keys Reference below). Passing an unknown key
exits with an error. Structured settings (model_aliases, the plan_* group)
are rejected here — use their dedicated subcommands (cfg model-alias, plan set).
Legacy alias:
stackunderflow config set KEY VALUE
Remove a key from the config file, reverting it to its built-in default.
Usage: stackunderflow cfg rm [OPTIONS] KEY
No options beyond --help.
Examples:
$ stackunderflow cfg rm port
port removed
$ stackunderflow cfg rm auto_browser
auto_browser removed
Legacy alias:
stackunderflow config unset KEY
Manage model aliases — a map from a proxy-rewritten model id to a canonical id our pricing tables know about. Use this when sessions go through OpenRouter, Replicate, LiteLLM, or an internal company gateway that rewrites model names.
Why you'd reach for this. Suppose your sessions emit
"model": "openrouter/claude-opus" but our rate tables only know
claude-opus-4-6. Without an alias, compute_cost() falls into the
fallback rates (or even returns $0 once stricter resolution lands), and
spend on those sessions is silently misreported. Adding the alias
openrouter/claude-opus → claude-opus-4-6 patches the gap so the
dashboard shows the real number.
Usage: stackunderflow cfg model-alias set FROM TO
stackunderflow cfg model-alias rm FROM
stackunderflow cfg model-alias ls [--json]
| Argument | Required | Description |
|---|---|---|
FROM |
yes (set / rm) | The proxy-rewritten id as it appears in your session logs |
TO |
yes (set) | The canonical id (must match a key our pricers recognise) |
| Option | Type | Default | Description |
|---|---|---|---|
--json |
flag | false | (ls only) JSON output instead of table |
Resolution semantics. Aliases are consulted before the
provider-specific canonicalize / identify logic in compute_cost().
Resolution is single-step — no recursive chasing — so a chain like
a → b → c returns b, not c. Aliasing to an unknown canonical id
falls through to the existing fallback behaviour rather than looping.
Examples:
$ stackunderflow cfg model-alias set openrouter/claude-opus claude-opus-4-6
openrouter/claude-opus -> claude-opus-4-6
$ stackunderflow cfg model-alias set litellm/sonnet claude-sonnet-4-6
litellm/sonnet -> claude-sonnet-4-6
$ stackunderflow cfg model-alias ls
Model aliases:
litellm/sonnet -> claude-sonnet-4-6
openrouter/claude-opus -> claude-opus-4-6
$ stackunderflow cfg model-alias ls --json
{
"litellm/sonnet": "claude-sonnet-4-6",
"openrouter/claude-opus": "claude-opus-4-6"
}
$ stackunderflow cfg model-alias rm litellm/sonnet
litellm/sonnet removed
Worked end-to-end example.
$ stackunderflow cfg model-alias set my-proxy claude-opus-4-6
my-proxy -> claude-opus-4-6
$ python -c "from stackunderflow.infra.costs import compute_cost; \
print(compute_cost({'input': 1000, 'output': 1000}, 'my-proxy')['total_cost'])"
0.09
The alias map is stored under the model_aliases key in
~/.stackunderflow/config.json. Generic cfg set model_aliases ... is
intentionally rejected — use this dedicated subcommand instead.
Track monthly AI spend against a known plan (Claude Pro, Claude Max, Cursor Pro, Cursor Max, or a custom amount). Status banding tells you whether you're on track:
| pct of budget | status |
|---|---|
< 80% |
ok |
80% – 100% |
warn |
> 100% |
over |
The plan is stored in three settings keys (plan_name, plan_monthly_usd,
plan_reset_day) but managed through this command — cfg set plan_name ...
is intentionally rejected because the three keys have inter-key invariants.
The Projected figure comes from the burn projector: once the period
has at least three non-zero daily samples it uses a weighted 7-day
average (recent days dominate, decay 0.85/day), falling back to a linear
running mean below that. Read it as a directional signal, not a forecast
— it does not model weekends, project ramps, or week-of-month
seasonality. The Projection method note under plan show covers the
switch in detail.
The cost rollup reuses the same engine as stackunderflow month
(reports.aggregate.build_report), so the Used number always matches
what the dashboard's monthly spend shows.
Print the active plan and current usage against budget.
Usage: stackunderflow plan show [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--format |
text|json |
text | Output format |
Example:
$ stackunderflow plan show
Plan: claude-pro
Budget: $20.00 / month (resets day 1)
Period: 2026-05-01 → 2026-05-31 (day 12 of 31)
Used: $12.50 (62.5% of budget)
Remaining: $7.50
Projected: $32.29 (weighted-7d, $1.04/day burn)
Days to limit: ~7 days at current burn
Status: ok
Alert: Crossed 50% of plan budget
$ stackunderflow plan show --format json
{
"plan": {"name": "claude-pro", "monthly_usd": 20.0, "reset_day": 1},
"usage": {
"used": 12.5,
"budget": 20.0,
"remaining": 7.5,
"pct": 62.5,
"projected_month_end": 32.29,
"status": "ok",
"period_start": "2026-05-01",
"period_end": "2026-05-31",
"days_so_far": 12,
"days_in_period": 31
},
"projection": {
"projected_month_end_usd": 32.29,
"projection_method": "weighted-7d",
"daily_burn_usd": 1.04,
"days_to_limit": 7,
"thresholds": [50, 75, 90],
"crossed_threshold": 50,
"alert": "Crossed 50% of plan budget"
}
}
If no plan is set:
$ stackunderflow plan show
No plan set. Run: stackunderflow plan set claude-pro
The same payload is exposed over HTTP at GET /api/plan. With no plan
configured, the route returns {"plan": null, "usage": null, "projection": null}.
Projection method. Burn projector v2 auto-picks weighted-7d once the
period has accumulated at least 3 non-zero daily samples; the weighted
average decays at 0.85/day so recent activity dominates and weekends fade.
Below that threshold (or when the last 7 days are all zero against an
otherwise non-empty period — the stale-store case), the method falls back
to linear (running mean over the period to date) and the response
surfaces projection_method: "linear" so the cause is visible.
Alert thresholds. Default to 50 / 75 / 90 percent of budget; override
via stackunderflow plan thresholds set N N N. The CLI / route / UI
surface a banner when the most-severe threshold crossed bumps up; a
separate "projected to overrun" banner supersedes the threshold notice
when the forecast exceeds the budget before the period ends.
Set the active plan. Preset names accept an optional --monthly-usd to
override the listed amount (useful if you're grandfathered into an
older price); custom requires --monthly-usd.
Usage: stackunderflow plan set NAME [OPTIONS]
| Argument | Required | Description |
|---|---|---|
NAME |
yes | One of claude-pro, claude-max, cursor-pro, cursor-max, custom |
| Option | Type | Default | Description |
|---|---|---|---|
--monthly-usd |
FLOAT | (preset amount) | Required for custom; overrides the preset amount otherwise |
--reset-day |
INTEGER (1–31) | 1 | Day-of-month the billing window rolls over |
Preset amounts:
| Preset | Monthly USD |
|---|---|
claude-pro |
20 |
claude-max |
200 |
cursor-pro |
20 |
cursor-max |
40 |
custom |
--monthly-usd (required) |
--reset-day semantics. A reset day greater than the current month's
length clamps to the last day of the month — so --reset-day 31 lands on
Feb 28 (or 29 in leap years), then rolls back to 31 on March.
Examples:
$ stackunderflow plan set claude-pro
plan = claude-pro ($20.00/month, resets day 1)
$ stackunderflow plan set claude-max --reset-day 15
plan = claude-max ($200.00/month, resets day 15)
$ stackunderflow plan set custom --monthly-usd 75
plan = custom ($75.00/month, resets day 1)
$ stackunderflow plan set claude-pro --monthly-usd 18
plan = claude-pro ($18.00/month, resets day 1) # grandfathered price
Clear the active plan. Afterwards plan show prints No plan set. Run: stackunderflow plan set claude-pro, and plan show --format json
returns {"plan": null, "usage": null}.
Usage: stackunderflow plan reset
Example:
$ stackunderflow plan reset
plan cleared
Manage the burn-projector alert thresholds — the percentages of the plan
budget at which the CLI / /api/plan / UI surface a banner. Defaults to
50 / 75 / 90. Stored as a single settings key (plan_alert_thresholds)
and managed exclusively through this subcommand — cfg set rejects the
key with a hint pointing here.
Usage: stackunderflow plan thresholds {show, set, reset}
| Subcommand | Args | Description |
|---|---|---|
show |
[--format text|json] |
Print the active thresholds (default text) |
set |
N [N ...] |
Set thresholds (positional integers in [1, 200]) — deduped and sorted before write |
reset |
(none) | Restore the built-in default 50 75 90 |
Examples:
$ stackunderflow plan thresholds show
thresholds = 50%, 75%, 90%
$ stackunderflow plan thresholds set 60 80 95
thresholds = 60%, 80%, 95%
$ stackunderflow plan thresholds set 90 50 50 75
thresholds = 50%, 75%, 90% # deduped + sorted
$ stackunderflow plan thresholds reset
thresholds = 50%, 75%, 90% (default)
The ETL pipeline turns raw messages rows into a usage_events fact table and
eight indexed marts (daily_mart, session_mart, project_mart,
provider_day_mart, model_day_mart, tool_mart, command_mart,
message_tool_mart). The watcher keeps everything fresh in the background; these
commands let you do a one-shot backfill and check pipeline health from the command
line.
Convert all existing messages rows into usage_events, then refresh
every mart from the new watermark. The default run is incremental:
messages already converted on a prior run are skipped via the
uniq_events_msg UNIQUE index. When tqdm is installed a progress bar
is shown; otherwise the orchestrator logs a line every 10K events.
Usage: stackunderflow etl backfill [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--force |
flag | false | Wipe usage_events + mart_watermark, rebuild every mart from scratch, then run the normalize pass fresh — useful after a normalizer change or model-rate update |
Example:
$ stackunderflow etl backfill
Backfill complete.
events inserted: 247,278
events skipped (duplicate): 0
marts refreshed:
command 247,278 events
daily 247,278 events
message_tool 247,278 events
model_day 247,278 events
project 247,278 events
provider_day 247,278 events
session 247,278 events
tool 247,278 events
duration: 4.812s
All eight marts appear here because backfill refreshes every
registered mart. etl status (below) reports only five of them.
One-line health check for the ETL pipeline: watcher state, mart watermarks vs
the max event id, per-provider event counts, and an overall health enum
(live / syncing / stale / error). Same payload as GET /api/etl/status.
Reads the store directly, so it works without a running server.
Usage: stackunderflow etl status [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--format |
text|json |
text | Output format |
Health rules (evaluated worst-first — lag is max_event_id minus the
laggiest mart watermark, threshold 100 events):
error— lag is over 100 events and the watcher reportsrunning=false(we are behind, and nothing is catching up).stale— lag is over 100 events but the watcher is not known-stopped (catch-up may still happen).syncing— lag is above 0 but a refresh ran in the last 10 seconds (pipeline is actively catching up).live— zero lag, lag within the 100-event threshold, or no events at all.
A recently-failed backfill also forces error for the ~30 s its result
slot is retained, regardless of mart lag.
Example:
$ stackunderflow etl status
ETL pipeline — live (last refresh 7s ago)
Events: 228,311 total (228,311 max id)
by provider: claude=225,245 codex=1,171 cursor=1,035 cline=860
by cost source: rate_card=226,000 estimated=2,311
Marts:
daily=4,521 rows (watermark 228,311, fresh)
session=1,106 rows (watermark 228,311, fresh)
project=188 rows (watermark 228,311, fresh)
provider_day=312 rows (watermark 228,311, fresh)
model_day=482 rows (watermark 228,311, fresh)
Watcher: running
last cycle: 12 events processed
lock held by PID 48213
etl status surfaces five marts — daily, session, project,
provider_day, model_day (the Wave 2B set). The other three registered
marts (tool, command, message_tool) are refreshed by backfill and
the watcher but not shown here.
The last cycle: line appears only when the watcher handle reports a
last-cycle count, and lock held by PID N only when a watcher lock file
is present and readable — each is a separate indented line, not a suffix
on the Watcher: line.
JSON output. The payload is json.dumps(..., indent=2, sort_keys=True),
so keys are alphabetical and every nested object is fully expanded; the
inner objects are shown compact here only to keep the example short.
$ stackunderflow etl status --format json
{
"current_job": null,
"events": {
"by_cost_source": {"estimated": 2311, "rate_card": 226000},
"by_provider": {"claude": 225245, "cline": 860, "codex": 1171, "cursor": 1035},
"max_id": 228311,
"total": 228311
},
"health": "live",
"lag_seconds": 0,
"last_job": null,
"marts": {
"daily": {"last_refresh_ts": "2026-05-06T08:24:11+00:00", "row_count": 4521, "watermark": 228311},
"model_day": {"last_refresh_ts": "2026-05-06T08:24:11+00:00", "row_count": 482, "watermark": 228311},
"project": {"last_refresh_ts": "2026-05-06T08:24:11+00:00", "row_count": 188, "watermark": 228311},
"provider_day": {"last_refresh_ts": "2026-05-06T08:24:11+00:00", "row_count": 312, "watermark": 228311},
"session": {"last_refresh_ts": "2026-05-06T08:24:11+00:00", "row_count": 1106, "watermark": 228311}
},
"watcher": {
"enabled": true,
"events_in_last_cycle": 12,
"last_refresh_ts": "2026-05-06T08:24:11+00:00",
"lock_held_by": 48213,
"running": true,
"seconds_since_refresh": 7
}
}
marts carries the same five marts as the text view. current_job and
last_job reflect the in-process backfill slots — both null when no
backfill ran recently. lag_seconds is a misnomer kept for compatibility:
it counts events behind, not seconds.
When the CLI is run with no live server (the typical case for etl status),
watcher.running reports "unknown" — the assembler cannot introspect a
daemon thread that lives in another process.
stackunderflow memory is the agent-facing namespace — the single set of
commands a coding agent learns to ask the local store "have I decided this
before, what broke on this file, what worked last time". Each subcommand
wraps an existing discovery query (see Discovery Commands
below); memory adds a uniform option set and, with --json, a stable
output envelope built for splicing into a context window.
Every memory subcommand carries the same six options:
| Option | Default | Description |
|---|---|---|
--format text|json |
text |
text for humans, json for agents |
--json |
— | Shortcut for --format json |
--project SLUG |
cwd → slug | Scope to one project. Default: the current directory's project when StackUnderflow recognises it, otherwise all projects |
--since X |
(all time) | Time lower bound — 7d / 1w / 1m / 24h or an ISO date/datetime |
--limit N |
20 |
Hard cap on the number of results |
--context-budget N |
env or 2000 |
Token budget — results are ranked and packed greedily until ~N estimated tokens are used; 0 disables |
--project defaulting to the current directory is what makes these ergonomic
inside a repo: stackunderflow memory file src/foo.py Just Works without
naming the project.
With --json (or --format json), every subcommand emits one envelope:
{
"schema": "stackunderflow.memory/1",
"command": "decisions",
"query": { "text": "retry logic", "project": "-Users-you-dev-app",
"since": null, "limit": 20 },
"results": [ { "...": "command-specific result shape" } ],
"result_count": 7,
"token_estimate": 1840,
"budget": 2000,
"truncated": false
}| Field | Meaning |
|---|---|
schema |
stackunderflow.memory/<N>; the integer bumps only on a breaking change |
command |
The subcommand that produced the envelope |
query |
The resolved inputs, echoed back (project is the resolved slug) |
results |
The rows — discovery result dicts; the shape is per-command |
result_count |
len(results) |
token_estimate |
chars/4 estimate of results — what the payload costs to splice in |
budget |
The --context-budget that was enforced (0 = no budget) |
truncated |
true when the budget dropped at least one ranked row |
Contract guarantees: the envelope is stable and versioned, deterministic
(same store + same query → byte-identical JSON), and token-bounded. In
--format json, stdout is pure JSON with nothing on stderr; a non-zero exit
means stdout is an {"error": "..."} envelope instead. memory file adds a
risk block alongside the eight core fields; memory ask adds a note.
"Did I decide something about this before?" — substring-searches QUERY
across past message content, newest first, each result with a snippet. Wraps
search_past_decisions.
Usage: stackunderflow memory decisions [OPTIONS] QUERY
Example:
$ stackunderflow memory decisions "watcher flag"
Past decisions matching 'watcher flag' (2 session(s))
...
$ stackunderflow memory decisions "retry logic" --json
{
"schema": "stackunderflow.memory/1",
"command": "decisions",
...
}
"What do I know about this file?" — merges three file-scoped discovery calls
into one report: known failure modes, every session that touched the file, and
a risk summary. PATH is resolved against the current directory. Wraps
find_failure_modes_for_file + find_sessions_touching_file + risk file.
Usage: stackunderflow memory file [OPTIONS] PATH
Each results[] row carries a kind of "failure_mode" or "touched"; the
envelope's extra risk block carries total_sessions, reverted, failed,
worked, and recent_session_ids. --since filters the failure-mode and risk
portions — the touching-sessions query has no time bound.
$ stackunderflow memory file stackunderflow/routes/cost.py --json
"What worked last time I tried this?" — sessions where ACTION was performed
and the next user turn confirmed success. Wraps
find_sessions_where_action_worked.
Usage: stackunderflow memory worked [OPTIONS] ACTION
"Which past sessions ran here?" — with no PATH, lists sessions for the current
directory's project; give a directory to scope to that tree, or a file to list
only the sessions that touched it. An explicit --project SLUG overrides
PATH. Wraps find_sessions_in_path / find_sessions_touching_file — the file
form has no time bound, so --since applies to the path form only.
Usage: stackunderflow memory sessions [OPTIONS] [PATH]
A natural-language question of the local store. v1 is a keyword search:
ask runs the same query as memory decisions on QUESTION and labels the
result ask, with a note saying so. The local-LLM meta-agent — which needs a
running Ollama, so an agent caller cannot rely on it — is deliberately
deferred; until it lands, memory decisions with specific terms is the precise
tool.
Usage: stackunderflow memory ask [OPTIONS] QUESTION
Self-referential queries over the local store: "what's happened in this project /
this file / about this decision?" — designed for a coding agent to call before
starting non-trivial work, so it can reuse prior context instead of re-deriving it.
These are the lower-level commands the memory namespace wraps; they
read the store directly (no running server needed), accept --format text|json
(the JSON shape is stable), and resolve ~ / relative paths before matching.
--since everywhere accepts a relative spec (7d, 1w, 1m, 24h) or an
ISO-8601 date/datetime. An unparseable value exits with a --since parameter
error.
find-sessions-in-path, find-sessions-touching-file, and search-past-decisions
are budget-aware: within the --limit hard cap, results are ranked
(recency + cost + relevance — weights tunable via
STACKUNDERFLOW_DISCOVERY_RANK_WEIGHTS) and packed greedily until roughly
--context-budget estimated tokens (chars/4 heuristic) are used, so an agent
calling them doesn't get an unprioritised dump into a tight context window.
| Aspect | Behaviour |
|---|---|
| Default | STACKUNDERFLOW_DISCOVERY_BUDGET_TOKENS (env) or 2000 |
| Disable | pass --context-budget 0 — then --limit is the only cap |
| Text output | when rows were dropped, a tail marker reports how many more matched |
| JSON output | always carries _budget_used_tokens / _budget_max_tokens; adds _truncated: true + _more_available: <count> when rows were dropped |
List sessions whose project root is PATH or any ancestor of PATH (ancestor-only
— projects rooted below PATH don't match). Useful when an agent is working in
/a/b/c and wants to know what's happened in the project rooted at /a/b.
Usage: stackunderflow find-sessions-in-path [OPTIONS] PATH
| Option | Type | Default | Description |
|---|---|---|---|
--since |
TEXT | (all time) | Only sessions whose last activity is newer than this (7d/1w/1m/24h/ISO) |
--limit |
INTEGER | 20 |
Max sessions to return (hard cap) |
--provider |
TEXT | (all) | Filter by provider slug (claude, codex, cursor, …) |
--context-budget |
INTEGER | env or 2000 |
Token budget for the output; 0 disables |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow find-sessions-in-path "$(pwd)" --since 30d --limit 5
Sessions in path /Users/you/dev/my-app (3 session(s))
[claude] abc123def456… 2026-05-10T14:22:01 msgs=128 $1.4210
-Users-you-dev-my-app /Users/you/dev/my-app
...
$ stackunderflow find-sessions-in-path . --format json --limit 5 --context-budget 1500
List sessions where FILE shows up in tool calls (Read / Edit / Write /
MultiEdit / NotebookEdit, Bash redirects, …) or message text.
Usage: stackunderflow find-sessions-touching-file [OPTIONS] FILE
| Option | Type | Default | Description |
|---|---|---|---|
--limit |
INTEGER | 20 |
Max sessions to return (hard cap) |
--mode |
read|write|any |
any |
read = Read-style accesses; write = Edit/Write/MultiEdit/NotebookEdit mutations; any = any mention (tools or freeform) |
--context-budget |
INTEGER | env or 2000 |
Token budget for the output; 0 disables |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow find-sessions-touching-file stackunderflow/store/queries.py --mode write
Sessions touching stackunderflow/store/queries.py (mode=write) (2 session(s))
...
$ stackunderflow find-sessions-touching-file ~/code/app/main.py --format json --limit 5
Substring-search QUERY across past message content (and tool-call arguments);
return matching sessions, each with a short snippet for context.
Usage: stackunderflow search-past-decisions [OPTIONS] QUERY
| Option | Type | Default | Description |
|---|---|---|---|
--project |
TEXT | (all) | Filter by project slug (e.g. -Users-you-dev-foo) |
--since |
TEXT | (all time) | Filter to messages newer than this (7d/1w/1m/24h/ISO) |
--limit |
INTEGER | 20 |
Max sessions to return (hard cap) |
--context-budget |
INTEGER | env or 2000 |
Token budget for the output; 0 disables |
--use-embeddings |
flag | false | Re-rank substring matches by local sentence-transformers cosine similarity (requires pip install stackunderflow[embeddings]) |
--embed-model |
TEXT | env or all-MiniLM-L6-v2 |
Override the sentence-transformers model id; ignored without --use-embeddings |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow search-past-decisions "watchfiles inotify" --limit 10
Past decisions matching 'watchfiles inotify' (1 session(s))
[claude] def456abc789… 2026-04-25T09:11:33 msgs=64 $0.8800
-Users-you-dev-app /Users/you/dev/app
… "watchfiles is Rust-backed and cross-platform; raw inotify would mean a separate macOS path…"
$ stackunderflow search-past-decisions "auth middleware" --project -Users-you-dev-api --format json
Semantic search (opt-in). Plain substring matching misses phrases that
were worded differently in the transcript. Install
pip install stackunderflow[embeddings] (adds sentence-transformers) and
pass --use-embeddings to re-rank the substring-matched candidate set
by cosine similarity against the query. The substring filter still runs
first — --use-embeddings only re-orders the set, never widens it. JSON
output gains an embedding_score in [0, 1]; text output appends
cos=X.XX to each session headline. Model defaults to
sentence-transformers/all-MiniLM-L6-v2 (90 MB, 384-dim) and is loaded
lazily on the first call; override via the STACKUNDERFLOW_EMBED_MODEL
env var or --embed-model. Per-message vectors are cached in
discovery_embeddings (added by migration v014) so repeat queries
against the same candidate set are a SELECT, not a recompute.
$ pip install 'stackunderflow[embeddings]'
$ stackunderflow search-past-decisions "watcher behind a flag" --use-embeddings
Past decisions matching 'watcher behind a flag' (2 session(s))
[claude] def456abc789… 2026-04-25T09:11:33 msgs=64 $0.8800 cos=0.87
-Users-you-dev-app /Users/you/dev/app
… "we decided to ship the watcher behind a flag for the rollout…"
[claude] aaa111bbb222… 2026-04-19T15:02:11 msgs=22 $0.1200 cos=0.51
-Users-you-dev-app /Users/you/dev/app
… "the file watcher behind the etl service occasionally drops events…"
List sessions where ACTION was performed and the next user turn confirmed it
worked (an explicit "thanks"/"that worked", or no revert and no complaint before
the session ended). ACTION is matched as a case-insensitive substring against
tool calls and message text — a tool name (Edit), a file fragment (cost.py),
or a phrase (add caching). The positive-signal counterpart to
find-failure-modes-for-file. Not budget-aware — --limit is the only cap.
Usage: stackunderflow find-sessions-where-action-worked [OPTIONS] ACTION
| Option | Type | Default | Description |
|---|---|---|---|
--project |
TEXT | (all) | Filter by project slug |
--file |
TEXT | (none) | Narrow to sessions that also touched this file |
--since |
TEXT | (all time) | Only sessions whose matching activity is newer than this (7d/1w/1m/24h/ISO) |
--limit |
INTEGER | 20 |
Max sessions to return |
--min-confidence |
FLOAT | 0.5 |
Minimum outcome confidence in [0.0, 1.0]. Explicit-phrase confirmations clear the default; lower-confidence "silence ⇒ worked" rows do not. Pass 0.0 for the legacy "anything that didn't break is a success" behaviour |
-v, --verbose |
flag | false | Append outcome_confidence to each text row (JSON always carries it) |
--format |
text|json |
text | Output format |
Each result carries the inferred outcome ("worked"), outcome_evidence (the
message that established it), outcome_msg_id, and outcome_confidence. The text
output prints a → worked: <evidence> line under each session; with -v it also
shows the confidence score.
Example:
$ stackunderflow find-sessions-where-action-worked "add caching" --file stackunderflow/infra/cache.py
Sessions where 'add caching' worked (1 session(s))
[claude] aaa111bbb222… 2026-05-08T16:40:02 msgs=92 $1.1000
-Users-you-dev-app /Users/you/dev/app
→ worked: user replied "perfect, that's exactly it" two turns later
List sessions where editing FILE led to a follow-up correction — the user
reporting it broke, the agent reverting it (git revert / git reset --hard /
git checkout --), or a complaint — each with the triggering message as evidence.
The negative-signal counterpart to find-sessions-where-action-worked. Not
budget-aware.
Usage: stackunderflow find-failure-modes-for-file [OPTIONS] FILE
| Option | Type | Default | Description |
|---|---|---|---|
--since |
TEXT | (all time) | Only sessions whose edit is newer than this (7d/1w/1m/24h/ISO) |
--limit |
INTEGER | 20 |
Max sessions to return |
--min-confidence |
FLOAT | 0.5 |
Minimum outcome confidence in [0.0, 1.0]. Explicit-phrase complaints (0.8) and agent revert tool calls (0.5) clear the default; pass 0.0 to include lower-confidence inferences |
-v, --verbose |
flag | false | Append outcome_confidence to each text row (JSON always carries it) |
--format |
text|json |
text | Output format |
Each result carries outcome ("failed" or "reverted"), outcome_evidence,
outcome_msg_id, and outcome_confidence.
Example:
$ stackunderflow find-failure-modes-for-file stackunderflow/routes/cost.py --since 90d
Failure modes for stackunderflow/routes/cost.py (1 session(s))
[claude] ccc333ddd444… 2026-04-29T11:02:55 msgs=140 $2.0100
-Users-you-dev-app /Users/you/dev/app
→ reverted: agent ran `git checkout -- stackunderflow/routes/cost.py` after the test broke
Risk summary for a file before you edit it: how many distinct past
sessions that touched it ended reverted, failed, or worked. A
read-only aggregator over the same outcome heuristic that powers
find-failure-modes-for-file — no extra schema, the counts are computed
from messages / sessions rows on each call. Also surfaced, merged with
failure modes and touching sessions, by
stackunderflow memory file.
Usage: stackunderflow risk file [OPTIONS] PATH
| Argument | Required | Description |
|---|---|---|
PATH |
yes | File path to assess — ~ and relative paths are resolved to an absolute path before matching |
| Option | Type | Default | Description |
|---|---|---|---|
--since |
TEXT | (all time) | Only sessions whose activity is newer than this (7d/1w/1m/24h/ISO) |
--format |
text|json |
text | Output format |
recent_session_ids lists up to five of the most recent failure-mode
sessions (reverted ∪ failed) for the file — read them with
session_query to see what went wrong.
Example:
$ stackunderflow risk file stackunderflow/routes/cost.py --since 90d
File risk for /Users/you/dev/app/stackunderflow/routes/cost.py
since: 90d
total sessions touching the file: 14
reverted: 2
failed: 1
worked: 9
recent failure-mode sessions:
- ccc333dd-1a2b-4c3d-...
- 91b2f0a1-9e8d-4f7c-...
JSON output is {path, since, total_sessions, reverted, failed, worked, recent_session_ids}.
Mine the local store for project-specific workflow patterns ("always run pytest
after editing", "never pkill", a flag combo this project favours, a path the
user keeps steering edits away from) and emit Claude Code SKILL.md files. These
are derived from ground truth (what actually happened across your sessions) — never
from CLAUDE.md. Pure-regex synthesis, no network. Full background in
docs/skills.md.
Guardrails (deliberate): project-scoped by default — there is no
--all-projects mode; cross-project mining is opt-in via --scope user with an
explicit --project/--projects allowlist. Generated skills land under
<project>/.claude/skills/auto-*/ (or ~/.claude/skills/ with --scope user) —
never into the package, never released. Idempotent (.bak written before any
overwrite); never clobbers a hand-authored skill.
Usage: stackunderflow skills generate [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--project |
TEXT | (cwd's project) | Project slug to mine. Default for --scope project: the project the current directory belongs to |
--projects |
TEXT | (none) | Comma-separated slugs for cross-project mining (required for --scope user when --project isn't given) |
--scope |
project|user |
project |
project → ./.claude/skills/ ; user → ~/.claude/skills/ (global; requires explicit --project/--projects) |
--min-occurrences |
INTEGER (≥1) | 5 |
A pattern must appear in this many distinct sessions |
--kind |
choice (repeatable) | (all) | Restrict to these pattern kinds: avoids-X, never-touches-paths, canonical-test-command, always-runs-X-after-Y, uses-tool-flag-combo |
--window |
TEXT | 90d |
Only consider sessions newer than this (90d/1w/ISO; all or empty for no bound) |
--out |
PATH | (per --scope) |
Output directory |
--dry-run |
flag | false | Show what would be generated; write nothing |
--format |
text|json |
text | Output format |
Examples:
$ stackunderflow skills generate
Generated 2 skill(s) under /Users/you/dev/app/.claude/skills:
[created] auto-canonical-test-command (.../auto-canonical-test-command/SKILL.md)
[updated] auto-never-touches-store-db (.../auto-never-touches-store-db/SKILL.md)
· auto-canonical-test-command: canonical-test-command, 14 sessions
· auto-never-touches-store-db: never-touches-paths, 9 sessions
$ stackunderflow skills generate --dry-run --format json
$ stackunderflow skills generate --project -Users-you-dev-foo --min-occurrences 8 --window 60d
$ stackunderflow skills generate --scope user --projects app,api,infra
Usage: stackunderflow skills list [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--scope |
project|user |
project |
Where to look: ./.claude/skills/ or ~/.claude/skills/ |
--out |
PATH | (per --scope) |
Skills directory to inspect |
--format |
text|json |
text | Output format |
Lists only the auto-generated skills (directory prefix auto-, frontmatter
auto_generated: true).
Usage: stackunderflow skills clean [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--scope |
project|user |
project |
Where to clean: ./.claude/skills/ or ~/.claude/skills/ |
--out |
PATH | (per --scope) |
Skills directory to clean |
--older-than |
TEXT | (all) | Only remove skills generated before this (30d/2w/ISO) |
--dry-run |
flag | false | Show what would be removed; delete nothing |
-y, --yes |
flag | false | Actually delete. Without this, clean only previews |
Only ever removes auto-* directories marked auto_generated: true — never a
hand-authored skill. Without --yes it prints the would-be deletions and exits.
$ stackunderflow skills clean --older-than 30d # preview
$ stackunderflow skills clean --older-than 30d --yes # delete
Proactive suggestions mined from your local session store. Both subcommands are read-only — they never write a file or change a setting; acting on a recommendation is always a separate, explicit step.
List workflow patterns you've re-run by hand often enough to be worth a
skill and don't already have one for. Reads messages plus the skills
already on disk; each row carries an accept_command you can paste to
generate that skill. The proactive counterpart to skills generate.
Usage: stackunderflow recommend skills [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--project |
TEXT | (cwd's project) | Project slug to scan. Defaults to the project the current directory belongs to |
--threshold |
INTEGER (≥1) | 5 |
A pattern must appear in this many distinct sessions |
--window-days |
INTEGER (≥1) | 30 |
Lookback window in days |
--no-cache |
flag | false | Bypass the recommendation cache and re-mine |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow recommend skills
Found 2 skill recommendation(s) for -Users-you-dev-app:
• run-pytest-after-edit [always-runs-X-after-Y] occurrences=18
You ran `pytest` right after editing a source file in 18 sessions.
accept: stackunderflow skills generate --project -Users-you-dev-app --pattern always_runs_pytest_after_edit
• avoid-pkill [avoids-X] occurrences=7
You consistently steered away from `pkill` across 7 sessions.
accept: stackunderflow skills generate --project -Users-you-dev-app --pattern avoids_pkill
The --pattern selector in each accept command is forward-compatible —
skills generate does not consume it yet, so accepting a recommendation
today means re-running skills generate for the whole project. If the
current directory can't be mapped to a project, the command exits with a
usage error; pass --project explicitly.
Pattern-match a task prompt against your own past sessions and suggest
the cheapest model whose similar sessions had the lowest cost. Heuristic
v1 — read it as a routing nudge, not a hard model picker. A confidence
of 0.00 means there isn't enough similar history to have an opinion.
Nothing leaves the machine.
Usage: stackunderflow recommend mode [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--prompt |
TEXT | (required) | The task prompt to score (quote it) |
--current-model |
TEXT | (none) | The model you'd otherwise route to; drives the cost delta |
--no-cache |
flag | false | Skip the 24 h cache and recompute from history |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow recommend mode --prompt "rename a local variable" --current-model claude-opus-4-6
Recommended model: claude-sonnet-4-6
Current model: claude-opus-4-6
Confidence: 0.82
Estimated savings: $0.1840/session
Similar sessions: 23
Why: 23 similar past tasks finished on Sonnet at a median $0.02
A citation-feedback loop on discovery: the three budget-aware discovery commands
passively record which sessions they surface (loaded_count), and a later
session-detail lookup records which surfaced sessions actually get used
(cited_count). cite_rate = cited_count / loaded_count feeds the discovery
ranking so sessions agents actually use climb and uncited noise sinks. Telemetry
is local-only (session ids + counters, no transcript content) and the passive
recording is gated behind STACKUNDERFLOW_DISCOVERY_TELEMETRY (default on; set to
0 to disable).
Show the telemetry table: loaded/cited counters + cite-rate per session, sorted most-recently-surfaced first.
Usage: stackunderflow discovery telemetry [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--command |
TEXT | (all) | Filter to one discovery command (find_sessions_in_path, find_sessions_touching_file, search_past_decisions) |
--session |
TEXT | (all) | Filter to one session id |
--limit |
INTEGER | 50 |
Max rows to show; ≤ 0 means no limit |
--format |
text|json |
text | Output format |
Flag sessions surfaced N+ times over M+ days that were never cited. Demoted sessions drop out of default discovery ranking (their cite-rate term is zeroed) but stay reachable via direct lookup.
Usage: stackunderflow discovery demote-uncited [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--dry-run |
flag | false | List candidates without flagging them |
--min-loads |
INTEGER | 20 |
Minimum times surfaced |
--min-age-days |
INTEGER | 7 |
Minimum age (days since first surfaced) |
--format |
text|json |
text | Output format |
Long-form discussion of the snapshot directory layout, the rsync
--link-dest hard-link contract, restore semantics, and the macOS
launchd auto-backup integration lives in
docs/backup.md.
Create an incremental backup of all ~/.claude/ data. Backs up sessions, file history,
plans, tasks, todos, settings, shell snapshots, and prompt history. Excludes debug logs
and plugin binaries to save space. Uses hard links for efficiency — unchanged files cost
zero additional disk space.
Usage: stackunderflow backup create [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--label |
TEXT | (none) | Optional label appended to the backup directory name |
--keep |
INTEGER (>=1) | 10 | Max backups to retain; oldest are pruned automatically |
Backups are stored in ~/.stackunderflow/backups/ as timestamped directories
(YYYYMMDD-HHMMSS[-label]).
Examples:
$ stackunderflow backup create
Backing up ~/.claude → /Users/you/.stackunderflow/backups/20260419-143209
(excluding: debug, plugins, cache, statsig...)
Done: 2884 files (1102 JSONL), 3216.6 MB
$ stackunderflow backup create --label pre-upgrade --keep 5
Backing up ~/.claude → /Users/you/.stackunderflow/backups/20260419-143209-pre-upgrade
(excluding: debug, plugins, cache, statsig...)
Done: 2884 files (1102 JSONL), 3216.6 MB
List all existing backups with their file counts and sizes.
Usage: stackunderflow backup list [OPTIONS]
No options beyond --help.
Example:
$ stackunderflow backup list
7 backup(s) in /Users/you/.stackunderflow/backups
20260409-153720-full (2743 files, 3018.2 MB)
20260410-111823-test (2804 files, 3066.6 MB)
20260414-175009-pre-upgrade (2819 files, 3094.0 MB)
20260419-143209-pre-upgrade (2884 files, 3216.6 MB)
Restore ~/.claude/ from a named backup. Prompts for confirmation before overwriting.
Usage: stackunderflow backup restore [OPTIONS] NAME
| Argument | Required | Description |
|---|---|---|
NAME |
yes | Backup directory name as shown by backup list |
| Option | Type | Default | Description |
|---|---|---|---|
--dry-run |
flag | false | Show what would be restored without making any changes |
Examples:
$ stackunderflow backup restore 20260409-153720-full --dry-run
Would restore 2743 files from /Users/you/.stackunderflow/backups/20260409-153720-full
→ /Users/you/.claude
$ stackunderflow backup restore 20260409-153720-full
This will overwrite files in /Users/you/.claude. Continue? [y/N]: y
Restoring 2743 files from ... → /Users/you/.claude
Restore complete.
Set up or remove daily automatic backups. On macOS, installs a launchd plist
(~/Library/LaunchAgents/com.stackunderflow.backup.plist) that runs at 3:00 AM.
On Linux, prints the cron line to add manually via crontab -e.
Usage: stackunderflow backup auto [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--enable / --disable |
flag | --enable |
Enable or disable daily backups |
Examples:
$ stackunderflow backup auto --enable
Daily backup enabled (3:00 AM). Keeps last 10.
Plist: /Users/you/Library/LaunchAgents/com.stackunderflow.backup.plist
$ stackunderflow backup auto --disable
Automatic backups disabled.
Two opt-in surfaces for pulling pull-request and CI data into the local
store. Neither runs unless you invoke it; tokens and secrets come from
the environment, never the database. This ingest command group is
separate from the --ingest flag on the read commands, which refreshes
session data.
Backfill a repository's pull requests and workflow runs over the GitHub REST API into the local store — a one-shot catch-up for history from before the webhook receiver was turned on.
Usage: stackunderflow ingest github [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--repo |
OWNER/REPO |
(required) | The GitHub repository slug, e.g. octocat/hello-world
|
--token |
TEXT | (from env) | GitHub PAT. Falls back to $STACKUNDERFLOW_GITHUB_TOKEN, then $GITHUB_TOKEN. Public repos work without one but hit the 60-requests/hour anonymous limit |
--state |
all|open|closed |
all |
PR state filter passed to the GitHub API |
--max-pages |
INTEGER (1–50) | 10 |
Maximum pages of 100 to fetch per endpoint (PRs and CI) |
--no-ci |
flag | false | Skip the workflow-runs fetch — a quick PR-only refresh |
--format |
text|json |
text | Output format |
Example:
$ stackunderflow ingest github --repo 0bserver07/StackUnderflow --max-pages 5
Backfill complete for 0bserver07/StackUnderflow
PRs: inserted=42 updated=3 pages=2
CI: inserted=118 updated=0 pages=5
duration: 6.30s
A request that exhausts the GitHub rate limit exits with an error.
Run a small FastAPI receiver that serves the
/api/webhooks/{github,gitlab,ci} endpoints on a dedicated port,
separate from the dashboard. Each endpoint verifies a signature and
returns 503 until its secret is set, so the receiver never accepts an
anonymous payload.
Usage: stackunderflow ingest webhook serve [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--port |
INTEGER | 8096 |
Port to bind the receiver on |
--host |
TEXT | 127.0.0.1 |
Bind address. Loopback by default; set 0.0.0.0 only when tunnelling from a public webhook URL |
The endpoints check these secrets (HMAC-SHA256 for GitHub and CI, token comparison for GitLab):
| Endpoint | Secret env var |
|---|---|
/api/webhooks/github |
STACKUNDERFLOW_GITHUB_WEBHOOK_SECRET |
/api/webhooks/gitlab |
STACKUNDERFLOW_GITLAB_WEBHOOK_SECRET |
/api/webhooks/ci |
STACKUNDERFLOW_CI_WEBHOOK_SECRET |
Example:
$ STACKUNDERFLOW_GITHUB_WEBHOOK_SECRET=... stackunderflow ingest webhook serve
configured receivers: github
Webhook receiver listening on http://127.0.0.1:8096/api/webhooks/
| Key | Type | Default | Description |
|---|---|---|---|
port |
int | 8081 |
HTTP port the dashboard server binds to |
host |
str | 127.0.0.1 |
Address the server binds to |
auto_browser |
bool | true |
Open the browser automatically on start/init |
max_date_range_days |
int | 30 |
Maximum days allowed in a dashboard date range query |
messages_initial_load |
int | 500 |
Number of messages loaded on initial dashboard view |
log_level |
str | INFO |
Python logging level (DEBUG, INFO, WARNING, ERROR) |
auto_reindex_on_ingest |
bool | true |
Re-aggregate dashboard data automatically after a background ingest cycle |
currency |
str | USD |
Display currency for cost figures — any 3-letter ISO 4217 code |
model_aliases |
dict[str,str] | {} |
Proxy-rewritten model id → canonical id (manage via cfg model-alias) |
plan_name |
str | null | null |
Active plan preset name (manage via plan set) |
plan_monthly_usd |
float | null | null |
Monthly budget in USD (manage via plan set) |
plan_reset_day |
int | 1 |
Day-of-month the budget resets (manage via plan set) |
plan_alert_thresholds |
list[int] | [50, 75, 90] |
Burn-projector alert thresholds, percent of budget (manage via plan thresholds set) |
discovery_budget_tokens |
int | 2000 |
Default --context-budget for the memory namespace and the budget-aware discovery commands |
discovery_rank_weights |
str | 0.5,0.2,0.3 |
Discovery-ranking weights recency,cost,relevance (malformed → default) |
Example — set, verify, then reset a key:
$ stackunderflow cfg set port 9000
port = 9000
$ stackunderflow cfg ls
Settings:
...
port 9000 [file]
...
$ stackunderflow cfg rm port
port removed
Every config key can be overridden by an environment variable. The variable name is the
second argument to the _Opt descriptor in stackunderflow/settings.py (shown below).
Environment variables take precedence over the config file.
| Config key | Env var | Example |
|---|---|---|
port |
PORT |
PORT=9000 stackunderflow start |
host |
HOST |
HOST=0.0.0.0 stackunderflow start |
auto_browser |
AUTO_BROWSER |
AUTO_BROWSER=false stackunderflow start |
max_date_range_days |
MAX_DATE_RANGE_DAYS |
MAX_DATE_RANGE_DAYS=90 stackunderflow start |
messages_initial_load |
MESSAGES_INITIAL_LOAD |
MESSAGES_INITIAL_LOAD=1000 stackunderflow start |
log_level |
LOG_LEVEL |
LOG_LEVEL=DEBUG stackunderflow start |
auto_reindex_on_ingest |
AUTO_REINDEX_ON_INGEST |
AUTO_REINDEX_ON_INGEST=false stackunderflow start |
currency |
STACKUNDERFLOW_CURRENCY |
STACKUNDERFLOW_CURRENCY=GBP stackunderflow start |
discovery_budget_tokens |
STACKUNDERFLOW_DISCOVERY_BUDGET_TOKENS |
STACKUNDERFLOW_DISCOVERY_BUDGET_TOKENS=4000 stackunderflow memory decisions "retry logic" |
discovery_rank_weights |
STACKUNDERFLOW_DISCOVERY_RANK_WEIGHTS |
STACKUNDERFLOW_DISCOVERY_RANK_WEIGHTS=0.6,0.1,0.3 stackunderflow find-sessions-in-path . |
Boolean env vars accept 1, true, yes, on (case-insensitive) as truthy values;
anything else is treated as false.
Resolution order (highest to lowest):
- Environment variable
- Config file (
~/.stackunderflow/config.json) - Built-in default
These aren't in ~/.stackunderflow/config.json — they're read directly at the call
site:
| Env var | Default | Effect |
|---|---|---|
STACKUNDERFLOW_DISABLE_WATCHER |
unset | Truthy = skip spawning the ETL filesystem watcher (same as start --no-watcher) |
STACKUNDERFLOW_DISABLE_LOCK |
unset | Truthy = skip the watcher single-instance lock at ~/.stackunderflow/server.lock (same as start --no-lock) |
STACKUNDERFLOW_DISCOVERY_TELEMETRY |
on | Set to 0 / false to disable the passive discovery citation-feedback recording |
STACKUNDERFLOW_BETA_<NAME> |
unset | Truthy = enable a beta-flagged provider adapter (e.g. STACKUNDERFLOW_BETA_GEMINI=1) |
STACKUNDERFLOW_EMBED_MODEL |
sentence-transformers/all-MiniLM-L6-v2 |
sentence-transformers model id for search-past-decisions --use-embeddings; the --embed-model flag overrides it |
STACKUNDERFLOW_GITHUB_TOKEN / GITHUB_TOKEN |
unset | GitHub PAT for ingest github. The --token flag overrides; STACKUNDERFLOW_GITHUB_TOKEN is consulted before GITHUB_TOKEN |
STACKUNDERFLOW_GITHUB_WEBHOOK_SECRET |
unset | HMAC-SHA256 secret for the /api/webhooks/github receiver; unset → that endpoint returns 503 |
STACKUNDERFLOW_GITLAB_WEBHOOK_SECRET |
unset | Token-comparison secret for /api/webhooks/gitlab; unset → 503 |
STACKUNDERFLOW_CI_WEBHOOK_SECRET |
unset | HMAC-SHA256 secret for /api/webhooks/ci; unset → 503 |
The discovery_rank_weights value is recency,cost,relevance (three comma-separated
floats); a malformed value or the wrong component count falls back to the default
(0.5,0.2,0.3).
Cost figures default to USD. To display them in another currency set the currency
key to any 3-letter ISO 4217 code (stackunderflow cfg set currency GBP or
STACKUNDERFLOW_CURRENCY=EUR). Validation only checks the format — runtime resolves
the rate via the public Frankfurter API (ECB FX data, no auth) and caches it for
24h at ~/.stackunderflow/cache/exchange-rate.json. Cost computation stays in USD
internally; conversion happens at the API boundary, so the cached rate-card and the
displayed numbers are independent. If the FX fetch fails and the cache is empty or
stale-and-uncached, responses fall back to USD with rate_from_usd=1.0 rather than
crash. Every API endpoint that returns dollar figures now also returns a
currency: {code, symbol, rate_from_usd} block at the top level so the frontend
can render symbols and labels without a second round-trip.
Costs are computed from per-message token usage via the
stackunderflow.infra.costs.compute_cost(tokens, model, provider, *, speed)
shim, which routes through provider-specific pricers in
stackunderflow.infra.providers/. The Anthropic pricer recognises the
priority/fast tier flag set by the Claude adapter from
message.usage.service_tier:
| Field value | Record.speed |
Pricing impact |
|---|---|---|
"priority" |
"fast" |
Opus models: 6× input + 6× output rate, cache rates unchanged. Sonnet/Haiku: 1× (unchanged). |
"standard" |
"standard" |
Standard rate card. |
"batch" |
"standard" |
Batch tier is cheaper (not faster); we conservatively price it at standard until separate batch rates are wired in. |
null / missing |
"standard" |
Pre-tier records and non-Claude adapters default here. |
Unknown model ids (which fall back to the Sonnet-3.5 rate card) are
priced at 1× even when speed="fast" so a misclassified record never
gets accidentally over-charged. Aggregator collectors group tokens by
(model, speed) so a session that mixes standard and fast records gets
each subset priced at its correct rate. The store schema does not yet
carry the speed flag — SQLite-backed stat queries
(store/queries.get_project_stats) report standard rates until a
follow-up migration lands.
Opt-in integration with Claude Code's lifecycle hooks (PostToolUse,
UserPromptSubmit, Stop, PreCompact). Nothing is installed unless you run
hooks install. Full details — what's captured, the privacy model, the
performance characteristics, the Windows status — in docs/hooks.md.
stackunderflow hooks install [--scope project|user] [--dry-run] [--capture-content]
Merges StackUnderflow's hook entries into a settings.json —
<git-root>/.claude/settings.json for --scope project (default), or
~/.claude/settings.json for --scope user. Idempotent and convergent: a
re-run lands on exactly the config the current flags describe (a stale entry or
a changed --capture-content choice is replaced, never duplicated). Writes
settings.json.bak.<utc-ts> before any mutation (not on a no-op, not under
--dry-run). Never touches another tool's hooks. --dry-run prints the
would-be hooks block and writes nothing. --capture-content makes the
installed hook commands store full payloads (prompt text, tool output) instead
of the conservative sanitised-metadata default.
stackunderflow hooks uninstall [--scope project|user]
Removes only the entries StackUnderflow recognises as its own. Never deletes the file, never touches other entries. Backs up first iff the file actually changes.
stackunderflow hooks status [--scope project|user] [--format text|json]
Shows which StackUnderflow hooks are installed in which scope (default: both), whether any are stale (recognisably ours but not in the portable form), and how many non-StackUnderflow hook entries the file carries.
stackunderflow hooks repair [--scope project|user|all] [--dry-run]
Rewrites stale StackUnderflow hook commands (e.g. a hardcoded path left over from
a moved venv) back to the portable stackunderflow hooks run <id> form,
preserving --capture-content. Changes nothing else; backs up each file first;
--dry-run reports without writing. --scope all walks $HOME for every
project's .claude/settings.json — bounded (depth ≤ 8), never follows symlinks,
prunes node_modules/.git/.npm/.cache/.nvm and other heavy trees. Only
ever runs when you invoke it.
Internal — Claude Code invokes this; reads the hook payload as JSON on stdin and
records a captured_events row when warranted. Always exits 0 (it must never
disrupt Claude Code). Not for direct use.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
A command-level error (click.ClickException) — e.g. an unknown --period passed to report or optimize |
2 |
A usage error (bad parameter / click.UsageError) — e.g. an invalid cfg set key, or a --period outside the fixed choice list on compare / yield / export |
An unknown period given to report or optimize exits 1 and prints
Error: Unknown period '<value>'. Valid: today, 7days, 30days, month, all.
The same bad period on compare, yield, or export is caught by Click's
choice validation instead and exits 2. An unknown key passed to cfg set
exits 2 with a message listing the valid keys.