contextspin
v0.7.1
Published
Replace Claude Code spinner/statusline text with live org context (meetings, Slack, CI, incidents, PRs) aggregated from your existing MCP servers, CLIs, and HTTP endpoints.
Maintainers
Readme
ContextSpin
Live context in your Claude Code status bar — weather, the top Hacker News story, PRs awaiting your review, CI failures, incidents, meetings — pulled from tools you already run. One-line install, and the bar is never empty.
curl -fsSL https://raw.githubusercontent.com/mannutech/contextspin/main/install.sh | bashRequires Node.js ≥ 18. MIT licensed. The only runtime dependency is commander.
It does NOT fetch data
ContextSpin is a renderer, not a data layer — no API clients, no auth flows, no integrations of its own. It aggregates from sources you already have:
- MCP servers registered in
~/.claude.json/.mcp.json(stdio only) - CLI tools already installed and authed (
gh,kubectl,glab, your scripts…) - HTTP endpoints you can already reach
It formats whatever they return into one-line snippets and shows the most relevant one. If a tool you have can't reach the data, ContextSpin can't show it — by design.
How it works (daemonless)
There is no background process by default. The statusline render is the engine — it serves the cached snippet instantly, and only when a source is past its cooldown does it spawn a detached one-shot refresh (lock-guarded so frequent renders never overlap). Nothing runs when you're not in Claude Code, so idle cost is zero.
Claude Code draws the status bar
│
▼
RENDER (~/.contextspin/statusline.mjs)
1. read the cache, print one snippet NOW (stale is fine) ──► status bar
2. if a source is due and no refresh is in flight:
│
▼ (detached, non-blocking)
REFRESH (one-shot, src/refresh-entry.js)
• run each DUE source, format, merge/dedup/prioritize, record lastRun
• write ~/.contextspin-cache.json (atomic)This is stale-while-revalidate: the bar is always fast, freshness catches up in the background. A legacy always-on daemon is still available behind injection.daemonless: false — only worth it for stdio MCP sources, where a persistent connection beats per-render handshakes.
Install
curl -fsSL https://raw.githubusercontent.com/mannutech/contextspin/main/install.sh | bashThis wires a SessionStart hook into ~/.claude/settings.json (so it self-heals each session), seeds a no-credentials starter pack (weather, a dad joke, the top HN story), and wires your status bar non-destructively (any existing status line is preserved and composed above ours). Restart Claude Code to see it.
npx contextspin install does the same. npx contextspin uninstall removes everything. npx contextspin status shows the current snippets.
Sources
Every source returns a list of records. Each record is optionally filtered, then rendered with format using {{ field }} templating — dotted/bracketed paths work ({{ results[0].value }}), {{ env.NAME }} reads an environment variable, unknown fields render empty.
mcp — call a tool on a stdio MCP server discovered from your Claude config (JSON-RPC over stdin/stdout, no SDK):
{ "type": "mcp", "tool": "slack_search_public", "args": { "query": "mentions:me is:unread" },
"format": "Slack: {{ text }}", "label": "Slack", "cooldown": 300, "maxSnippets": 2 }tool may be bare or mcp__<server>__<tool>; server is optional (otherwise the first stdio server exposing the tool is used).
cli — run a shell command (output parsed as a JSON array/object/primitive, or split into lines):
{ "type": "cli", "command": "gh pr list --review-requested @me --json title,number --limit 3",
"format": "PR #{{ number }} needs review: {{ title }}", "label": "GitHub", "cooldown": 120, "maxSnippets": 3 }http — fetch a JSON or text endpoint:
{ "type": "http", "url": "https://grafana.example.com/api/.../query?q=incidents",
"headers": { "Authorization": "Bearer {{ env.GRAFANA_TOKEN }}" },
"jq": ".results[0].value", "format": "Grafana: {{ value }}", "label": "Grafana", "cooldown": 30 }url and headers are interpolated (use {{ env.X }} for secrets, never hard-code them). jq supports a minimal subset: identity, dotted keys, bracket indexing, iteration (.[]), and pipes.
Configuration
One JSON file, ~/.contextspin.json (override with CONTEXTSPIN_CONFIG); cache at ~/.contextspin-cache.json (override with CONTEXTSPIN_CACHE).
{
"sources": [
{ "type": "cli", "command": "gh pr list --json title --limit 3", "format": "PR: {{ title }}" }
],
"injection": { "mode": "statusline", "refresh": 30, "maxVisible": 5 },
"snippets": { "deduplication": true, "cooldownAfterShown": 3, "priorityOrder": ["incident", "ci", "github"] }
}| Field | Default | Meaning |
|-------|---------|---------|
| sources[].type | — | mcp | cli | http (required) |
| sources[].tool / command / url | — | Required for mcp / cli / http respectively |
| sources[].format | — | One-line {{ field }} template (required) |
| sources[].filter | — | Keep a record only if it passes (see below) |
| sources[].label | derived | Snippet source label (mcp→tool, cli→first token, http→host) |
| sources[].cooldown | 300 | Min seconds between polls of this source |
| sources[].maxSnippets | 2 | Max snippets kept per poll |
| injection.refresh | 30 | Status-bar refresh interval, seconds |
| injection.maxVisible | 5 | Cap on snippets held in the cache |
| injection.style | true | Styled box (cyan bars + italic); false for plain text |
| injection.daemonless | true | Self-refreshing render; false for the legacy daemon |
| injection.mode | statusline | statusline | patcher | both |
| snippets.deduplication | true | Drop duplicate-text snippets when merging |
| snippets.cooldownAfterShown | 3 | A snippet stops showing after this many displays |
| snippets.priorityOrder | [] | Source labels sorted first (case-insensitive); rest last |
Filters are a single safe comparison (no eval): the expression is interpolated, then parsed as LEFT OP RIGHT where OP is ==, !=, >=, <=, >, <, or includes. No &&/||.
{ "filter": "{{ status }} == failure" }Cache
{
"updatedAt": "2026-06-17T09:00:00.000Z",
"snippets": [
{ "text": "CI failing: build on main", "source": "CI", "sourceId": 2,
"fetchedAt": "2026-06-17T09:00:00.000Z", "shownCount": 0 }
],
"meta": { "lastRun": { "2": 1781860451773 } }
}shownCount rises each time a snippet is shown; past cooldownAfterShown it's retired. meta.lastRun maps sourceId → last poll (ms) so the refresh honors per-source cooldowns across runs. When the cache is empty or every snippet is retired, the render rotates through built-in defaults (jokes + tips) — so the bar is never blank.
CLI
| Command | What it does |
|---------|--------------|
| install | Wire the self-healing SessionStart hook, create config, wire the statusline (what the curl script runs). |
| uninstall | Remove the hook, restore your prior statusline in every scope, stop any daemon. |
| status | Show the engine and cached snippets. |
| refresh | Force a one-shot refresh of all due sources now. |
| setup [--yes] | Create ~/.contextspin.json (interactive, or detected with --yes). |
| ensure | Idempotent create-config + wire-statusline (run by the hook each session). |
| inject / uninject [--mode <m>] | Install / reverse just the injector. |
| start / stop / restart | Manage the legacy daemon (only when injection.daemonless: false). |
Statusline injection
Uses Claude Code's official status line feature, so it survives updates. The wrapper is non-destructive and scope-aware: any status line you already had is composed above the ContextSpin line, and in a project (CLAUDE_PROJECT_DIR set) it writes the gitignored <project>/.claude/settings.local.json so a repo's own status line can't shadow it. Reverse with uninject (this scope) or uninstall (everything).
There's also an experimental patcher mode (injection.mode: "patcher") that rewrites Claude Code's hard-coded spinner words in the binary — inspired by claude-depester. It's length-preserving and best-effort, but every Claude Code update overwrites it, so the statusline is the supported path. Restore with uninject --mode patcher.
Limitations
- MCP is stdio-only — discovered from
~/.claude.json/.mcp.json; HTTP/SSE MCP transports aren't supported (use acli/httpsource instead). - OAuth claude.ai connectors aren't reachable — their tokens live in the OS keychain, out of reach of a standalone process. Use the matching CLI (
gh, …), an HTTP endpoint, or a local stdio MCP server.
Also available as a plugin
A Claude Code plugin wraps this package, via the mannutech marketplace — for those who prefer installing that way. The curl line above needs neither.
License
MIT. See LICENSE.
