Morgan Stuart
← All work

Stuart LabsOpen sourceOctober 2026

echo-OS

A macOS menu bar tool that speaks a short headline when a long Claude Code turn finishes, tagged with the project folder and a per-session call sign, so I can leave several agents running and hear which one is done.

Role
Sole developer
Status
Open source (MIT)
Stack
Python 3.13, PyObjC (AppKit, WKWebView), Claude Code hooks, Claude Haiku via claude -p, MCP, launchd
Links
GitHub ↗

I run several Claude Code sessions at once, each in its own project folder, and the long turns are the ones I walk away from. Coming back meant clicking through terminal tabs to see which session had finished and what it had done. echo-OS replaces that with a voice. When any interactive session on my Mac ends a long turn, a small model writes a one- or two-line headline and the Mac reads it aloud, prefixed by the project folder and the session's call sign:

"billing svc, Alpha. The billing refactor is done and all 42 tests pass."

Every session gets a NATO call sign for as long as it runs, counted per folder, so two projects can each have an Alpha and a second session in the same folder is its Bravo. The project name and call sign are filled in by code. No model is trusted with naming.

Three echo popup cards for different projects arriving one by one in the top corner of the screen, then clearing
Popup cards, rendered from the repo's demo data. Each project folder gets its own card color.

I built it over four days in October 2026 (20 commits) and published it under the MIT license.

How it works

Claude Code hooks for session start, prompt submit, stop and session end all run one shell script. It posts the event to a daemon on 127.0.0.1 with a one-second curl timeout and returns at once. A hook must never slow down or break a session, so if the daemon is down the echo is missed and nothing else happens.

On a stop event, the daemon reads the finished turn out of the session transcript and checks a length gate: by default at least 3 minutes and at least 5 tool calls. Duration is what "I walked away" means, and the tool floor screens out long answers that were mostly thinking. A turn below the gate gets a small silent "done" chip with no model call. A turn that passes goes to a single worker, which runs two agents in sequence and hands the result to one speech queue. Echoes, replays and anything posted to the local /api/speak endpoint play one at a time, so voices never overlap.

Diagram: hooks post to a local daemon, which checks the length gate; one worker runs the KB and headline agents, then a single speech queue speaks the headline and shows a popup
Hooks to daemon to worker to speech queue. Short turns never reach a model.

The two agents are headless claude -p calls on Haiku that run through the user's own Claude Code login. The first, a KB relevance agent, decides whether the user's knowledge base has anything worth adding and fetches it if so. The second writes the headline as structured output: the spoken line, plus any questions or suggested next steps from the session's final reply. Those items are shown as bullets on a popup card and never spoken. The voice just says "two questions for you on screen", and cards with questions stay up until you reply to that session or close them. Clicking a card jumps to that session's tab in Terminal or iTerm2.

Making each call cheap

The first thing I measured was the cost of a background model call. A default headless claude -p call carries Claude Code's full context, about 163k tokens or roughly $0.16 per call. With a custom system prompt, no built-in tools, a strict MCP config, hooks disabled, no session persistence and no CLAUDE.md files, the same call is about 640 tokens, about $0.001 and about 3 seconds. Disabling hooks also stops echo-OS's own calls from triggering echoes of themselves.

On the live service, the time from a stop event to speech was 12 to 16 seconds with a knowledge base lookup, most of it model round trips. I considered one persistent Claude session fed every turn and rejected it. Its context grows with every echo, summaries from different sessions could bleed together, and it needs reset logic, all to save about one of those seconds in CLI startup.

Failing open

The knowledge base step is optional and must never cost an echo. During one outage, every lookup waited 30 seconds and answered "not relevant" while the panel's KB light showed green. The server was connecting fine, but its tool listing timed out, so the agent ran with no KB tools and concluded, reasonably, that nothing was relevant. I changed the runner to check which tools actually loaded. Missing KB tools now count as a failure instead of "not relevant", the light turns red, and echoes skip the lookup until the server is healthy again. With the lookup skipped, the next echo arrived in 7 seconds.

Knowledge lookups go through a one-method KnowledgeLookup protocol, so the default MCP agent can be swapped for a notes folder or a database. Implementations return an error result instead of raising, which keeps a KB outage from costing an echo. The model and the voice each sit behind a small contract of their own (HeadlessModel, and VoiceOutput with macOS say by default and OpenAI text-to-speech as an option) so either provider can be swapped. Their failures are caught and logged. A failed headline drops that echo, and a failed voice still leaves the popup card on screen.

Open-sourcing it

The repo is public, so nothing about my own setup can ship in it. The knowledge base is bring-your-own. You list any MCP server (http, sse or stdio) and the read-only tools the agent may call in a local, gitignored kb.json. Without that file the KB agent is off and echoes work the same. Before publishing I audited every commit, not only the latest tree, and rewrote history to remove personal details. I then added a publication check that runs before every push and scans for secrets and for a private list of strings that is itself gitignored.

The project was built test-first. The suite has 274 tests, all passing, and none of them call a model or speak. A setup.sh script and one ./echoctl install command take a new user from clone to first echo.

How it relates to ego-OS

The names are close, but these are separate projects. ego-OS is my personal context engine, a knowledge base served to agents over MCP. echo-OS is a much smaller tool that listens to Claude Code sessions and talks.

They connect through one config file. On my machine, echo-OS's KB relevance agent points at ego-OS's MCP server, so a headline can pull in a fact from my notes about the project it is reporting on. That wiring lives only in my gitignored kb.json.

Next

AgenticNodes →