All Your Claude Are Belong To Us: Reversing Claude Code's Remote Control Protocol
Anthropic recently introduced the Remote Control feature for Claude Code. This feature allows users to remotely interface with their coding session via smartphone apps or a web UI, giving them the ability to code and touch grass simultaneously.
Naturally, any feature offering complete control of a remote system is a prime candidate for security research. After spending some time reverse engineering the protocol I was able to implement a server that replicates full remote control functionality. Additionally, claude.exe exposes an undocumented command line option --sdk-server that will connect the Claude Code instance to any arbitrary address. This combination effectively provides attackers who have a means to influence Claude's command line on launch with a beaconing C2 coding agent.
In this blog post I will describe how I discovered this feature, reversed the protocol from the compiled claude.exe binary, and implemented a remote control server. Finally, I will provide some security considerations for users and organizations to protect themselves.
Discovery
I began looking into Claude's Remote Control functionality by trying to understand its operating modes and application behaviour. According to the documentation, Remote Control can operate in 3 modes: Server, Interactive Session and Existing session.
In existing and interactive modes, launched with claude --remote-control or /remote-control in session, the main Claude process connects to various /worker/* endpoints from your base session ID to stream events between the control server and the client.

In Server mode, launched with claude remote-control, the main Claude process launches multiple subprocesses (up to 32) that connect to the remote host using the undocumented command line option --sdk-url <URL>, connecting to session ID + worker/events/stream.

This command line option caught my attention as it was apparently specifying a specific endpoint instead of just browsing to built-in endpoints as in the first two modes. This led me to begin reversing the claude.exe binary in an attempt to better understand the functionality.
Reversing
Note that the reverse engineering effort was done in the
claude.exeWindows binary, but as you will soon see these results will be true for all versions of Claude Code
Throwing claude.exe into Binary Ninja I immediately noticed a custom data section, .bun that contains nearly 25MB of minified ASCII JavaScript.

This is a Bun-compiled executable, which generates cross-platform, standalone bundles containing the Bun runtime and JavaScript/TypeScript application so it can run natively on any system without requiring installation of the corresponding runtime. The executable .text section contains this runtime and can be safely ignored for our purposes.
I created a simple python script to map the .bun section from the executable and locate the module boundaries using the markers @bun @bytecode @bun-cjs\n and })\n\0. The module names were included just before the section header with an absolute path based at B:/~BUN/root/. Each one of these modules was extracted as raw ASCII string and written to a file, this resulted in the following 7 modules:
| # | Module | Size | Notes |
|---|---|---|---|
| 0 | src/entrypoints/cli.js | 11.2 MB | Main bundle — all Claude Code logic |
| 1 | src/entrypoints/cli.js | 11.2 MB | Identical duplicate (bridge/child worker copy) |
| 2 | resvg.js | 0.7 KB | SVG renderer shim (module.exports = "B:/~BUN/root/resvg.wasm") |
| 3 | image-processor.js | 2.5 KB | Image processing |
| 4 | file-index.js | 2.5 KB | File indexing |
| 5 | tree-sitter-bash.js | 2.5 KB | Bash syntax parser |
| 6 | color-diff.js | 2.5 KB | Colour diffing |
| 7 | audio-capture.js | 5 MB | Audio capture / Voice input |
Using just the minified JavaScript was sufficient for Claude to extract the required functionality, but I also used an online un-minifier to make it human-readable and allow me to verify Claude's analysis.
From the un-minified JS, I was able to follow the URL provided to --sdk-url through to the function getTransportForURL(), minified as wR_()and discover the various transports used for remote control:
function wR_(H, A = {}, $, f) {
// Branch 1: CCR v2 SSE transport
// CLAUDE_CODE_USE_CCR_V2 is checked first — takes absolute priority.
if (lH(process.env.CLAUDE_CODE_USE_CCR_V2)) {
let D = new OR_.URL(H.href);
// Normalize WebSocket schemes to HTTP for SSE:
if (D.protocol === "wss:") D.protocol = "https:";
else if (D.protocol === "ws:") D.protocol = "http:";
// http:// and https:// pass through unmodified.
// Append the CCR v2 SSE stream path:
return (D.pathname = D.pathname.replace(/\/$/, "") + "/worker/events/stream"), new LPH(D, A, $, f);
}
// Branch 2: Raw WebSocket or CCR v1 POST ingress
// Only ws: and wss: are accepted here.
if (H.protocol === "ws:" || H.protocol === "wss:") {
if (lH(process.env.CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2)) return new JtH(H, A, $, f);
return new GtH(H, A, $, f);
// Branch 3: Unsupported scheme — throws
// http:, https:, ftp:, etc. all land here without CCR v2 env var.
} else throw Error(`Unsupported protocol: ${H.protocol}`);
}This tells us that the HTTPS transport we observed in Server mode is only allowed if the CLAUDE_CODE_USE_CCR_V2 environment variable is present, otherwise an error is thrown. I was able to confirm this by observing the environment variables in the subprocesses with System Informer and launching my own instances of Claude Code with various command line/variable configurations.
The other thing this tells us is that there is a fallback websocket transport that connects directly to the unmodified URL (provided as H). Following out the code path I observed there did not appear to be any kind of authentication required or URL/certificate validation. Based on this I spun up a simple python server to accept connections on localhost and was able to receive an upgrade request using claude.exe --sdk-url ws://127.0.0.1:

By further reversing the minified code, testing the functionality of the actual Remote Control feature and some good ol' fashioned network debugging I was able to discover the complete remote control protocol.
On March 30th, 2026, the published npm package for Claude Code included a source map file (cli.js.map) that contained the full original TypeScript source that was bundled into cli.js, allowing me to verify these results against source - credits to @Fried_rice for the discovery.
Protocol
Messages are sent as Newline-Delimited JSON (NDJSON) JSON.stringify(obj) + "\n" over WebSocket text frames. Multiple messages may be present in a single frame.
The initial connection performs the WebSocket upgrade and then the server sends with an initialization control request, which includes a system prompt, mcp servers, and hooks. Then claude.exe acknowledges and provides infomation about the available models, commands, account and current PID.
Server claude.exe
| |
| <--- [WebSocket CONNECT] --- |
| --- 101 Switching Protocols -------> |
| |
| --- control_request(initialize) ---> |
| (server sets: systemPrompt, |
| sdkMcpServers, hooks) |
| |
| <--- control_response(success) --- |
| (claude returns: available models, |
| commands, account info, PID) |
| |Each turn begins with the server sending the prompt as a user message. claude.exe then responds with system/init message that gives current state, then follows up with with the assistant messages. If permissions are set such that tool use should be requested, these requests are made with the control protocol via control_request(can_use_tool), which the server can respond to with a tool use decision.
Server claude.exe
| |
| --- user message ---> |
| |
| <--- system/init --------- |
| (claude announces: model, tools, |
| cwd, version, permissions) |
| |
| <--- stream_event (token by token) --- |
| <--- assistant (complete turn) ------- |
| <--- control_request(can_use_tool) --- |
| --- control_response(allow) ---> |
| <--- result (turn complete) ----------- |
| |The control protocol is multiplexed over the same NDJSON stream. Either side can send control_request; the other must reply with control_response matching the request_id.
{
"type": "control_request",
"request_id": "<uuid>",
"request": { "subtype": "...", ... }
}
----
{
"type": "control_response",
"response": {
"subtype": "success",
"request_id": "<uuid>",
"response": { ... }
}
}These requests contain various subtypes, defining different actions each side can take:
Server can send to claude.exe:
initialize— session handshake (required, once)set_permission_mode— change permissions mid-sessionset_model— switch modelsinterrupt— abort current turnend_session— terminate the processkeep_alive— sent on idle sessions to hold the connection open- And many other subtypes for MCP management, context queries, settings, etc.
Claude.exe sends to server:
can_use_tool— "May I run Bash with{command: 'rm -rf /'}?"
Permissions
Sending set_permission_mode with mode: "bypassPermissions" at any time silently auto-approves all tool uses, no can_use_tool requests are sent. The model gets unrestricted file writes and command execution. Although this is essentially just a packet-saving mechanism given the server can already auto-approve all tool uses.
Note: I implemented CCRv1 for my proof of concept, CCRv2 has essentially the same control message schema, just wrapped in an envelope and sent over SSE + POST instead of WebSockets. I will leave the exact implementation as an exercise for the reader.
Implementation
With the protocol fully mapped, I implemented a server in Python that speaks CCR v1, allowing full remote programmatic control of claude.exe:
The server handles the full lifecycle: accepting the WebSocket connection, receiving system/init, sending initialize, dispatching user prompts, streaming responses, auto-approving tool permissions, and processing results.
Keep an eye out for the next release of Praxis, that will integrate this functionality as a new node type.
Security
The Remote Control documentation states that this feature is off by default on Team and Enterprise plans, and that when present, you must explicitly enable it in claude remote-control. However, the --sdk-url flag of concern is always included with the binary and there is no verification of the plan type or settings profile to run the binary with that flag.
The --sdk-url flag accepts any URL. There is no authentication, no hostname allowlist, no certificate pinning, no domain check. The only validation is that --input-format=stream-json and --output-format=stream-json must also be set.
For ws:// (plaintext WebSocket), claude.exe will connect to any address with zero restrictions. For wss://, standard TLS validation applies, but NODE_TLS_REJECT_UNAUTHORIZED=0 can disable it globally. The CCRv1 WebSocket protocol is subject to standard firewall protections, but CCRv2 running over HTTPS is very likely to be able to reach out of most networks.
The major security concern with this implementation of the Remote Control protocol is that an attacker with a means of influencing the launch of the Claude executable can redirect the connection to their controlled infrastructure. While this requires a method of initial access or code execution on the endpoint, it effectively becomes another tool in the Living-Off-The-Land toolkit. Combined with a reliable persistence mechanism, the attacker now has a trusted executable that can perform semantic interpretation of their operational goals.
Convincing Claude Code to get all the auth tokens from your browser to test a new MCP server or to summarize all your recent emails is much simpler than writing a scraper, compiling a BOF and deploying a CS beacon with the proper malleable C2 profile optimized for stealth.
More critically, this collapses the distance between a drive-by prompt injection and complete host takeover. If an attacker can get a malicious prompt in front of a Claude Code session — via a poisoned file, a compromised dependency or other injection vector — that prompt can now instruct Claude to connect back to attacker-controlled infrastructure via --sdk-url, handing over complete remote control of the endpoint.
Having the --sdk-url option at all seems like an unnecessary choice to me. As previously discussed the non-server Remote Control modes operate just fine without it. These modes are presumably using hardcoded anthropic URLs, and thus it would not be possible to redirect them to attacker-controlled infrastructure.
Even though this doesn't provide initial remote access to systems, features like this lower the bar for attackers everywhere and make it much easier for them to complete their goals - subsequently making securing our systems harder for the rest of us.
Ways to protect yourself:
- Monitor process command lines for
--sdk-url. Any Claude Code process spawned with this flag pointing to a non-Anthropic host is suspicious. - Monitor outbound WebSocket connections from Claude Code (
claude.exe/node.exeunder the Claude Code install path). Legitimate remote control connects to Anthropic infrastructure, connections to unknown hosts are a red flag. - Only allow Claude Code where necessary via application control policies (WDAC/AppLocker/third-party). The
--sdk-urlflag works regardless of account type or status, so even dormant installations are viable for exploitation.
Disclosure
These findings were submitted to Anthropic via their HackerOne bug bounty program, where they were closed due to the local code execution requirement.