voiceos-tmux-mcp
Enables voice control of local Claude Code sessions running in tmux, allowing users to list, read, send messages to, and manage sessions via nine MCP tools.
README
voiceos-tmux-mcp
A VoiceOS integration for steering hand-started Claude Code sessions by voice.
Claude Code sessions started in a terminal register themselves under
~/.claude/sessions/ and record the tmux pane they are running in. This server reads that
registry, matches each entry against the panes tmux reports as alive, and exposes the result
as nine MCP tools. That makes it possible to ask aloud what is running, what a session said,
what it is waiting on, and to answer it — without leaving whatever you were doing.
It runs as a remote-mcp integration whose URL is on this Mac: http://127.0.0.1:7392/mcp.
"Remote" is about the transport, not the location — VoiceOS makes an HTTP call to a server
you start here, rather than spawning it. That is forced by the app: installation goes by URL,
not by folder. The listener is bound to 127.0.0.1, so only processes on this Mac can reach
it. Nothing is sent anywhere, and only local tmux sockets are ever addressed.
The server must be running or the integration is dead — see Start the server below.
Tools
| Tool | What it does |
|---|---|
smoke_test_tmux |
Reports whether the integration can see tmux at all — the "is this alive" check when something breaks after an OS or app update. |
list_claude_sessions |
Lists the running Claude Code sessions with the folder each is working in. |
check_claude_session_status |
Reports whether a session is working, idle, or waiting, and what it is waiting on. |
read_claude_session |
Reads back a session's recent turns from its transcript, for summarizing aloud. |
send_to_claude_session |
Types an instruction into a session and submits it, then confirms the session actually moved. |
read_claude_prompt |
Reads the approval prompt a waiting session is showing, with its numbered options and a fingerprint. |
answer_claude_prompt |
Answers that prompt by option number, refusing if the prompt changed since it was read out. |
launch_claude_session |
Starts a new session in a given folder, optionally with a first instruction. |
interrupt_claude_session |
Stops what a session is doing, without closing it. |
Safety model
Every write target resolves through the session registry. No tool takes a pane, socket,
or window argument. A tool is given a session name, which is matched against
~/.claude/sessions/ entries that declare a tmux pane, have that pane alive right now,
and whose recorded pid is in the foreground process group of that pane's terminal. Panes
with no Claude Code registry entry — a bare shell, an editor, a tail -f — are not in the
set the lookup searches, so they are unreachable by construction rather than by a filter that
could be bypassed. This matters because text sent to a bare shell is executed.
The foreground-process-group condition is the kernel's own answer to "who receives a keystroke typed into this pane", and it is what closes three holes that a liveness check alone leaves open:
- Two entries claiming one pane. A Claude Code started from inside another inherits
$TMUXand registers the same pane under its own pid. Under liveness alone both were steerable, and sending to the nested one typed into the other session's input box. The nested process does not own the pane's terminal, so it no longer resolves. - A tmux server restart. Pane ids are monotonic within one server and never recycled,
but a restart resets the counter to
%0and reallocates the whole id space — and the pty device numbers come back identical too. A registry entry written before a reboot therefore names a live, unrelated pane on the same/dev/ttysNNN. Its pid is dead, so it is refused. - A session that has handed over its terminal to a pager, an editor or a shell. Text typed there lands in the pager, not the agent. Such a session is reported as running but not accepting input, and is never a write target.
It fails closed everywhere: an unreadable tty, a failed ps, or a pid that cannot be
determined all mean the session does not resolve. A session that silently disappears is an
annoyance; a wrong target is a stray keystroke into somebody's terminal. One race remains and
is not closable from here: the foreground group can change between the check and the
keystroke. The window is milliseconds, not eliminated.
Three further guards:
- Empty-input check.
send_to_claude_sessionrefuses to type when the session already has text in its input box, rather than merging into a half-written line. - Prompt fingerprint.
read_claude_prompthashes the entire prompt block.answer_claude_promptre-reads the screen at the moment of answering and refuses if the hash differs — so an approval spoken for one command cannot land on a different one that appeared meanwhile. - Explicit sockets. Every tmux invocation passes
-S <socket>. The server never relies on the ambient default socket.
There is deliberately no general "run a terminal command" tool: ls and rm -rf ~ would be
the same call differing only by a model-written string.
Start the server
./start.sh # leave it running; Ctrl-C stops it
It listens on http://127.0.0.1:7392/mcp. Port 7392 is fixed in server-http.mjs;
7391 belongs to the environment probe on this Mac. Confirm it is up and loopback-only:
curl http://127.0.0.1:7392/health # {"ok":true,...}
lsof -nP -iTCP:7392 -sTCP:LISTEN # must say 127.0.0.1:7392, never *:7392
start.sh resolves node absolutely (/opt/homebrew/bin/node, then /usr/local/bin/node,
then PATH) and installs dependencies on first run if node_modules/ is absent, so a
stripped environment fails loudly instead of silently. It execs node, so Ctrl-C reaches
the server rather than a shell wrapper.
Starting it at login instead
com.seanchiu.voiceos-tmux-mcp.plist.sample is a launchd login agent that brings the server
up at login and restarts it if it dies. It is a sample — not installed and not loaded.
Install commands are in the comments at the top of the file. Two details there are
load-bearing: the job runs through zsh -lc because launchd hands a job a minimal PATH
(/usr/bin:/bin:/usr/sbin:/sbin), not the login shell's; and it execs node so KeepAlive
watches the server instead of a zsh parent.
Install into VoiceOS
With the server running, install by URL and give VoiceOS:
http://127.0.0.1:7392/mcp
The install-by-URL option lives under Settings → Agent Mode → Integrations. (The exact menu
wording is not recorded here — the env-probe integration was installed this way at
http://127.0.0.1:7391/mcp on this Mac and answered, but the label was never written down.)
VoiceOS handshakes with the endpoint and discovers the nine tools from the server itself.
voiceos.integration.json declares the same URL under runtime, and its tool list is what
verify.mjs checks the server against.
The stdio path is still here
server.mjs + run.sh still work and still speak stdio, for a local-mcp runtime:
"runtime": { "kind": "local-mcp", "command": "/bin/zsh", "args": ["run.sh"] }
Both entrypoints call buildServer() from lib/tools.mjs, so the nine tools are defined
once and the two transports cannot drift apart.
Verify
node verify.mjs
Prints the live sessions the state layer can see, then checks that the tools declared in
voiceos.integration.json and those registered in lib/tools.mjs are the same set — which
covers both entrypoints, since both build from that one file. Exit code 1 means drift — the
failure that installs cleanly and then exposes nothing. Run it before installing, and after
any change to the tool list.
verify.mjs is read-only: it issues only list-sessions / list-panes and reads two files.
It never sends keys, launches, or interrupts, so it is safe against live sessions.
Tests
npm test
The glob must stay quoted — node --test test/ (a bare directory) fails on Node 26.
Tests that need a real tmux server create their own on a scratch socket under /tmp and kill
only that one; they never touch the user's socket.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.