pulse #
pulse is a local observability companion for Claude Code orchestrator sessions. A small FastAPI + SQLite backend keeps a log of what each session is doing — status updates, decisions, questions it needs you to answer, and subagent results — and renders it as a calm, warm web page you can check in on instead of babysitting a terminal.
Claude Code talks to pulse two ways: a stdio MCP server gives the orchestrator three
tools (note, ask, resolve) to report progress and raise questions instead of
silently guessing, and two hook scripts (SessionStart, SubagentStop) register
sessions and log subagent results automatically. Everything is local, unauthenticated,
and lives on 127.0.0.1:8787 — there's nothing to deploy or configure beyond pointing
Claude Code at it once.
Install #
With uv (recommended):
cd ~/dev/pulse
uv sync
Without uv, a plain venv works too:
cd ~/dev/pulse
python3 -m venv .venv
source .venv/bin/activate.fish
pip install -e .
Run the server #
cd ~/dev/pulse
./run.sh
This starts uvicorn on http://127.0.0.1:8787. The SQLite database lives at
~/.local/share/pulse/pulse.db and is created automatically on first run.
Wire up Claude Code #
1. Register the MCP server #
claude mcp add pulse -s user -- uv run --project ~/dev/pulse python -m pulse.mcp_shim
Use --project, not --directory: --directory changes the shim's working
directory, so os.getcwd() would report pulse's own directory for every
session and all notes would land in one bucket. --project picks up pulse's
environment while preserving the session's cwd, which is how events find
their session.
Or add directly to ~/.claude.json / your MCP config:
{
"mcpServers": {
"pulse": {
"command": "uv",
"args": ["run", "--project", "~/dev/pulse", "python", "-m", "pulse.mcp_shim"]
}
}
}
2. Add the two hooks #
In ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/dev/pulse/hooks/session_start.py"
}
]
}
],
"SubagentStop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 ~/dev/pulse/hooks/subagent_stop.py"
}
]
}
]
}
}
Both scripts read hook JSON from stdin, are best-effort (they always exit 0 and never raise into your session), and only do anything useful while the pulse server is running.
3. Add the behavioral contract #
Paste this into your global CLAUDE.md so orchestrator sessions actually use the tools:
Log decisions and meaningful progress with `note()` as you work.
Before deciding anything the human would want a say in, call `ask()` and keep working on
other things — never resolve an open question yourself.
After the human answers in chat, call `resolve()` with a short note on what they decided.
Using it #
Open http://127.0.0.1:8787 for the session list. Sessions with open questions show a
small coral dot and count; click into a session to see the question pinned at the top,
a running summary, and the collapsed activity feed below.
The summary streams in live rather than blocking the page: POST /api/sessions/{id}/summarize/stream streams the response text back chunk by chunk as
plain text, persisting the final result on success. Summaries come from Groq
(openai/gpt-oss-120b, sub-second to first token, ~0.5s for a full summary); a Groq
key is required — without one the summary area shows an error explaining what to
configure.
Configuration lives in ~/.config/pulse/config.toml (no environment variables). The
key is fetched once at server startup (retried lazily if that fails, e.g. a locked
vault) and cached in memory; the api_key_command form keeps the secret out of files
entirely:
[groq]
api_key_command = "op item get <item-uuid> --fields credential --reveal"
Page loads never wait on this — a stale or missing summary auto-starts a stream on load, and the "catch me up" button re-triggers it manually. Only one summarize can run per session at a time; a second request while one is in flight gets a 409.