herald-cc
v0.3.1
Published
Voice-controlled permission proxy for Claude Code sessions
Maintainers
Readme
Herald tells you - out loud - when a Claude Code session is waiting for permission, and you answer by voice, click, or a single key.
You're running five Claude sessions in tmux. Every few minutes one of them stops dead: "Can I run git push?" Today that means finding the right window, reading the prompt, pressing a key, finding your way back. Herald deletes that loop: it watches every session, announces the request ("frontend wants to push local commits to origin. Allow?"), and you say "yes". Or click the popup. Or press y. Your hands never leave what you were actually doing.
Why "herald"?
A herald was the officer a medieval court kept between the throne and everybody shouting at it. Someone at the gate? The herald sounds the trumpet 📯 and announces them. The crown decides. The herald carries the answer back. At no point does the monarch walk down to the gate.
In this metaphor you are the monarch (congratulations). Every blocked Claude session is a petitioner at the gate. Herald announces who is asking and what they want, you issue the decree - "yes push", "denied", "tell me more" - and herald delivers it. You never leave the throne, which is whatever tmux window you were already in.
Also, permission-request-proxy-daemon doesn't come with a trumpet.
What you get
- 🗣️ Voice both ways - announcements via macOS speech, answers via local whisper.cpp (offline, private) or any OpenAI-compatible API. "Yes", "deny", "why?", "details", "skip", "approve all reads", "approve frontend" - and words you add yourself.
- 🖱️ Native popups - one clickable dialog per waiting session; one click to allow, two for destructive. Dialogs vanish on their own the moment a request is answered anywhere else.
- 🌐 Web dashboard - live sessions and queue, every setting clickable with plain-language explanations, changes apply to the running daemon instantly. Localhost-only, token-gated.
- 🎛️ One-word modes -
herald mode popup,voice,both,silent,keyboard. Your tweaks survive switching. - 🪝 Two detection channels - a native Claude Code PermissionRequest hook (structured, race-free) plus a tmux screen watcher (zero-config, catches sessions already running). One queue, no duplicates.
- 🛡️ Safety that doesn't trust luck - nothing is approved without an explicit human decision, destructive actions demand their own word ("yes force push"), a misheard echo can't approve anything, and every decision lands in a secrets-redacted audit log.
- ⚡ Frictionless on purpose - barge in while herald is still talking, batch-approve the boring stuff, sticky focus so a late arrival can never steal your "yes".
Get started
# 1. install herald
npm install -g herald-cc
# 2. run claude inside tmux
tmux new -s frontend # new tmux session, named something you can say out loud
cd ~/code/frontend # your project
claude --continue
# 3. start herald in any OTHER terminal
herald startNo tmux yet? brew install tmux on macOS, or the official install guide for anything else - and there's a five-line primer in New to tmux? below.
That's it. Herald finds every Claude session running in tmux, and the next permission prompt gets spoken - "frontend wants to push local commits to origin. Allow?" - with a clickable dialog alongside. Say "yes" or "no", click, or press y / n. No config needed.
Optional extras: herald install-hook once (new sessions hand requests to herald directly, the terminal dialog never even appears), herald tmux-setup once (mouse scrolling + deep scrollback, without touching your own config), herald mode popup if you'd rather click than talk, herald start -d for background. From source instead of npm: git clone https://github.com/Osama-Yusuf/herald && cd herald && npm link.
How it works
Two detection channels feed one queue:
- Hook (after
herald install-hook): Claude Code hands herald the request as structured JSON and waits. Answer in time and the terminal dialog never appears; after 45s it falls back to the normal dialog. Harmless when herald isn't running. - tmux watcher: reads each claude pane every 2s, parses the on-screen dialog, answers with keystrokes. It re-checks the screen right before pressing anything, so it never answers a dialog you already handled. Covers sessions that were running before herald started.
Destructive prompts are announced first, reads last; duplicates across channels are merged.
Talking to it
After an announcement, say (or type after pressing t):
- "yes" / "allow" / "okay" / "go ahead" - approve. Risky requests need the word: "yes push", "yes delete".
- "no" / "deny" / "reject" - deny
- "always allow" / "never allow" - decide AND save a rule so this pattern stops asking
- "why?" - the session explains itself and rates the risk; herald reads it back
- "details" - the full command text (long ones get a time estimate first)
- "skip", "stop that task", "approve all reads", "approve frontend", "what's pending?", "say that again", "stop reading"
Talk naturally - "yes yes", "yeah go ahead" all land - and add your own words in the dashboard's Commands tab ("commands": { "approve": ["ship it"] }); they extend the built-ins, deny wins conflicts, risky still needs its word. Keyboard: same actions on y n w d s x a l r, t to type, q to quit.
When several sessions are waiting
A bare "yes" applies only to the request herald last announced; a request arriving mid-answer gets a one-line mention ("Also, docs is waiting") and waits its turn, so it can never steal your yes. Or address any session directly: "approve web", "why for docs", "yes push for web". Spoken numbers and unique prefixes both work ("web two" finds web2, "front" finds frontend).
Why tmux, and not just any terminal?
Herald needs three things no terminal emulator gives it, and tmux hands over for free:
- Eyes -
tmux capture-panereturns any pane's exact screen text from the outside, without touching your focus. Terminal.app, iTerm, and Ghostty have no portable equivalent. - Hands -
tmux send-keyspresses the answer inside exactly the right pane, silently. The GUI alternative is focusing windows and faking global keystrokes: not a permission proxy, a poltergeist. - Names - tmux sessions have short names you can say ("approve frontend"); tab titles change under you.
Long-running sessions want tmux anyway (they survive terminal quits; reattach over SSH). Not in tmux? Still covered - the hook channel works in any terminal after herald install-hook; tmux just adds the zero-config channel for sessions that were already running.
New to tmux?
tmux keeps terminal sessions alive outside any window: start something, detach, close the terminal, reattach later with everything still running - each session under a name you chose. That's the whole trick, and herald builds on it. The five commands you need:
brew install tmux
tmux new -s api # new session named "api", you're now inside it
claude # start claude in there like normal
# ctrl+b then d # detach (claude keeps running)
tmux ls # list sessions
tmux attach -t api # jump back inName sessions like you'd say them: short and pronounceable ("api", "web", "docs"). Symbol soup like "my-app=v2" is hard to say and harder to hear.
Stock tmux also has two newcomer traps: the scroll wheel doesn't scroll and scrollback is tiny. One command fixes both:
herald tmux-setup # mouse scrolling + click-to-switch + 50k history, applied liveIt never touches your settings - the config lands in ~/.herald/tmux.conf and your ~/.tmux.conf gains a single source-file line (delete it to undo).
Voice input
Output (announcements) works out of the box via say. Input needs whisper, and there's a script for it:
./scripts/setup-voice.sh
herald test-voiceIt checks before every step and only installs what's missing (whisper-cpp and sox via brew, plus the model into ~/.herald/models/), so it's safe to re-run. At the end it verifies the whole pipeline by synthesizing speech with say and transcribing it back, no microphone needed. Want a more accurate model than the fast default? --model base.en or --model small.en.
Two notes:
- the first real recording triggers macOS's microphone permission prompt for your terminal app. That's what the
herald test-voicerun after setup is for. - prefer doing it by hand? It's just
brew install whisper-cpp soxplus downloadingggml-tiny.en.binfrom huggingface.co/ggerganov/whisper.cpp into~/.herald/models/.
No whisper? Herald runs with voice out, keyboard in. Still kills the window switching.
Voices
A macOS truth herald had to be built around: say -v silently falls back to Samantha for any voice it doesn't list - even "Ava (Premium)" - and the premium voices you download are reachable only as the System Voice (no voice flag at all). So herald treats "system" as a real voice choice, defaults "auto" to it, and herald voices render-verifies every candidate so everything listed is real - no more twelve names that all sound like Samantha.
herald voices # honest list; * marks the active one, 🃏 the joke voices
herald voices preview system # hear any entry before committing
herald voices use Karen # switch (applies live); "system" and "auto" work tooTo sound great: dashboard Voice tab → "get voices" → download "Ava (Premium)", set it as the System Voice, leave herald on auto.
Making it snappy:
- the mic is open only while something is pending; Space is push-to-talk any time
- a Tink sound means herald heard you and is transcribing
- the "Fast" whisper model answers quicker; a model you haven't downloaded never mutes the mic - herald falls back to an installed one and says so
- no local whisper?
stt.engine: "openai"hits any OpenAI-compatible endpoint (Groq's whisper-large-v3 is very fast);"command"runs your own tool, and the same escape hatch exists for speech out - speech rate, decision feedback (Smart / Speak / Sound / Banner - a silent "✅ Approved" notification - / Off), and the end-of-speech pause are all sliders and switches in the dashboard, explained in place
- "say that again" (or
r) repeats; "details" reads a speech-friendly version, "read it verbatim" gets exact characters
And a misheard "no" can never become "go": herald's own voice is echo-filtered out of the mic, and destructive actions always need their specific word.
Answering before herald finishes
Already know the answer mid-sentence?
- clicks and keys work any time and cut the speech off instantly
- barge-in is on by default: a spoken "yes" interrupts too. Herald's own words are echo-filtered and can never approve anything; if open speakers make the mic noisy,
stt.barge_in: false - "stop reading" shuts herald up without deciding
If you don't answer
Nothing is lost or auto-decided while you're away: requests never expire, herald speaks one "still waiting" reminder (30s default, 0 = off), hook requests fall back to the normal terminal dialog after 45s so no session is ever stuck on you, and popups that time out leave the request answerable everywhere else. The dashboard's Alerts tab spells out this lifecycle next to the knobs that control it.
Popup mode
herald mode popup (or ui.popup: true): every pending request gets its own native macOS dialog - [More...] [Deny] [Allow] - one per waiting session, in parallel. "More..." opens Why / Details / Skip / Always allow / Never allow / Stop task.
- one click approves routine requests; destructive ones take exactly one more, on a button naming the action ("Confirm force push") with Cancel as the default (prefer typing the word?
ui.destructive_confirm: "typed") - answer a request anywhere else - terminal, voice, keys - and its popup vanishes immediately
- dialogs time out quietly; the request stays pending
Modes
How herald talks to you is one decision, so it's one command:
herald mode # list modes, * marks the active one
herald mode popup # dialogs + arrival sound, no speech, no mic- voice - speak requests out loud, answer by voice, no popups
- popup - clickable dialogs with an arrival sound, no speech, no mic
- both - voice and popups together, answer whichever is closer (the default)
- silent - popups and desktop notifications only, no sounds at all
- keyboard - terminal keys only
A mode only flips the channel switches - your voice, speed, and rule tweaks survive - and applies live, no restart.
Dashboard
herald dashboardopens the local web UI: mode cards, live sessions and queue, and every option across tabs with plain-language explanations. Every change saves itself and applies to the running daemon instantly - no save button.
It's localhost-only (127.0.0.1:4776) and token-gated (~/.herald/dashboard.token, chmod 600); herald dashboard opens the browser pre-authorized, so other websites can't touch it. Works standalone with the daemon down; ui.dashboard: false turns it off entirely.
Config
~/.herald/config.json is a fully commented template: every option listed with its default and a one-line description, commented out. Uncomment what you want to change; everything left commented keeps tracking herald's defaults as they improve. Comments and trailing commas are valid in this file.
Five ways in, pick your favorite:
herald dashboard- the web UI, every option clickable, applies liveherald mode <name>- one-word switch between voice/popup/both/silent/keyboardherald config set tts.rate 170- one option from the CLI (getandunsettoo); applies live when the daemon is runningherald setup- interactive: a few questions, writes the file for you (backup kept)herald config edit- open it in $EDITOR;herald configprints the effective merged result
On rules: both auto lists ship empty - every allow and deny is one-shot until you opt patterns in via "always allow" / "never allow" (saved and applied immediately) or the config. Patterns are exact token prefixes (Bash:git status matches git status -sb, not git statuses), and destructive commands never auto-approve, listed or not.
Honest note: these lists are convenience, not a security boundary. For things you always allow, Claude Code's own permission rules are the better home - they stop the prompt from existing at all. herald install-hook --guard additionally enforces your deny-list as a PreToolUse hook, which holds even for sessions running with bypassed permissions.
Audit
Every decision lands in ~/.herald/audit.log (chmod 600): timestamp, session, tool, command, decision, who decided (voice/keyboard/auto-rule). Command text is scrubbed for tokens and passwords, and Write/Edit file contents are never logged. herald log -f to watch it live.
Running a whole swarm? Meet fleet
Herald grew up alongside fleet, a sibling project for when a couple of tmux sessions becomes an operation: fleet spawns and tracks many Claude Code workers (it calls them bees), coordinates them through a queen, and gives you dashboards and inter-session messaging on top. Herald does not depend on it - it watches any Claude session, fleet-managed or not - but they were built to compose: fleet raises the workers, herald answers their permission prompts, and when both are running herald can lean on fleet's session discovery and context tools for richer "what is this session doing?" answers. If you got here because five hand-managed tmux windows are turning into a zoo, fleet is the next tool to look at.
Limitations
- macOS for voice and notifications. The watching and hook machinery is portable, Linux TTS/STT hookup is a config away (
tts.engine,stt.record_cmd). - Sessions outside tmux are reachable through the hook channel only.
- The terminal dialog parser is tested against claude v2.1.232 and tolerates older renders, but a future redesign could break it. That's exactly why the hook channel exists; parsing is the fallback, not the foundation.
Development
npm test # node --test, no deps