Your Slack status, synced with your local AI agents. Every session is a blip on your team's radar.
agentblip.com · npm · GitHub
Your team can't see your agents. You kick off three Claude Code sessions, go heads-down reviewing what they produce, and on Slack you look… idle. Meanwhile someone pings you mid-run, an agent sits blocked on a permission prompt for twenty minutes, and nobody — including you — knows. Presence used to mean "at the keyboard." Now half the work is delegated, and the green dot has nothing to say about it.
agentblip turns local agent sessions into your Slack status — the little green light for the AI era:
| Your status reads | When |
|---|---|
🤖 claude agent working |
one session busy |
🤖 3 agents working |
three sessions busy |
🤖 claude: finalizing CI/CD |
activity granularity — what it's actually doing |
✋ 1 agent(s) waiting on me |
an agent is blocked on you |
npm i -g agentblip
agentblip setup # walks through Slack connect + hook install
agentblip start --detach # daemon runs in the backgroundThat's it. Claude Code sessions appear automatically via hooks; Codex via its notify hook plus a session watcher. Anything else can report itself with one HTTP request (see Integrations). When every agent finishes, your status clears.
Claude Code hooks ─┐
Codex watcher ─────┤ POST /event ┌────────────────────┐ POST /api/status ┌──────────────┐
Any tool (curl) ───┼──────────────────▶ │ agentblip daemon │ ───────────────────▶│ relay Worker │──▶ Slack API
│ localhost:4519 │ SessionStore → │ pre-formatted │ (or direct │ users.profile.set
└ │ formatStatus →sink │ SlackStatus only │ Slack sink) │
└────────────────────┘ └──────────────┘
The daemon is the only thing that sees raw session data. Hooks and adapters report
events to 127.0.0.1:4519; the daemon aggregates your sessions and formats the status
text locally, applying your granularity, templates, and redaction patterns before
anything touches the network. The relay receives exactly three fields — text, emoji,
expiration — and passes them to Slack's users.profile.set. It cannot see your
prompts, tool calls, file paths, or projects, because they never leave your machine
unless you chose a granularity that puts them in the status text itself. (At
"presence" the relay sees the same fixed string all day.)
Every push carries a rolling expiration (5 minutes by default). If your laptop sleeps,
the daemon crashes, or wifi drops, Slack clears the status on its own — no stale
"working" lies. Server-side, the relay stores nothing about you beyond your Slack
user/team ids and an AES-GCM-encrypted Slack token keyed to your device, and you can
revoke that any time with agentblip unlink.
One knob controls how much your status reveals:
granularity |
Your status reads |
|---|---|
off |
never set |
presence |
🤖 heads down with agents |
count (default) |
🤖 claude agent working · 🤖 3 agents working |
activity |
🤖 claude: finalizing CI/CD · 🤖 3 agents · finalizing CI/CD |
At count and activity, agents blocked on you append · 1 waiting on me; when
nothing is working and agents are only waiting, the status flips to
✋ 1 agent(s) waiting on me.
Everything lives in ~/.config/agentblip/config.json (XDG_CONFIG_HOME respected),
created by agentblip setup. The comments below are documentation — the real file is
plain JSON:
The config block above must stay in sync with
configSchemainpackages/cli/src/lib/config.ts— that schema is the source of truth.
Status text is truncated to Slack's 100-character cap. {agent} is a friendly display
name (claude-code → claude, codex → codex, gemini-cli → gemini); unknown
sources display as-is.
Your status field isn't only agentblip's. Before every push the daemon reads your
current Slack status (via the relay in relay mode, or straight from Slack in direct
mode — that's the users.profile:read scope) and decides what it's allowed to do:
statusPolicy: "respect"(default). A status agentblip didn't set — one you typed yourself, or another app's — is never overwritten. The daemon stands down (agentblip statusshows it backing off) and resumes automatically the moment that status clears or expires.statusPolicy: "overwrite". agentblip displaces the existing status, remembers it, and puts it back when your sessions end.- A manual change mid-session always wins, under either policy. If your status changes out from under agentblip while it's active, that's you (or another app) speaking deliberately: the daemon backs off, treats the new status as the truth, and drops anything it had saved.
Honest edge cases: if the daemon dies mid-session, your status simply expires blank
after the rolling TTL (no stale "working" lies) — and a status agentblip had displaced
under "overwrite" is restored the next time the daemon starts, unless it carried its
own expiration that has since passed. If your Slack token predates the read scope,
agentblip can't see the current status and falls back to its old blind-push behavior;
re-authorize to get the new one:
agentblip unlink && agentblip setupagentblip setup installs hooks into your Claude Code settings. Seven hook events map
onto the session lifecycle:
| Claude Code hook | Event sent |
|---|---|
SessionStart |
start |
UserPromptSubmit |
working |
PreToolUse |
working (tool as activity) |
PostToolUse |
working (keeps liveness fresh) |
Notification |
waiting (permission prompt, question) |
Stop |
idle |
SessionEnd |
end |
Each hook pipes its JSON to agentblip hook claude-code, which forwards a normalized
event to the daemon. Multiple concurrent sessions just work — each is tracked by its
session id.
agentblip setup adds a notify hook to your Codex config (agentblip hook codex is
the entrypoint), which reports turn completions. Because notify alone can't see when a
turn starts, the daemon also watches Codex session logs to mark sessions working in
real time.
The daemon's localhost API is a stable public interface. Report any process as an agent session:
curl -X POST http://127.0.0.1:4519/event \
-H "authorization: Bearer $(cat ~/.local/state/agentblip/daemon.secret)" \
-H "content-type: application/json" \
-d '{
"source": "my-tool",
"sessionId": "run-42",
"kind": "working",
"activity": "backfilling embeddings",
"project": "acme-api"
}'The daemon auto-generates that bearer secret (0600, XDG_STATE_HOME
respected); every endpoint except GET /health requires it.
Send {"kind": "end"} with the same source + sessionId when it finishes. Full
schema, event semantics, staleness rules, and adapter-writing guide:
docs/INTEGRATIONS.md.
| Command | Does |
|---|---|
agentblip setup |
Interactive setup: pick a mode (relay / slack / console) + install agent hooks. Relay mode prompts for the Relay URL (default https://agentblip.com — enter your own domain to self-host); --relay-url <url> skips the prompt |
agentblip start [--detach] |
Run the daemon, foreground or in the background |
agentblip stop |
Stop the background daemon |
agentblip status [--json] |
Show daemon state, live sessions, and the current status |
agentblip emit |
Send a session event by hand (testing, scripts) |
agentblip hook <source> |
stdin→event adapter wired into agent hooks (e.g. agentblip hook claude-code) |
agentblip pause |
Pause Slack updates without stopping the daemon |
agentblip resume |
Resume Slack updates |
agentblip unlink |
Revoke this device's token and clear the status |
agentblip doctor [--json] |
Diagnose config, daemon, hooks, and connectivity |
A tiny native SwiftUI menu bar app puts the blip on your own screen — a colored dot that shows idle / N working / waiting, with a dropdown to pause, change the detail level, and flip the overwrite-vs-respect policy live. It's a thin client of the daemon (holds no logic of its own), so nothing duplicates the core.
cd apps/menubar && ./scripts/make-app.sh && open dist/agentblip.appRequires macOS 13+ and the daemon running. See apps/menubar/.
The relay is one Cloudflare Worker with one KV namespace — fork the repo, create the
namespace and two secrets, deploy, and point the CLI at your own domain with
agentblip setup --relay-url https://your.domain (or enter it at the Relay URL
prompt). The full walkthrough is in docs/SELF_HOSTING.md. If you'd rather
run no server at all, direct mode has the daemon call Slack's API straight from your
machine with your own Slack app's user token — no relay in the loop.
PRs welcome — see CONTRIBUTING.md for dev setup, the repo map, and what a good PR looks like. The one hard rule: the relay must never see anything beyond the final formatted status.
MIT © 2026 Sean Geng
{ // How much your status reveals: "off" | "presence" | "count" | "activity". "granularity": "count", // Append the project name: "claude agent working (agentblip)". // Project is usually the basename of the session's cwd. "showProject": false, // What to do when a status agentblip didn't set is already up: // "respect" (default) — never overwrite it; stand down until it clears. // "overwrite" — displace it once, remember it, restore it when sessions end. "statusPolicy": "respect", // Rolling expiration in seconds — Slack auto-clears the status if the daemon // stops refreshing it (sleep, crash, dead wifi). 0 = never expire. "statusTtlSec": 300, // Minimum ms between Slack pushes (users.profile.set allows ~50/min). "debounceMs": 10000, // Local daemon port (loopback only). "port": 4519, // Relay the daemon pushes through in relay mode. `agentblip setup` asks for // it at the "Relay URL" prompt (default https://agentblip.com), or takes it // via --relay-url. The AGENTBLIP_RELAY_URL env var overrides it at runtime. "relayUrl": "https://agentblip.com", // Strings or regexes scrubbed from activity/project text before it can reach // your status. Case-insensitive; strings that don't compile as regexes match // literally. Matches become "…". "redactPatterns": ["secret-project", "client-\\w+"], // Project names to suppress entirely — sessions in these projects still count // toward "N agents working" but never contribute activity/project text. "hideProjects": ["acme-confidential"], // Status emoji, Slack ":name:" form. "emoji": { "working": ":robot_face:", "waiting": ":raised_hand:" }, // Status text templates. Placeholders in {braces}; unknown ones are left as-is. "templates": { "presence": "heads down with agents", // fixed text for "presence" "workingOne": "{agent} agent working", // {agent} {activity} {project} "workingMany": "{working} agents working", // {working} {total} "waitingSuffix": " · {waiting} waiting on me", // {waiting} — appended while agents wait "activityOne": "{agent}: {activity}", // {agent} {activity} {project} "activityMany": "{working} agents · {activity}", // {working} {activity} "waitingOnly": "{waiting} agent(s) waiting on me" // {waiting} — nothing working, agents blocked }, // Auto-start the daemon from a hook when it isn't already running (a failed // start is cooled down for 60s so a broken config can't respawn on every hook). "autoStartDaemon": true, // Which source adapters the daemon activates. Codex also takes an optional // sessionsDir override (defaults to ~/.codex/sessions). "adapters": { "claudeCode": { "enabled": true }, "codex": { "enabled": true } } }