talk-to-claude-code
This server lets you drive local Claude Code sessions from any MCP client or voice assistant: you can list, inspect, message, poll, rename, and read the history of sessions, and have tools relay the work back for hands-free use.
List sessions – see which Claude Code sessions are available, with optional detailed info like cwd, status, and ids.
Check session status – get the current state of one session by name, id, or pid.
Read transcripts – view recent conversation turns, including work done from other clients.
Send messages – queue a prompt into a running session as if typed, returning a cursor for follow-up.
Poll for replies – repeatedly fetch new output since a cursor to follow long-running tasks and know when the session finishes.
Ask and wait – send a prompt and wait for the answer in one call for short questions.
Rename sessions – change a session's displayed name.
Voice/Shortcuts support – plain-text HTTP endpoints let Siri Shortcuts ask questions and read replies aloud.
Observation mode – can be restricted to read-only, or limited to specific sessions, for safe remote monitoring.
Enables OpenAI's ChatGPT to drive local Claude Code sessions via a tunnel or HTTP endpoint, allowing it to queue prompts, monitor progress, and retrieve results.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@talk-to-claude-code Ask my Claude Code session to run the tests and list any failures"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
talk-to-claude-code
Drive your Claude Code sessions by voice, hands-free, from any MCP client.
Ask Grok — or Claude, or your own script — to give a task to a Claude Code session running on your machine, hear what it is doing while it works, and get the answer read back. It was built to work from a car: Grok's voice mode on CarPlay reaches custom MCP connectors, so the whole loop runs without touching a keyboard.
It works by injecting messages into the target session's prompt queue as if they had been typed, through the cross-session messaging inbox the CLI already uses to talk to its peers. Because the prompt lands in the normal queue, a Remote Control client — your phone, or claude.ai/code — can watch and drive the same session at the same time. There is no proxy, no patched binary, and no interception of the Remote Control connection.
you (voice) ──> Grok ──> MCP ──> talk-to-claude-code ──> Claude Code session
▲ │
└────── "Running Read… 4 steps so far" ─────────┘The hard part is not the transport, it is keeping a remote model in the loop for the five to ten minutes a real coding task takes. Most of the design exists for that: short polls that always return a cursor, an elapsed counter so no two polls look alike, progress notifications, and result text written to tell the client what to do next. See Driving a session.
This lets whatever you connect it to run code on your machine. It queues prompts into
Claude Code sessions, and those sessions edit files and run shell commands. Anything that can
reach this server — including a model that has been prompt-injected — inherits that reach.
Do not expose it to the internet without a secret, and prefer narrowing it to one session with
CLAUDE_REMOTE_MCP_ALLOW, or CLAUDE_REMOTE_MCP_READONLY=1 when you only want observation.
Unofficial, and not affiliated with or endorsed by Anthropic. It talks to aninternal,
undocumented interface of the Claude Code CLI, reconstructed by inspection (see
Protocol notes). It works against the versions listed under Requirements and
may break on any update. scripts/probe.mjs is there to tell you quickly when it does.
Why this channel
Claude Code has three separate remote surfaces. Only one of them is a good foundation:
Channel | Transport | Redirectable? |
Device bridge |
| Yes, via |
Remote Control |
| No — |
Cross-session inbox | Unix domain socket, NDJSON | n/a — it is local by construction |
Redirecting the device bridge only buys you device_bash, and routes your OAuth token through
a proxy. Redirecting Remote Control needs a binary patch or TLS interception, and breaks on
every CLI update.
The cross-session inbox is orthogonal to Remote Control: the phone talks to the session over the bridge, this server talks to it over the socket, and both stay in sync because both end up in the same transcript.
Related MCP server: claudecode-mcp
Requirements
Node 22+
Claude Code 2.1.229 or newer to send to a session. The cross-session inbox arrived in that version; older sessions register themselves but never bind a socket. Restarting one picks it up, and resuming keeps the conversation:
cd <cwd> && claude --resume <sessionId>.
Reading has no such requirement — read_transcript works on any session, whatever its version,
because transcripts are files on disk.
Install
npm install
npm run buildRegister it with Claude Code:
claude mcp add talk-to-claude-code -- node /path/to/talk-to-claude-code/dist/index.jsOr in an MCP client config:
{
"mcpServers": {
"talk-to-claude-code": {
"command": "node",
"args": ["/path/to/talk-to-claude-code/dist/index.js"]
}
}
}Connecting Grok (the voice path)
Grok is the client this was built around, because it is the one that closes the loop end to end: its voice mode reaches custom MCP connectors, it ships on CarPlay, and it polls patiently instead of giving up after one call.
Grok calls your server from its own infrastructure, so it needs a public HTTPS URL. Run HTTP mode behind a tunnel of your choice — Tailscale Funnel gives a stable hostname without a domain, which matters because a connector has to be reconfigured every time the URL changes:
bash scripts/serve-tailscale.sh # READONLY=1 to expose observation only
bash scripts/serve-tailscale.sh --stopThat generates a secret on first run and reuses it afterwards, starts the server, puts it behind Funnel and prints the connector settings. The hostname is stable, so the connector is configured once and later restarts stay invisible to it — worth the setup, because reconfiguring a connector after every restart gets old fast.
Funnel only accepts ports 443, 8443 and 10000; the script defaults to 8443 and leaves any
existing mapping on 443 alone. Override with FUNNEL_PORT= and PORT=.
Without Tailscale, scripts/serve-public.sh does the same through a Cloudflare quick tunnel. It
needs no account, but the URL changes on every restart:
bash scripts/serve-public.shThen add it in the app: grok.com/connectors → New Connector → Custom. Give it the URL with
the secret in the path, and pick no authentication:
https://<your-host>:8443/mcp/<token>That matters. On the bare /mcp URL the client's first request arrives with no credentials, the
server answers 401, and clients read that as "this server wants OAuth" — Grok then asks for a
client ID, authorize and token endpoints, none of which exist here. Putting the secret in the URL
means no request is ever unauthenticated, so the OAuth prompt never appears. A token= query
parameter works too.
Over the API you can instead send a proper Authorization header:
tools: [{
"type": "mcp",
"server_label": "talk-to-claude-code",
"server_url": "https://<your-host>/mcp",
"authorization": "<your token>",
"allowed_tools": ["list_sessions", "read_transcript"] // optional: narrow what Grok may do
}]allowed_tools is a useful second lock: it restricts Grok to a subset regardless of what the
server exposes, and composes with CLAUDE_REMOTE_MCP_ALLOW on this side.
Other clients
Any MCP client works — the server is not Grok-specific. Two things decide whether a given one is usable for the voice case, and both are worth checking before you invest time:
Does its voice mode reach custom MCP connectors? Several assistants expose connectors in text chat but not in voice, which makes them useless in a car no matter how the server is set up. Test in voice early.
Does it keep polling? A single request cannot outlive the client's deadline, so the client has to call
get_replyrepeatedly. One that stops after the firststate=workingwill never see an answer to anything that takes more than a minute.
If your client fails the first test, the /voice endpoints
below sidestep MCP entirely.
Before you expose it
This server queues prompts into local Claude Code sessions, and those sessions edit files and run shell commands. Whatever you connect — and anything that successfully prompt-injects it — inherits that reach.
A sensible posture for anything internet-facing:
CLAUDE_REMOTE_MCP_TOKEN="$(openssl rand -hex 24)" \
CLAUDE_REMOTE_MCP_ALLOW=my-scratch-session \
node dist/index.js --httpThat pins it to one throwaway session. Use CLAUDE_REMOTE_MCP_READONLY=1 when you only want the
client to observe, and prefer a tunnel that gives you a private hostname over one that publishes
a guessable URL.
Transports
Mode | Command | Use |
stdio (default) |
| Claude Code, Claude Desktop, any local MCP client |
Streamable HTTP |
| Grok and other hosted clients, behind a tunnel |
HTTP mode binds 127.0.0.1:8787/mcp by default and refuses to start without
CLAUDE_REMOTE_MCP_TOKEN unless CLAUDE_REMOTE_MCP_NO_AUTH=1 is set. Override with --host,
--port, --path or CLAUDE_REMOTE_MCP_HOST / _PORT / _PATH.
Tools
Tool | Purpose |
| Names the sessions you can drive, in one line. |
| State of one session |
| Recent turns, including work done from a phone |
| Queue a prompt, return a cursor immediately |
| Poll for output since a cursor; says whether the session finished |
| Send and wait in one call, for short questions |
| Set periodic updates on/off, interval, and confirm-before-action |
| Shortcut for the update interval alone (5–55s) |
| Change a session's display name |
Sessions are addressed by the name you gave them, the registry name, the session id (or a unique
prefix), or the pid. Results use the name you gave: the registry only carries one derived from the
working directory — tachify-33 for a session you call Tachyo — which is no help when you are
naming a session out loud. The real title lives in the transcript and is read from there.
Results are written to be read aloud: short by default, with a detailed flag when the user
actually wants identifiers and paths. A voice client speaks the entire tool result, so a verbose
listing costs the user half a minute of talking to say nothing they can act on.
Driving a session
A remote client cannot sit on one long blocking call — tunnels and tool-call budgets cut it off, and the user sees nothing meanwhile. So work is followed in short hops:
send_message -> [session=my-session state=sent cursor="2026-08-13T07:55:29.595Z"]
get_reply -> [session=my-session state=working ... cursor="…31.919Z"] # tool activity so far
get_reply -> [session=my-session state=finished ... cursor="…35.729Z"] # answerEvery result leads with a machine-readable state line, so the decision to poll
again never depends on reading prose. state=working means call get_reply
again with the returned cursor; state=finished means stop.
A third state matters more than it looks: state=needs_answer. A session that
ends its turn by asking the user something goes idle exactly like one that
finished the job, so without this it reads as done and the pending decision is
never surfaced — the user hears a report, answers out loud, and the answer goes
nowhere. On needs_answer the client must put the question to the user and send
their reply back with send_message. Detection is by wording, since nothing in
the transcript marks a question: scripts/test-asks.mjs pins the awkward cases,
including requests that end in a full stop rather than a question mark. Assistant turns
list the tools the session used, which is the progress signal during a long
task.
When the session asks a question
A session can stop and put a form up — AskUserQuestion — and it then sits idle-looking while it
waits. Results carry the question and its options verbatim, so a relayed client can read them out
instead of reporting that nothing is happening.
The answer, though, has to be given in the session itself — the Claude app over Remote Control, or the terminal. Two measurements say why:
An ordinary message queues behind the form. On a session parked on a question, it had still not been seen 93s later, with the form untouched.
A message sent with
interrupt: truejumps the queue and dismisses the prompt — but the session records it as declined, not answered, and says so: a relayed claim is not the user's decision.
This is not a gap to close. Everything through the messaging inbox is stamped as a peer by the
receiving CLI, using the connecting process's kernel-verified credentials (SO_PEERCRED) — not a
field the sender controls. There is no inbox action to submit a form answer, and no way to make a
message count as the user. The only inputs that authenticate as the user are the session's own
terminal and Remote Control (the phone). Answering a question is therefore done there; the
connector cannot, by construction.
plain_questions (default on) is what makes questions answerable by voice at all. It asks the
session, via a fenced relay note, to never use AskUserQuestion and never hold a pending
question — instead to present the question and its options as plain-text output, the way it
would present a result, and end its turn. Because there is no pending prompt, the user's spoken
reply arrives as an ordinary instruction and the session acts on it — verified end to end: a
session laid out two options, the relayed "take the second one" was accepted and executed, with
no peer-refusal. It does not decide for the user, and it does not block. interrupt still drops
a question outright when the user would rather not engage with it.
Pacing and confirmation
How a driven session keeps the user in the loop is set per conversation, ideally by asking at the start (the server instructions prompt the client to onboard):
Periodic updates. When on, a still-working
get_replytells the client to keep polling on its own and give one short update every interval, until the session finishes — no nudging. When off, it reports once and stops until asked.Interval. How often those updates come —
set_delay(seconds, 5–55) orset_preferences(interval_seconds:). A ceiling: a call returns as soon as the session settles.Confirm before acting (
confirm_before_action, or per-callconfirm). When on, before a new request the session restates what it understood and lays out its plan as output — a result to hear, not a form — then ends its turn. The user's "go" or changes arrive as an ordinary instruction it then acts on.
set_preferences({periodic_updates, interval_seconds, confirm_before_action})
records all three; they stick for the conversation. The client is asked to
collect them up front with three short questions.
Voice endpoints (Siri Shortcuts)
If your assistant cannot reach MCP connectors from its voice mode — several cannot — this route skips MCP altogether. Siri runs hands-free in CarPlay without any special entitlement, and a Shortcut can call a URL and speak the reply. These endpoints exist for that: plain text in, plain text out, short enough to be read aloud.
GET|POST /voice/ask?session=<name>&message=<text>&wait=20
GET /voice/reply?session=<name>&wait=15Auth is the same shared secret, accepted as a Bearer header, any path segment,
or a token= query parameter — Shortcuts cannot set headers easily, so the
query form is usually simplest.
/voice/reply remembers where it left off per session, so a Shortcut just keeps
asking "what's new" without tracking a cursor:
Shortcut "Ask Claude"
1. Dictate Text -> spoken prompt
2. Get contents of …/voice/ask?session=my-session&token=…&message=[Dictated Text]
3. Speak Text -> the reply, or "Working on it. Running Read."
4. Repeat 8 times:
Get contents of …/voice/reply?session=my-session&token=…
Speak Text
If it contains "Still working" -> continue, else StopRun this behind Tailscale rather than a public tunnel if you can: the phone joins the tailnet, the endpoint stays off the public internet, and it still works on cellular.
Live progress
ask and get_reply emit notifications/progress while they wait, one per
turn the session produces, each carrying a short spoken-style line (Running Read, Checking the config…). A client that surfaces them — a voice client in
particular — narrates the work as it happens instead of going quiet.
Two constraints shape this:
MCP clients abort a request after 60s (
DEFAULT_REQUEST_TIMEOUT_MSEC), andresetTimeoutOnProgressdefaults to false, so progress notifications do not extend that deadline unless the client opts in. Every call here therefore stays under 45s and returns a cursor; progress complements short calls rather than replacing them.MCP has no way to push into a conversation between calls. Continuous narration comes from the client looping on
get_reply, which is why the instructions insist on it.
Relay discipline
The server is a conduit between the user and another agent, so a client that paraphrases in either direction corrupts the channel. The expectation is stated in three places, deliberately:
the server
instructionssent at initialize,the description of the
messageparameter onsend_messageandask— the highest-leverage spot, since it sits where the client writes the value,a footer on every completed answer, where the client is about to decide how to report back.
These are instructions, not enforcement: a model can still ignore them. Client-side settings (your client's own system prompt or custom instructions) are a stronger lever and compose with these.
Two details worth knowing if you change this logic:
Finished means idle across consecutive polls with no new output. It does not require output in the current call — a turn whose text was already consumed is equally finished, and requiring output made polling never terminate.
A session stays
idlefor a moment after a prompt is queued, before it picks it up. A poll issued in that window would otherwise report "finished" before any work started, so a 12s startup grace applies while no output has appeared yet. An older cursor is past the window and settles immediately.
Permissions
Reads are always allowed. Writes are conservative by default and configurable by environment:
Variable | Effect |
| Disable every write tool |
| Only these sessions accept writes |
| These sessions never accept writes |
| Permit writing to the session hosting this server |
Writing to the host session is refused by default: the message would feed straight back into the conversation that sent it.
The recipient may also gate an incoming message itself — a session in prompting mode holds peer
messages for the user's approval. That verdict comes back as a peer_message_status receipt
(held / denied / expired / delivered) and is surfaced in the tool result.
Protocol notes
Reconstructed from the CLI binary (v2.1.229); this is an internal interface and may drift.
Discovery — ~/.claude/sessions/<pid>.json:
{ "pid": 12345, "sessionId": "bf577127-1c0a-4a1e-9c2f-0d6b7e5a8f31", "cwd": "/home/you/project",
"status": "idle", "peerProtocol": 1,
"messagingSocketPath": "/run/user/1000/cc-socks/12345.sock",
"bridgeSessionId": "session_01ABCDEF…" }A record outlives its process after an abrupt kill, so liveness is confirmed by checking the pid
and matching procStart against /proc/<pid>/stat field 22.
Auth — the token lives at ~/.claude/sessions/<pid>.<sha256 of the canonical socket path>.key
(mode 0600), containing {"peerToken":"<32 hex>","procStart":"…"}. On Linux auth is optional, but
sending it is what earns the peer role rather than being treated as an anonymous writer.
Frames — newline-delimited JSON, 1 MiB per line:
{"type":"auth","token":"<peerToken>"}
{"type":"user","message":{"role":"user","content":"hello"},"msg_id":"…","from":"uds:/path.sock"}
{"type":"control","action":"rename","name":"new-name"}
{"type":"control","action":"peer_message_status","status":"held","orig_msg_id":"…"}Receipts — a recipient only replies to an address in the same directory as its own socket
that ends in .sock, so the receipt listener binds inside the shared cc-socks directory.
Replies — read from the session's transcript at
~/.claude/projects/<cwd-slug>/<sessionId>.jsonl.
Development
node scripts/probe.mjs # discovery + auth-key derivation, sends nothing
node scripts/e2e.mjs <session> # drives a session over a real MCP stdio client
node scripts/e2e-http.mjs # Streamable HTTP: auth rejection, bearer, secret path
node scripts/e2e-poll.mjs <session> [prompt] # send_message + get_reply loopNote when spawning a test session from inside another Claude Code session: unset
CLAUDE_CODE_CHILD_SESSION, or the child is treated as nested, session persistence is disabled,
and it never registers or binds a socket.
Available Tools
7 toolsaskAsk a session and wait for its replyA
WHEN TO USE THIS: any question or instruction about the user's own projects, code, repositories, machine, builds, deployments or work in progress belongs to a session — relay it. You do not know what is on their disk, and neither does a web search. Do not answer from your own knowledge, do not search the web, and do not ask clarifying questions the session could answer better: pass it through and let the session look. Answer in your own voice only when the user asks about you, or about how this connector itself works.
Send a prompt and wait for the answer in one call. Best for short questions. If the session is still working when the wait runs out, this returns the output so far plus a cursor — continue with get_reply rather than calling ask again, which would send the prompt a second time.
Inputs: message (required), session (required), timeout_seconds.
get_replyPoll a session for new outputA
Watch a session and return whatever it produced since the given cursor, along with a fresh cursor. Blocks for at most wait_seconds, then returns what it has — so call it repeatedly to follow a long task. The result says whether the session finished or is still working. If it is still working, call this again with the returned cursor. Assistant turns list the tools the session used, which is the progress signal while a task is underway.
Inputs: session (required), since, wait_seconds.
list_sessionsList Claude Code sessionsA
Name the sessions you can drive. Returns one short line by default, because a voice client reads the whole result aloud. Ask for details only when the user wants them.
Inputs: detailed, include_unreachable.
read_transcriptRead session transcriptA
Read recent conversation turns from a session, including work it did while driven from a phone or from claude.ai. Subagent sidechains and tool plumbing are omitted.
Inputs: session (required), limit, include_user.
rename_sessionRename a sessionA
Change a session's display name, as shown in its prompt box and session pickers.
Inputs: name (required), session (required).
send_messageSend a message to a sessionA
WHEN TO USE THIS: any question or instruction about the user's own projects, code, repositories, machine, builds, deployments or work in progress belongs to a session — relay it. You do not know what is on their disk, and neither does a web search. Do not answer from your own knowledge, do not search the web, and do not ask clarifying questions the session could answer better: pass it through and let the session look. Answer in your own voice only when the user asks about you, or about how this connector itself works.
Queue a prompt in a running session and return immediately with a cursor. The message enters that session as if typed, so it is also visible to any Remote Control client watching it. Follow up with get_reply, passing the cursor as since, to watch the session work. Prefer this over ask for anything long-running.
Inputs: message (required), session (required).
session_statusGet session statusA
Report the current state of one session, resolved by name, session id, or pid.
Inputs: session (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
send_message and ask both relay prompts to a session, differentiated mainly by async-vs-sync behavior; the descriptions document this distinction but the overlapping purpose creates selection risk. read_transcript and get_reply also both retrieve conversation output, though they differ in full-history vs. incremental-polling. The remaining tools (list_sessions, session_status, rename_session) are clearly distinct.
Most tools follow a clean verb_noun pattern (list_sessions, read_transcript, send_message, get_reply, rename_session), but session_status breaks the pattern by omitting a verb, and ask is a bare verb with no noun. These are minor deviations that don't cause real confusion.
Seven tools is well within the ideal range for a session-driving server. Each tool has a clear job: list, status, transcript, async send, reply polling, sync ask, and rename — nothing feels redundant or missing at the count level.
The core drive-a-session workflow is fully covered: discover sessions, check status, send prompts (both sync and async), follow progress, and read history. The main gap is the lack of a stop/cancel operation for long-running work, and session creation appears out of scope since sessions come from phone or claude.ai clients.
Related MCP Connectors
Shared memory MCP server for Claude Code, Codex, Gemini CLI and Cursor. Cited, editable.
Connect ChatGPT and Claude to your Mac, Windows or Linux with secure MCP remote access.
Use AI models for chat, image, and video generation from Claude Code and other MCP hosts.
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Related MCP Servers
- AlicenseBqualityDmaintenanceMCP server for managing Claude Code conversation sessions1237 npmMIT
- AlicenseAqualityBmaintenanceLocal MCP server that wraps the headless Claude Code CLI as MCP tools, providing stateless access to Claude's coding capabilities through prompt-based interactions. It enables users to execute Claude Code commands with various prompt formats and structured outputs directly from MCP clients.3MIT
- AlicenseAqualityDmaintenanceThis MCP server enables remote control and management of Claude Code agents, allowing you to execute missions, configure agent personalities, and integrate with other MCP tools.715 npm1MIT
- FlicenseNot gradedqualityDmaintenanceExposes headless Claude Code as a remote MCP server with a voice client, enabling hands-free task execution and session management via OpenAI's Realtime API.-