MITM & interception
There are two methods to send the HTTPS traffic of a CLI to Glider. Glider then adds one mechanism for Cursor only: the Agent RPC plane.
A comparison of the gateway and MITM
| Gateway | MITM | |
|---|---|---|
| The client setting | Set Override Base URL to :8080/v1 | Set the proxy, or set nothing for transparent mode |
| The traffic | Only the traffic that you sent to Glider | The cloud traffic of the CLI |
| A request that is not local | Glider uses your own OpenAI or Anthropic key | Glider sends the request to the origin of the CLI. The credentials do not change. |
| When to use it | Use the gateway for agent work with tools. It is tested and it operates with each CLI. | Use MITM for local text replies in Cursor, for transparent interception, and for delegation. |
CONNECT and the TLS certificate
CONNECT host:443
→ 200 Connection Established
→ HostMatcher.Match?
yes → make a leaf certificate from the Glider CA, decrypt, then try
a local reply, a delegation, or the origin
no → tunnel the data with no decryption (bidirectional io.Copy)
Glider decrypts only the hosts in the list. It makes the CA at
~/.glider/mitm/ca.{crt,key} when MITM starts the first time. Install
ca.crt in the trust store of your operating system. For a CLI that uses Node
or Electron, also set NODE_EXTRA_CA_CERTS. The Node runtime of Claude Code is
different: a live test showed that it trusts the operating system store, and it does not
need this variable. This is important for transparent mode below.
| Host pattern | Vendor |
|---|---|
api2.cursor.sh, api3, api4, *.api5.cursor.sh | Cursor |
api.anthropic.com | Claude Code |
daily-cloudcode-pa.googleapis.com, cloudcode-pa.googleapis.com, antigravity-unleash.goog, oauth2.googleapis.com, www.googleapis.com, play.googleapis.com | agy (Antigravity) |
A host that is not in this list goes to its origin with no decryption, because
mitm.passthrough_default is true. Set it to false to
refuse a host that you did not list, in place of sending it.
The certificate authority, and how to trust it
Glider makes its own CA the first time that MITM starts
(LoadOrCreateAuthority). It signs one leaf certificate for each host that it
decrypts.
| File | Content |
|---|---|
~/.glider/mitm/ca.crt | The certificate. You install this one. The setting is mitm.ca_cert. |
~/.glider/mitm/ca.key | The private key. Give it to no one. The setting is mitm.ca_key. |
Decryption fails until the CLI trusts that certificate. There are two steps, and one is not necessary for each CLI:
-
Install
ca.crtin the trust store of the operating system. On Windows this is Trusted Root Certification Authorities. -
For a CLI on Node or Electron, such as Cursor, point that runtime at the same file:
$env:NODE_EXTRA_CA_CERTS = "$env:USERPROFILE\.glider\mitm\ca.crt"
Use a variable of the user or of the system, so it continues after a restart. Stop Cursor completely after you change this.
Claude Code needs step 1 only. Its Node runtime trusts the store of the operating system directly. Therefore the trust is one action at the level of the system, and it is not a variable for each process. This is why transparent mode is complete for Claude Code, and why Cursor keeps the caveat about a session that is already open.
Routing and trust fail separately. The redirector can move the packets correctly, and the handshake can still fail because the CLI does not trust the CA. Test the two conditions one at a time.
Transparent redirection (Windows: WinDivert — Linux: iptables)
For the usual MITM mode, you must set the proxy in the CLI. Transparent mode does not need that setting. Glider redirects the outbound HTTPS packets in the operating system. It selects the packets by port and by process name. The CLI does not help, and the mode operates on a session that started before Glider. You must still trust the CA: refer to the section above.
mitm: transparent: true transparent_port: 8083 transparent_ports: [443] windivert_dll_path: ~/.glider/mitm/windivert/WinDivert.dll # Windows only; WinDivertNN.sys alongside it
| Platform | The mechanism | The permission that it needs |
|---|---|---|
| Windows | WinDivert. It changes the destination of a matched packet. | Administrator |
| Linux | An iptables REDIRECT rule in the nat OUTPUT chain, and SO_ORIGINAL_DST to recover the true destination. Glider calls sudo -n iptables. | root, or sudo with no password |
| macOS | None. The design uses pfctl, and no code exists. Glider gives a clear error. | — |
The iptables rules stay on the machine until Glider removes them. A stop that
is not clean — a failure, taskkill /F or SIGKILL — runs no code,
therefore it removes nothing. Glider cannot see that event when it occurs. It sees it at the
NEXT start, and it gives a warning. The warning tells you to test for a rule that stayed and
for a delegate subprocess with no parent:
iptables -t nat -L on Linux, or tasklist and netstat
on Windows. Glider gives the warning, and it does not repair the condition for you.
WinDivert changes the destination of a matched packet. The new destination is the primary
local IP address of this machine, on transparent_port. In the example above
this is :8083. It is not the loopback address. WinDivert then calculates the
checksums again and sends the packet. The TCP stack of the client does not see this
change. Windows has no function that gives the original destination. Linux has
SO_ORIGINAL_DST, but Windows does not. Therefore the redirector keeps its own
table of the flows. The key is the source ip:port of the client. To select
the processes, the redirector reads the vendor registry. The Vendors page
of the dashboard fills this registry. Do the discovery on that page first. If you do not,
transparent mode has no process filter and uses only the port.
A live test showed that this mode operates correctly with Claude Code. The test included
the redirection, the CA trust, no help from the CLI, and a terminal session that was
already open. There is no equivalent test for cursor-agent or agy. Linux has its own
implementation (internal/mitm/redirector_linux.go). It uses
iptables REDIRECT in the nat OUTPUT chain, and
SO_ORIGINAL_DST to get the true destination. On macOS the design is complete,
but no code exists: that platform gets a stub which gives an error at start. The macOS
design uses pfctl.
Cursor's Agent RPC plane (BidiAppend → RunSSE)
The Cursor protocol sends the prompt and the answer in two different RPCs. This section and the two sections below apply to the Cursor MITM plane only. They are not a general mechanism.
- BidiAppend. The prompt and the context arrive. Glider extracts the text of the most recent turn. Glider then makes a decision and calls
ArmLocalorArmOriginon the hub. The key is the request UUID. - RunSSE. The reply stream opens. The hub calls
Wait(reqID, 800ms). For a local decision, Glider encodes the text and sends the reply. If there is no local decision, Glider connects to the origin.
Glider can give a local text reply only for a root Agent RunSSE. A child
RunSSE and a tool-loop RunSSE go to the origin. You can turn on
mitm.agent_rpc_tool_codec, which gives a small map of the usual ToolCall
frames. The full list of frames does not exist yet. For agent work with tools, use the
gateway.
How the fulfill hub operates
AgentFulfillHub connects the BidiAppend prompt to the RunSSE answer for the
same request UUID. One mutex protects three maps. waiting holds the requests
where RunSSE arrived first. pending holds the requests where BidiAppend
arrived first. family holds the sticky condition of a turn family.
ArmLocal and ArmOrigin tell a waiter if a waiter exists. If no
waiter exists, they write to pending and start a 30-second timer to remove
it. Wait looks in pending first. If it finds nothing, it adds a
waiter and then waits for an offer or for an 800-millisecond timer. After the timer,
Wait looks in pending one more time. This is necessary when
BidiAppend arrives at the same moment as the timer. If the local encode operation fails
after a partial write, Glider must not connect to the origin, because the client already
received part of a reply.
The sticky-cloud states
The turn-family sticky condition does not apply to the full conversation. An explicit flag
or a cloud DecideLocal decision opens a family for a short time. The key is
the root request UUID. The immediate follow-on messages, such as the reply summary and the
title, get the same condition. Without this mechanism, those messages look like small
local prompts. The router then sends them to the local model, but they must stay with the
cloud parent that just operated.
| Constant | Value | Function |
|---|---|---|
defaultRunSSEFulfillWait | 800 ms | RunSSE waits this long for a BidiAppend decision. An idle reconnection goes to the origin. |
DefaultTurnFamilyTTL | 90 s | The length of the family window. The setting is routing.turn_family_ttl. |
DefaultCloudPostRunGrace | 120 s | The additional sticky time after the parent RunSSE ends |
Rule: while a StickyCloud family is live, a concurrent StickyLocal must not replace it. The full policy is in planning/routing_and_context.md.
The config settings
| Key | Function |
|---|---|
enabled | In the default profile this is true |
hosts | The hosts that Glider decrypts. Refer to the table above. |
agent_rpc_fulfill | Lets Glider give a Cursor text reply from BidiAppend through RunSSE |
agent_rpc_canned_on_error | Keep this false in normal operation |
agent_rpc_tool_codec | Turns on the Cursor tool-call frames. For agent work with tools, use the gateway. |
origin_on_local_error | Sends the request to the origin after a local failure. This is the default. Pure local mode sets it to false. |
transparent, transparent_port, transparent_ports, windivert_dll_path | Refer to transparent redirection above |
passthrough_default | true sends a host that you did not list to its origin. false refuses it. |
require_local_healthy | Glider selects the local model only after a health check succeeds. This stops a silent change to the origin. |
agent_rpc_canned_text | The fixed text of agent_rpc_canned_on_error. |
debug_agent_rpc, debug_dump_dir | Refer to the debug output below. |
You must restart Glider after you change the port, the CA, the hosts or the transparent settings. Glider accepts all other changes while it runs.
The configuration reference gives the type and the default of each key.
How to make transparent mode more narrow
Transparent mode selects the traffic by port and by the image name of the process. Therefore it catches EACH process on the machine with a name that agrees. To limit it to the processes that you name, send their PIDs:
curl -X POST -H 'content-type: application/json' \
-d '{"pids":[1234,5678]}' \
http://127.0.0.1:8081/api/mitm/enrollment
Glider then uses these PIDs in addition to the test on the name. An empty array, or no array, stops the narrow selection. That is the behaviour when you start.
The call succeeds and does nothing when transparent mode does not operate, or when the redirector of your platform does not support this. Therefore it is not dangerous to call it against a setup with no transparent mode.
The debug output
mitm.debug_agent_rpc: true adds three things. The environment variable
GLIDER_MITM_DEBUG_RPC does the same.
| What you get | Where |
|---|---|
| Structured logs of the Agent RPC plane | The output of Glider |
| The bodies of the requests and the replies | ~/.glider/mitm-debug/, or mitm.debug_dump_dir |
| The most recent intercepted requests | GET /api/mitm/debug/recent |
A dump holds the full body of a request. That body can contain your source code and your prompts. Turn this off when you finish, and remove the directory.