Personal Project

turnbell - Voice, Desktop & Webhook Alerts for CLI Coding Agents

A local-first CLI that reads your coding agent's response aloud, fires a desktop notification, or pings your phone the moment it finishes or needs input, zero telemetry.

turnbell calls out the moment a CLI coding agent finishes a turn or stalls waiting on input. Kick off a long Claude Code, Codex, or Gemini CLI run, switch to something else, and stop babysitting the terminal: turnbell reads the response aloud, fires a desktop notification, plays a sound, or pings a webhook the instant the agent needs you: not a minute sooner, not ten minutes later.

turnbell - Voice, Desktop & Webhook Alerts for CLI Coding Agents

What I built

  • 8 first-class agent integrations: Claude Code, Codex, Gemini CLI, Copilot CLI, Cursor, opencode, Antigravity, and Amp, each adapter wired to whatever done/blocked/read-aloud signal that agent's own hook system actually exposes, plus a universal turnbell run wrapper for any other command.
  • Ledger-tracked config writes: every hook turnbell adds to an agent's settings file is recorded in an atomic, append-only ledger with a line-level diff, so turnbell uninstall can surgically remove exactly what turnbell wrote and nothing else, preserving edits made afterward.
  • Read-aloud "speakable mode": an opt-in nudge written into an agent's own memory file (CLAUDE.md, AGENTS.md, GEMINI.md) asking it to keep responses speakable and route code or dense detail to a temp file instead of speaking it. It's reversible, and auto-cleaned after 24h.
  • Background-aware done detection: Claude Code's done alert holds while sub-agents, workflows, or teammates are still reporting in-flight work, so you're pinged once at the real end of a turn instead of mid-delegation.
  • Local-first webhook sink: one serializer emits exactly six metadata fields (timestamp, agent, event, project, summary) to ntfy, Pushover, Slack, or a raw endpoint: never the working directory, the code, or the agent's response.

Why it mattered

Every long agent run used to mean either babysitting the terminal or missing the moment it stalled on a permission prompt. turnbell closes that gap without adding a background daemon, an SDK, or any telemetry: it wires each agent's own hooks, and every wire it adds can be traced and reversed.

Why This Exists

Long Claude Code and Codex runs don't fail loudly, they stall quietly on a permission prompt or finish while I'm three tabs away in something unrelated. The fix isn't a bigger terminal window, it's getting told the instant a turn actually needs me. I'm the sole contributor, actively maintained, built to solve my own workflow first and shipped as a public npm package second. It was previously named hollr; the rename to turnbell is deliberate enough that the package still ships a compatibility bin alias and an automatic config migration for anyone on the old name.

The Constraint: No SDK, No Daemon, Nothing Invasive

Every agent integration in turnbell answers to one rule: it edits the agent's own hook configuration to call turnbell emit, and nothing else. No background process, no SDK the agent has to import, no polling. That constraint is also why the uninstall story had to be solved properly instead of documented as a known limitation.

Ledger-Tracked Writes Instead of "Best Effort" Uninstall

The obvious way to build turnbell init is to append hook entries into whatever config file the agent uses and call it done. The obvious way to build turnbell uninstall on top of that is to grep for turnbell's own markers and hope nothing else in the file collides. That's what a "best effort" uninstall looks like, and it's not good enough when the file being edited is a shared config the user's own tooling depends on.

src/adapters/diffwire.ts takes a different approach: every write is expressed as a WireOp against a JSON or text file, applied through an atomic write (temp file + rename, never a partial write on crash), and appended to a ledger (listWiredKeys, appendLedgerEntry) that records exactly which keys or markers turnbell touched and what the prior value was. turnbell init previews every change as a generated line diff before writing anything. turnbell uninstall replays the ledger instead of re-deriving intent from the file's current state, which is what makes "surgically removes only what turnbell added, edits you make afterward are preserved" an actual guarantee instead of a best-effort claim in the README.

The Codex Special Case

Codex only allows a single top-level notify command in its config, no list of hooks. If a user already had a notify command wired up for something else, naively overwriting it would silently break their existing setup. The adapter archives the original notify value before replacing it, and turnbell uninstall restores it from that archive rather than deleting the key outright. The one case that can't be recovered is if config.toml itself is deleted between setup and uninstall, there's nothing left to restore into, and that's documented as a real boundary rather than papered over.

Not Pinging You Mid-Delegation

Claude Code's own done hook fires per top-level turn, but a turn can spawn sub-agents, workflows, or teammates that keep working after the parent turn reports finished. An early version of turnbell fired the alert right there, which meant getting pulled back to the terminal while background work was still running. The fix holds the done announcement until Claude Code reports no more in-flight delegated work, while explicitly ignoring long-lived shell/watcher and monitor tasks, since treating those as "in flight" would silence every alert on any session that happens to have a background process running.

The Payload Is Metadata Only, By Construction

turnbell is local-first: voice, notifications, and sound never leave the machine. The one thing that can go over the network is a webhook the user explicitly configures, and that path routes through a single serializer that emits six fields and nothing else, timestamp, agent, event, project basename, and a short summary string. There's no second code path that could accidentally leak the working directory or the agent's actual response, because the serializer is the only thing with network access in the whole codebase. turnbell test --show-payload prints the exact payload without sending it, so the claim is verifiable rather than asserted.

Stack

TypeScript, tsup for the bundle, Vitest with an 80% coverage gate, native OS primitives for voice/notifications/sound per platform (macOS stable, Linux and Windows in beta), and per-agent hook/config formats (Claude Code, Codex, Gemini CLI, Copilot CLI, Cursor, opencode, Antigravity, Amp) with no shared SDK across them. MIT licensed.