New
Apps 515Makers 515This week 515
Promote your app−30%
Agentic Voice screenshot 1
0views in
the last 30 days

Agentic Voice

Local voice output for coding agents, backed by a local TTS bridge.

Built with Claude Code

by maxanatsko · added Oct 29, 2026
no ratings0 comments
Open app Code

Local voice output for coding agents, backed by a local TTS bridge. The default backend (bridge/) runs Kokoro-82M — an open TTS model by hexgrad, unrelated to NVIDIA — via FluidAudio's Apple Neural Engine runtime, entirely on-device.

The MVP exposes a single MCP tool, speak, and a small CLI, both in one Swift command, agentic-voice. Both paths share the same application service, so agent-triggered speech and deterministic lifecycle hooks do not duplicate TTS or playback logic.

Goals

  • Let an agent speak short questions, blockers, and completion summaries.
  • Keep detailed output in the normal text UI; voice is a concise attention layer.
  • Run TTS locally or on another machine reachable over HTTP.
  • Keep the implementation small enough to understand and modify without framework overhead.

Non-goals

  • Speech-to-text or full duplex conversation.
  • An embedded LLM summarizer. The calling agent should summarize before invoking speak.
  • Queues, persistence, web UI, telemetry, or multi-provider abstraction in the MVP.

Architecture

Agent / lifecycle hook
        |
        +-- MCP speak tool
        |        |
        +-- CLI -+
                 |
            VoiceService
           /            \
     TTS client        Audio player
          |                 |
 POST /v1/audio/synthesize  afplay/custom command

The HTTP endpoint above is served locally by the bundled bridge/ by default; any HTTP service implementing the same contract (see "TTS HTTP contract" below) works.

Responsibilities are intentionally narrow. The command is in bridge/Sources/AgenticVoiceCLI/, everything it runs in the AgenticVoiceKit library (bridge/Sources/AgenticVoiceKit/), which the macOS app shares:

  • MCPServer.swift (CLI) — MCP boundary and startup/shutdown wiring, on the official MCP Swift SDK.
  • Speak.swift and InstallClients.swift (CLI) — command-line boundaries only.
  • VoiceService.swift — use-case orchestration and speech-length policy.
  • TTSClient.swift — TTS HTTP client only.
  • BridgeProcess.swift — readiness and ownership of the separate bridge process.
  • AudioPlayer.swift — local playback and the speaking lock only.
  • Config.swift — configuration (environment, .env, settings file) only.
  • Clients/ — writing agentic-voice entries into client config files only.
  • SpeechEvents.swift — the speech started/ended notifications the app's speaking overlay listens for only.

Requirements

  • macOS 26+ on Apple Silicon with Xcode 26+ / Swift 6.2+ to build.
  • A built bundled bridge, or an externally managed TTS backend. MCP automatically starts the bundled bridge at http://127.0.0.1:9000 when needed.
  • A local WAV player: afplay, unless overridden.

The TTS backend can run on a separate machine reachable over HTTP — set TTS_URL to that host.

Configure

cp .env.example .env

Create .env in the plugin/repository root. MCP and CLI resolve it from the location of the agentic-voice executable (<root>/bridge/.build/release/agentic-voice), independently of the calling project's working directory. Existing environment variables take precedence over .env.

Voice and speed can also come from the shared settings file ~/Library/Application Support/AgenticVoice/config.json ({"voice": "af_bella", "speed": 1.1}), which the Agentic Voice macOS app writes. Precedence: environment variables, then .env, then the settings file, then defaults. The MCP server re-reads voice and speed on every speak call, so changes apply without a restart; TTS_URL is fixed when MCP starts. A .env copied from an older .env.example sets TTS_VOICE and VOICE_SPEED, which hide the settings file; remove those lines to use it.

Environment variables:

Variable Default Purpose
TTS_URL http://127.0.0.1:9000 Base URL of the TTS backend
TTS_VOICE am_michael Voice id — see bridge/README.md for the full roster
TTS_LANGUAGE en-US Locale sent to TTS
VOICE_SPEED 1.0 Playback speed multiplier sent to TTS
VOICE_MAX_CHARS 1200 Hard limit for spoken text. Longer text is synthesized in chunks of up to 320 characters, each prepared while the one before it plays
VOICE_PLAYBACK_COMMAND auto Optional playback executable

Install and run

swift build -c release --package-path bridge   # agentic-voice, TTSBridge and the app
swift test --package-path bridge

Run the MCP server over stdio:

bridge/.build/release/agentic-voice mcp

MCP reuses a healthy bundled bridge or launches the already-built bridge/.build/release/TTSBridge as a separate process. No separate terminal is needed. Startup never installs dependencies or compiles code. The bridge's first model initialization can download missing FluidAudio assets; later starts reuse its cache. MCP initialization and tool listing remain available during initialization, and speak waits for readiness (up to five minutes). If the bridge fails to become ready, MCP stays connected and each speak call returns the startup error; restart MCP after fixing the cause. Bridge output and actionable startup failures go to stderr; stdout contains only MCP protocol messages.

Automatic bridge management applies only to HTTP loopback URLs (127.0.0.1, localhost, or ::1) on port 9000 without a base path, query, credentials, or fragment. These aliases use the bundled bridge's IPv4 listener. Remote URLs and other custom endpoints are externally managed: MCP connects through the existing synthesis contract and never starts a local bridge for them.

When MCP closes or receives SIGINT/SIGTERM, it stops only the child it started. Reused bridges survive that MCP instance's shutdown. With multiple instances, a borrowed bridge can disappear when its owner exits; restart the borrowing MCP instance if needed. There is no ownership transfer.

MCP instances coordinate startup by exclusively binding 127.0.0.1:19000 before spawning the bridge. This reserved coordination port prevents concurrent model downloads/loading while port 9000 is still closed. The owner holds it until its child exits; other instances wait for readiness and reuse the bridge. The OS releases the coordination socket if its owner exits, with no persistent lock files. Keep port 19000 available; an unrelated listener there causes startup to time out with diagnostics.

Only one session speaks at a time. Whichever agentic-voice process is speaking (MCP, CLI, or hook) holds an exclusive bind on 127.0.0.1:19001 from synthesis through playback; the OS releases it if that process exits. A request that arrives while the speaker is held is not queued: the speak tool returns a non-error "another session is speaking" result so the agent can skip or retry later, and the CLI prints the same message to stderr and exits with code 75. Keep port 19001 available.

If the executable is missing, build it as above. If startup times out or model initialization fails, run bridge/.build/release/TTSBridge to inspect diagnostics and finish first-use initialization, then restart MCP. If another service occupies port 9000, free the port or configure TTS_URL for your backend.

The CLI and lifecycle hooks do not start the bridge. To try the CLI without an active MCP server, start a backend explicitly:

bridge/.build/release/TTSBridge

Try the CLI:

bridge/.build/release/agentic-voice speak "Review complete. Two blocking issues need your attention."

--kind <question|completion|alert|custom> before the text tells the speaking overlay which orb to show:

bridge/.build/release/agentic-voice speak --kind question "Should I also update the docs?"

Or pipe text from a hook:

printf '%s' "The task finished successfully." | bridge/.build/release/agentic-voice speak

macOS app

Agentic Voice.app is a menu bar app (no Dock icon) that keeps the voice engine running at 127.0.0.1:9000, so MCP servers, the CLI and hooks always find a bridge to use, and none of them owns it. It bundles the agentic-voice command, TTSBridge and the hooks, so it needs nothing else installed.

Download Agentic-Voice-<version>.dmg from the latest release (macOS 26+, Apple Silicon) and drag the app to Applications. It is ad-hoc signed and not notarized, so macOS blocks the first launch: open it once, then choose Open Anyway in System Settings → Privacy & Security, or run xattr -dr com.apple.quarantine "/Applications/Agentic Voice.app".

To publish a release, bump the version in bridge/AgenticVoice-Info.plist (CFBundleShortVersionString), package.json, .claude-plugin/ and .codex-plugin/, then push a matching tag: git tag v0.3.0 && git push origin v0.3.0. The release workflow tests, builds the app with scripts/build-app.sh and attaches the DMG to the release; it fails if the tag doesn't match the app's version.

Or build it from source on an Apple Silicon Mac running macOS 26+ with Xcode 26+:

scripts/build-app.sh   # → build/Agentic Voice.app (ad-hoc signed, not notarized)

Move it to /Applications and open it. On first run it downloads the model and the default voice, showing progress in the menu. It follows the same rules as MCP's bridge management: if a healthy bridge is already running it uses that one and takes over when it goes away, and it takes the 127.0.0.1:19000 startup lock so only one process loads the model. The menu offers Settings, Welcome, Show Speaking Overlay, Launch at Login and Quit.

Until the shared settings file exists, the app opens a short onboarding window at launch: it shows the engine preparing, then lets you pick or clone a voice, install agentic-voice into your clients and turn on Launch at Login. Done saves your voice to the settings file, so onboarding doesn't open again; Welcome in the menu reopens it. Quitting stops the engine; MCP servers that were using it can't speak until they restart.

Settings has four tabs:

  • General: the default voice and speed, saved to the shared settings file and applied on every agent's next spoken message. Also shows the voice engine's status.
  • Voices: clone your own voice, and install, preview or delete any of the 28 English voices, or download the five Voxtral voices (Mistral AI, about 2.5 GB, CC BY-NC 4.0: personal use only; see Voxtral voices). Preview speaks through the same VoiceService agents use, so it takes the same speaking lock and never talks over them. To clone, record yourself reading a short script (the app asks for microphone access) or choose an existing recording, then name the voice; it becomes a clone_<name> voice you can select like any other. See voice cloning.
  • Clients: install, update or uninstall agentic-voice in each client with the same installer as agentic-voice clients (see below), using your login shell's PATH so it can find claude. Client entries and hooks point at the bundled agentic-voice command, so they show as outdated if you move the app (or if they were written by an older, Node-based version); Update fixes them.
  • Overlay: show or hide the speaking overlay's orb and text, and choose its style, corner and display.

Speaking overlay

While an agent speaks, the app shows a thinking orb (from thinking-orbs, MIT, ported in visual-explainer) and the spoken text. Turn either off, or the whole overlay, in Settings → Overlay or from the menu. The orb follows the speak tool's kind: a flowing band for completion, a waveform for question, scrambling bands for alert, and a calm ring otherwise, including the lifecycle hooks' phrases. The text shows the project the agent runs in and up to five lines of what it says.

Two styles:

  • Companion (default): the orb floats on its own in a corner of the screen, bottom right unless you choose another, and what it says rises above it in speech bubbles; an agent's previous two lines stay faded above the current one while it keeps talking. Drag it anywhere; it appears there next time on that display. Double-click puts it back in the corner, × hides it until the next message.
  • Island: a cream island grows down from the top centre of the display to hold the orb and text, then slides back when the voice stops, on any display; on one with a camera housing it grows out of the notch. Click it to hide it.

When your accent color is Graphite, the orb is drawn in warm coral instead of grey.

Whichever process speaks (MCP, CLI or hook) posts a distributed notification, com.maxanatsko.agentic-voice.speech, when playback starts and when it ends; its object is a JSON SpeechEvent (see SpeechEvents.swift). Nothing is posted while another session holds the speaker. Posting never waits or fails: without the app running, speech is unaffected and nothing is shown.

Install into clients

agentic-voice clients <install|uninstall> <client...> adds or removes the agentic-voice MCP server, and the input-needed hook where the client supports one, in each client's user-level config. The Agentic Voice macOS app runs the same command. Entries use the absolute path of the agentic-voice executable that ran the installer and of this checkout's hooks, so rerun install after moving it; status reports such entries as not current. Install and uninstall need the client names, so nothing is written to a client you did not pick; agentic-voice clients status [client...] reports every client when none is named. Output is JSON, one object per client.

Client MCP entry Hook
claude-code claude mcp add-json -s user Notification in ~/.claude/settings.json
claude-desktop ~/Library/Application Support/Claude/claude_desktop_config.json —
codex managed block in ~/.codex/config.toml PermissionRequest in ~/.codex/hooks.json (trust it with /hooks)
cursor ~/.cursor/mcp.json —
vscode ~/Library/Application Support/Code/User/mcp.json —
windsurf ~/.codeium/windsurf/mcp_config.json —
opencode ~/.config/opencode/opencode.json plugin shim in ~/.config/opencode/plugins/

Other entries are kept. The first change to an existing file saves the original next to it as <file>.agentic-voice-backup. A file that isn't plain JSON (for example, one with comments) is left unchanged, and the error shows the entry to add by hand. Edited JSON files are written back pretty-printed with their keys sorted; their contents are otherwise unchanged. Restart a client after installing. Don't combine this with the agentic-voice plugin in the same client, or it will speak twice.

TTS bridge (bridge/)

The bundled backend is a small Swift package (Vapor HTTP server + FluidAudio's Kokoro-ANE CoreML runtime) that implements the exact HTTP contract this repo's client speaks — see bridge/README.md for voice roster, the voice-conversion script, and model cache details.

Build and run it:

swift build -c release --package-path bridge --product TTSBridge
bridge/.build/release/TTSBridge

MCP tool

The server exposes one tool:

speak({ text, kind? })

If another session is already speaking, speak returns Not spoken: another agentic-voice session is speaking… (not an error), and the server's instructions tell the agent to usually skip rather than retry.

kind is optional: question, completion, alert, or custom. It does not change synthesis; the app's speaking overlay picks its orb from it.

The server sends MCP instructions telling the agent when to speak: a one- or two-sentence summary when it finishes a turn of work, the question itself when it needs input (in addition to showing it as text), and blockers. Spoken text is written for listening, never code, paths, or logs. See instructions in MCPServer.swift.

Lifecycle hooks

The speak MCP tool above already covers rich, context-aware notifications — the agent decides when to call it and summarizes for itself. The hooks below are a different, deterministic thing: a fixed-phrase backstop for the moment an LLM can't speak for itself — the tool is blocked waiting on you. Where the speak tool is registered (Claude Code and Codex CLI), turn endings are spoken by the agent as a summary, so there is no fixed "finished" hook. They intentionally do not parse a transcript or try to be clever; that would duplicate what speak already does properly, and a shell hook has no LLM in the loop to summarize with.

Fixed phrase templates, one shared script (hooks/speak-notify.sh). The script prefixes each phrase with the calling project's directory name (basename "$PWD", the hook's own cwd) so overlapping sessions across projects are distinguishable — it's still fully deterministic, just reading a directory name, not summarizing anything:

Event Phrase
Turn/task finished (opencode session.idle only) "<project> finished."
Waiting on you (Claude Code Notification, Codex CLI PermissionRequest, opencode permission.ask) "<project> needs your input."

For example, running in this repo: "agentic-voice finished." / "agentic-voice needs your input."

Claude Code and Codex CLI both consume the same declarative JSON hook schema, so hooks/hooks.json works for either unchanged — each engine reads the keys it recognizes and ignores the rest. Its command fields use ${CLAUDE_PLUGIN_ROOT}, a variable both engines substitute at execution time when a hook is loaded via the plugin system below — no hardcoded path, portable to wherever the repo is cloned. opencode's plugin model is different — real JS event callbacks, not declarative config — so opencode/plugin.mjs maps the same two events to the same shared script directly.

Install as a plugin (recommended — nothing happens automatically until you run these)

Claude Code and Codex CLI — this repo is itself a valid plugin for both (.claude-plugin/ + .codex-plugin/ + .agents/plugins/marketplace.json), installable straight from a local clone, no publishing required (note the trailing slash — claude plugin marketplace add . without it is rejected as an invalid source format):

# Claude Code — installs at user scope (fires in every project)
claude plugin marketplace add ./
claude plugin install agentic-voice@agentic-voice -y

# Codex CLI
codex plugin marketplace add ./
codex plugin add agentic-voice@agentic-voice

Check it loaded cleanly: claude plugin details agentic-voice / codex plugin list --json. To remove: claude plugin uninstall agentic-voice && claude plugin marketplace remove agentic-voice, codex plugin remove agentic-voice@agentic-voice && codex plugin marketplace remove agentic-voice.

Both the

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#voice · #dictation · #agents