Repository navigation
Conversation
Add crates/agent/src/intercept, a loopback HTTP CONNECT proxy for Claude Desktop in first-party mode. It is not wired into the daemon yet. - For the intercepted hosts, the listener terminates TLS with a leaf from an intercept CA and serves HTTP/2 and HTTP/1.1 by ALPN. It forwards each request to a gateway with the original host, the user's Cookie and Authorization unchanged, and one x-agentdesktop-identity header. It replaces any identity header that the client sends. - Every other host is tunnelled without decryption. - The intercept CA has Name Constraints for the intercepted hosts and excludes all IP ranges. Leaves are minted only for those hosts. - The listener refuses a request whose host does not match the CONNECT host or the TLS server name (421 or a failed handshake), an intercepted host on a port other than 443, and requests that are not CONNECT. - Bodies, server-sent events and WebSocket upgrades stream in both directions. Upgrades use HTTP/1.1 to the gateway. There is no 401 retry, and tunnels do not close when a credential expires. - Fail closed: when the gateway cannot be reached the request gets a 502. Nothing connects to an intercepted host directly. - Every connection resolves its host through the system resolver, so a DNS change after a network change applies at once. Pooled gateway connections are dropped after a wall-clock idle gap, which also covers sleep. A request without a body that fails before a response is sent once more. - Health counts forwarded requests, failures and the last failure with its cause, for the daemon status. The walkthrough test runs the listener end to end on loopback with no admin rights: cargo test -p agentdesktop-agent intercept::walkthrough -- --nocapture
…arty mode Wire the intercept listener into the system daemon and pin Claude Desktop to it. Configuration: - programs.claudeDesktop.gatewayMode: inference (the default, today's third-party mode) or intercept. - llmGateway.interceptUrl: where the listener forwards. Only the scheme, host and port are used. Without it, the listener uses those parts of llmGateway.url. - daemon.intercept.listen: the loopback listener address, which defaults to 127.0.0.1:18471. It is rejected in user mode. - Validation refuses intercept mode without a gateway, together with auth, or with pass-through keys that start with "inference" or "egressProxy", which would undo it. It also refuses an interceptUrl with a path. Daemon: - A supervisor in the system daemon follows the applied configuration. It starts the listener while Claude Desktop is in intercept mode, restarts it when the gateway changes, and stops it otherwise. When the listener cannot start, the supervisor tries again every 10 s. - The intercept CA key is kept in the daemon's secret store and the certificate in <state_dir>/intercept-ca.pem. A new CA is made when the intercepted hosts change, and the daemon logs its path and SHA-256. - The identity header carries the same gateway credential that the Claude Desktop credential helper gets today (client id claude-desktop). It is reused for at most 60 s and never within 30 s of its expiry. - daemon-info has an intercept section: enabled, bound, the last error, the CA path and fingerprint, counts, and the last failure with its cause. Provider: - In intercept mode the Claude Desktop managed settings get egressProxyUrl pointing at the listener and no inference keys, and the credential helper is removed. The pin is written even when the listener is down, so Claude Desktop fails instead of connecting directly. The schema is regenerated.
…ntegration suite The new integration test runs the system daemon in a container with programs.claudeDesktop.gatewayMode set to intercept. curl stands in for Claude Desktop, because Claude Desktop has no Linux release. The test checks: - the managed settings pin egressProxyUrl and set nothing else; - daemon-info reports the listener and its CA; - requests to claude.ai and api.anthropic.com reach the gateway with their host and cookie, and without the identity header that the client sent; - other hosts are tunnelled; - with interceptUrl on a closed port, requests fail with 502 and daemon-info shows the cause; - the daemon reuses its stored CA after a restart; - turning the mode off removes the pin and the listener. The gateway fixture now also records the host, cookie and identity headers.
…gateway The new integration test runs the daemon in Claude Desktop intercept mode in front of agentgateway v1.6.0. jwtAuth in strict mode reads the token from x-agentdesktop-identity. A fake OIDC provider signs the daemon in with device authorization and issues ES256 tokens, and the test origin stands in for Anthropic. The test checks: - before sign-in, the listener returns 502, daemon-info shows that sign-in is required, and no request reaches the origin; - after sign-in, requests to claude.ai and api.anthropic.com pass agentgateway, and the origin gets the original host, Cookie and Authorization without the identity header; - agentgateway refuses a request with no identity or a forged one, and accepts one with a valid token. agentgateway needs `prefix: "Bearer "` on the header location, because the daemon sends `Bearer <token>`. Without it every request gets a 401.
Run the real Claude Desktop app in first-party intercept mode on a GitHub-hosted macOS runner: build agentdesktop-headless, start agentgateway v1.6.0 with passthrough routes for claude.ai and api.anthropic.com, run the system daemon with programs.claudeDesktop.gatewayMode: intercept, trust its CA in the System keychain, then install Claude Desktop from its update feed (signature and team checked) and launch it without signing in. The job passes when main.log shows the pinned egress proxy and no ERR_CERT or ERR_SSL lines, agentgateway logs claude.ai answering with 2xx or 3xx, and daemon-info reports intercept.forwarded above zero. agentgateway logs Cloudflare's cf-mitigated response header, so a bot challenge from claude.ai fails with its own message. Triggered by workflow_dispatch and a weekly schedule only.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved credential-handling, transport-security, and proxy-configuration issues can expose credentials or interrupt Claude Desktop connectivity.
5 open issues
Require HTTPS for non-loopback intercept gateways · New Use one config snapshot for gateway routing and credentials · New Reject one-shot intercept mode without a running listener · New Avoid cancelling credential refresh when acquisition times out · New Reject ephemeral port zero for intercept listeners · New
What changed in this PR
Adds first-party Claude Desktop routing through agentgateway for #182, preserving claude.ai sign-in and account features instead of switching to third-party inference mode.
Changes:
- Adds intercept-mode configuration, validation, managed proxy settings, and daemon status.
- Implements a local TLS-intercepting listener with a name-constrained CA, gateway identity forwarding, streaming, and tunnels.
- Adds unit, integration, walkthrough, and macOS smoke coverage.
| File | Description |
|---|---|
| schema/daemon-config.md | Documents intercept configuration options. |
| schema/daemon-config.json | Adds generated intercept schemas. |
| crates/core/src/model.rs | Defines intercept status fields. |
| crates/core/src/config.rs | Adds intercept settings and validation. |
| crates/agent/tests/integration.rs | Registers Claude Desktop integration tests. |
| crates/agent/tests/common/mod.rs | Exposes gateway and identity fixtures. |
| crates/agent/tests/common/issuer.rs | Adds a test OIDC issuer. |
| crates/agent/tests/common/gateway.rs | Records forwarded identity and session headers. |
| crates/agent/tests/common/agentgateway.rs | Adds a real agentgateway container fixture. |
| crates/agent/src/reconcile/mod.rs | Passes the intercept address into reconciliation. |
| crates/agent/src/provider/vscode/tests.rs | Updates fixtures for new fields. |
| crates/agent/src/provider/vscode/settings_tests.rs | Updates gateway configuration fixtures. |
| crates/agent/src/provider/mod.rs | Adds intercept proxy reconciliation context. |
| crates/agent/src/provider/copilot/tests.rs | Updates fixtures for new fields. |
| crates/agent/src/provider/claude_desktop/testdata/Dockerfile | Adds the daemon-and-curl test image. |
| crates/agent/src/provider/claude_desktop/reconcile.rs | Pins the proxy without inference settings. |
| crates/agent/src/provider/claude_desktop/mod.rs | Supplies the intercept address to reconciliation. |
| crates/agent/src/provider/claude_desktop/integration_tests.rs | Tests lifecycle and gateway authentication. |
| crates/agent/src/provider/claude_code/mod.rs | Updates reconciliation test context. |
| crates/agent/src/llm_proxy.rs | Updates existing proxy test fixtures. |
| crates/agent/src/lib.rs | Exposes the intercept module. |
| crates/agent/src/intercept/walkthrough.rs | Exercises protocols and failure behavior on loopback. |
| crates/agent/src/intercept/store.rs | Persists the CA key and certificate. |
| crates/agent/src/intercept/run.rs | Supervises the listener and obtains credentials. |
| crates/agent/src/intercept/mod.rs | Defines shared health and connection utilities. |
| crates/agent/src/intercept/listener.rs | Handles CONNECT, TLS termination, and tunnels. |
| crates/agent/src/intercept/forward.rs | Forwards requests, streams, and WebSockets. |
| crates/agent/src/intercept/ca.rs | Generates constrained CA and leaf certificates. |
| crates/agent/src/daemon.rs | Integrates listener startup and reconciliation. |
| crates/agent/src/api.rs | Exposes live intercept status. |
| crates/agent/Cargo.toml | Enables interception and test dependencies. |
| Cargo.toml | Declares shared TLS and service dependencies. |
| Cargo.lock | Records added agent dependencies. |
| .github/workflows/claude-desktop-smoke.yml | Adds a real-app macOS smoke workflow. |
Review effort: Balanced
- Intercept mode requires an HTTPS gateway unless the gateway is on this device. The listener sends claude.ai cookies and tokens to it, so plain HTTP is allowed only for a loopback address, in validation and when the listener starts. - The listener fetches credentials for the configuration it started with, not the last saved one, so the identity always belongs to the gateway that requests go to. Any llmGateway change, such as a new issuer, restarts the listener and drops its cached credential. - A credential fetch runs on its own task, and only the wait for it times out. A token refresh that finishes late is still stored. - daemon --once refuses intercept mode, because nothing would serve the pinned listener after it exits. Dry runs, and cleanup of a Claude Desktop the user opted out of, still work. - daemon.intercept.listen refuses port 0, because Claude Desktop is pinned to that exact address.
peterj
reviewed
Oct 22, 2026
The walkthrough now lives in crates/agent/tests/intercept.rs, its own test target, and uses only the public intercept API. It still needs no Docker, keychain or admin rights. - The Name Constraints check needs leaves that the listener never mints, so it moved to a unit test in intercept/ca.rs and checks the verifier directly. - The Windows CI job runs the new target as well as the library tests, so the walkthrough keeps running on Windows. - tests/README.md says how to run it.
… into feat/claude-desktop-intercept # Conflicts: # crates/agent/Cargo.toml # crates/agent/src/provider/claude_desktop/reconcile.rs
The identity integration test failed in CI because a pull through cr.agentgateway.dev returned HTTP 525. The fixture now pulls the same v1.6.0 image from ghcr.io, pinned by digest, and tries the start up to three times, so a transient registry error does not fail the test.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Implements the design in #185 for #182. This is a draft, and it will change with the design.
Summary
In third-party inference mode, Claude Desktop does not sign in to claude.ai. Users lose their chat history, connectors and organization settings.
This PR adds
programs.claudeDesktop.gatewayMode: intercept. Claude Desktop stays signed in to claude.ai, and its Anthropic traffic goes through agentgateway:egressProxyUrl. It writes no inference settings.claude.aiandapi.anthropic.comwith a local CA. It tunnels all other hosts.CookieandAuthorization. It adds one header,x-agentdesktop-identity, with the gateway credential.If the listener or the gateway is not available, requests fail. Nothing connects to an intercepted host directly.
Configuration
llmGateway, anduseLlmGateway.daemon.intercept.listenmust be a loopback address with a fixed port.auth, and pass-through keys that start withinferenceoregressProxy.daemon --oncerefuses intercept mode, because no listener runs after it exits. Dry runs still work.On the device
<stateDir>/intercept-ca.pem.claude.aiandapi.anthropic.com, and never for an IP address.egressProxyUrlonly at launch.daemon-infohas a newinterceptsection: listener state, CA path and SHA-256, request and failure counts, and the last failure.On the gateway
Each intercepted host needs a passthrough route, matched by hostname, with TLS to the real host:
alpn: ["http/1.1"]lets WebSocket upgrades work on the upstream connection.With gateway authentication, add
jwtAuthto each route:mode: strictis necessary, because the default mode lets requests without a token through.Bearer <token>, soprefix: "Bearer "is necessary. Without it, every request gets a 401.jwtAuthremoves the header after validation.Listener behavior
CONNECT, and only on a loopback address.CONNECThost or the TLS server name. It also refuses an intercepted host on a port other than 443.Testing
cargo test -p agentdesktop-agent --test intercept -- --nocaptureruns the listener end to end on loopback, without Docker or admin rights. CI runs it on Linux and Windows. It covers HTTP/2, SSE, a 4 MiB upload, WebSockets, tunnels, the refusals, fail closed, and a TLS gateway.intercept/ca.rsshows that a verifier rejects leaves outside the Name Constraints.intercept_lifecycle: the daemon writes onlyegressProxyUrland serves the listener. With the gateway down, requests get a 502 anddaemon-infoshows the cause. With the mode off, the daemon removes the pin and stops the listener.identity_through_agentgateway: agentgateway v1.6.0 runs withjwtAuthin strict mode, and a test OIDC provider signs the daemon in. Requests fail before sign-in and pass after it. The upstream gets the original host,CookieandAuthorization, but not the identity header. agentgateway refuses a missing or forged identity.daemon-info. It runs on a manual trigger and weekly, not on pull requests. It has not run yet. Two questions are open. Do Anthropic's terms allow Claude Desktop in CI? Does claude.ai send a bot challenge to runner addresses?Known limitations
Cookie,Set-CookieandAuthorization.bad_record_macalert between agentgateway andapi.anthropic.com. Claude Desktop retried both. The cause is not known.interceptstatus yet.Refs #182