Skip to content

feat(claude-desktop): land the local intercept chain on dev - #5319

Merged
lidge-jun merged 18 commits into
devfrom
codex/260920-claude-desktop-chain
Sep 20, 2026
Merged

lidge-jun merged 18 commits into
devfrom
codex/260920-claude-desktop-chain

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Verification

  • Only the two test-layout registries conflicted, both because each side registers new test files. The resolution is the union of the two entry sets, and both files parse as JSON.
  • Local suites, typecheck, builds and installs were not run in this lane; exact-head hosted CI on this pull request is the execution evidence.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

lidge-jun and others added 18 commits September 20, 2026 02:39
…ver wiring

Claude Code honours HTTPS_PROXY/NODE_EXTRA_CA_CERTS from its settings env, so a
loopback CONNECT proxy plus a locally-signed TLS listener for api.anthropic.com
lets the router see Claude Code's Messages traffic without any ANTHROPIC_BASE_URL
rewrite and without touching the Desktop app's own (first-party) configuration.

- src/claude/intercept/local-ca.ts: ECDSA P-256 CA + leaf issuance with a
  hand-rolled DER encoder (node:crypto only); CA persisted under
  <OPENCODEX_HOME>/claude-intercept with a 0600 key, never installed in an OS store.
- src/claude/intercept/connect-proxy.ts: CONNECT-only loopback proxy; splices
  api.anthropic.com:443 onto the TLS listener, relays other targets blind,
  refuses plain HTTP, loopback targets and oversized heads.
- src/claude/intercept/listener.ts: TLS listener; POST /v1/messages and
  /v1/messages/count_tokens are rewritten onto a loopback origin and dispatched
  to the route table under the new claude-intercept ingress (loopback policy);
  every other path is relayed verbatim to the configured Anthropic upstream.
- src/claude/intercept/settings.ts: ownership-aware apply/inspect/remove of the
  two env keys in Claude Code settings.json (anchored on the CA path).
- src/claude/intercept/runtime.ts + server wiring: enabled by default on a hub,
  proxy port = public port + 100 unless claudeCode.intercept.port is set; bind
  failure degrades to a warning; stop joins both sockets.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…t wiring out of index.ts

startServer(0) has no stable port to derive the CONNECT proxy from, so the
intercept pair now stays off unless claudeCode.intercept.port is explicit;
this also keeps in-process test fixtures at their expected listener count.
The wiring moves into src/server/index/claude-intercept-lifecycle.ts and the
inbound-body-limit warning into startup-warnings.ts so src/server/index.ts
stays under its file-size cap.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…gets, share the intercepted-path predicate

isLoopbackTarget now checks 127/8, ::1, 0.0.0.0 and :: through a BlockList (which also
matches IPv4-mapped IPv6), *.localhost, and numeric resolver shorthands like 127.1.
serve-options.ts reuses isClaudeInterceptedPath from listener.ts so the two route lists
cannot drift apart.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… explicit opt-in

Add claudeCode.desktopMode (first-party | gateway). First-party keeps Claude
Desktop on claude.ai and writes only HTTPS_PROXY/NODE_EXTRA_CA_CERTS into
Claude Code's settings.json so the Code tab, subagents and the claude CLI go
through the local intercept pair. Gateway keeps the existing 3P profile writer.

- resolveClaudeDesktopMode: explicit > applied gateway fingerprint > first-party,
  so existing gateway installs do not flip on update while new installs get 1P
- resolveClaudeDesktopApplyMode: implied 1P falls back to gateway where the
  intercept pair cannot run (client role / intercept disabled)
- CLI: ocx claude desktop apply [--first-party|--gateway]; legacy shape flags
  imply --gateway; connected clients default to gateway
- API: /api/claude-desktop/apply accepts first-party|gateway (+ legacy shapes),
  status reports mode + firstParty block; native toggle applies resolved mode
  and disable removes both gateway profile and 1P env
- ocx ensure: refresh stale 1P env when ON, remove it when OFF
- modes are mutually exclusive; foreign proxy/CA env is never overwritten

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…file

Switching gateway -> first-party from the GUI/CLI/API previously refused with
gateway_profile_active and required turning the integration off first, while the
docs and the GUI switch note promise a direct replacement in both directions.
The first-party branch now pivots the owned gateway profile back to standard
(removeDesktop3pStandardPivot with replaceWhileEnabled, since the durable switch
stays ON) before writing the env, and fails without writing when the pivot cannot
complete.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…for first-party

- The native ON toggle pivots an owned gateway profile to standard (replaceWhileEnabled)
  before writing the first-party env, and persists the desktopMode marker, matching
  POST /api/claude-desktop/apply.
- Gateway apply now reports a failed mode-marker save as saved:false plus a warning
  instead of dropping the result.
- ensure/update warns when an explicit first-party marker contradicts a gateway profile
  still on disk rather than returning silently.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… record gateway mode from the native toggle

All three mode-marker writers (CLI apply, /api/claude-desktop/apply, native toggle) now share
recordClaudeDesktopMode. Switching to first-party clears desktopProfile.appliedFingerprint/appliedAt
so a lost explicit marker can no longer resolve back to gateway while first-party env is on disk;
the profile assignments stay for a later gateway apply. The native toggle's gateway enable branch
saves desktopMode="gateway" like the apply route does.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…er from an unmarked config

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ty docs

Dashboard Desktop tab gains a Connection mode picker (first-party default,
gateway opt-in) that sends the chosen mode with /api/claude-desktop/apply and
shows the intercept proxy state in first-party mode. Strings added to every
locale.

Docs describe both modes, the settings.json env the first-party apply writes,
intercepted routes, the local CA trust boundary, update behavior for existing
gateway installs, and Claude Code CLI compatibility/limitations. Translated
guides get a summary section pointing at the canonical English text.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
gui/src/styles.css sits at its file-size cap; the picker rules move byte-for-byte
into gui/src/styles/claude-desktop-mode-picker.css, imported from main.tsx.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The picker no longer pre-checks the first-party default while the status request is
in flight, so a gateway install does not see the wrong radio and badge flash.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ilure

A failed /status with no cached status left the picker disabled forever. It now unlocks on
the first-party default once the error is shown, while the current-mode badge and switch
note stay hidden because the real mode is still unknown.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ction mode

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Consolidates the three-part Claude Desktop chain into one branch against the
current dev: the local intercept proxy with its CA and CONNECT handling, the
first-party mode default with the gateway profile, and the connection-mode
selector with its documentation.

Both sides register new test files in the layout maps; the resolution is the
union of the two entry sets.

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 20, 2026 11:14
@lidge-jun
lidge-jun merged commit e3ac5be into dev Sep 20, 2026
1 check passed
@lidge-jun
lidge-jun deleted the codex/260920-claude-desktop-chain branch September 20, 2026 11:14
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-20T11:18:36.284795Z 44755a4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • UI screenshot required.

What to do

  • Add a screenshot of the UI change to the PR description.

Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required enforce-target check will keep failing until every issue above is resolved.

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3a8938a3-436b-4259-92ae-8a788e097650

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff7f33 and 44755a4.

📒 Files selected for processing (48)
  • docs-site/src/content/docs/fr/guides/claude-code.md
  • docs-site/src/content/docs/guides/claude-code.md
  • docs-site/src/content/docs/ja/guides/claude-code.md
  • docs-site/src/content/docs/ko/guides/claude-code.md
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/fr.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/tr.ts
  • gui/src/i18n/vi.ts
  • gui/src/i18n/zh-TW.ts
  • gui/src/i18n/zh.ts
  • gui/src/main.tsx
  • gui/src/pages/ClaudeDesktop.tsx
  • gui/src/styles/claude-desktop-mode-picker.css
  • gui/tests/claude-desktop-mode-picker.test.tsx
  • scripts/test-layout/layout.json
  • src/claude/desktop-3p.ts
  • src/claude/desktop-first-party.ts
  • src/claude/intercept/connect-proxy.ts
  • src/claude/intercept/listener.ts
  • src/claude/intercept/local-ca.ts
  • src/claude/intercept/runtime.ts
  • src/claude/intercept/settings.ts
  • src/cli/claude-desktop.ts
  • src/cli/ensure-desired-integrations.ts
  • src/config/schema/config-schema.ts
  • src/server/index.ts
  • src/server/index/claude-intercept-lifecycle.ts
  • src/server/index/serve-options.ts
  • src/server/index/startup-warnings.ts
  • src/server/management/agent-settings-routes.ts
  • src/server/management/native-integration-routes.ts
  • src/types/config.ts
  • structure/clients/claude-desktop.md
  • structure/gui-and-management-api.md
  • structure/runtime.md
  • tests/claude-integration/claude-desktop-first-party.test.ts
  • tests/claude-integration/claude-intercept-local-ca.test.ts
  • tests/claude-integration/claude-intercept-proxy.test.ts
  • tests/claude-integration/claude-intercept-settings.test.ts
  • tests/claude-integration/claude-management-api.test.ts
  • tests/codex-integration/native-claude-desktop-toggle.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/lab/core-lab-boundary.test.ts
  • tests/server/claude-intercept-integration.test.ts
 ____________________________________________________________________________________________________
< Don't live with broken windows. Fix bad designs, wrong decisions, and poor code when you see them. >
 ----------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44755a4b99

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +115 to +116
writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
renameSync(tmp, path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve private permissions when rewriting settings

When an existing ~/.claude/settings.json is mode 0600, this replacement file is created with the process umask (commonly 0644) and then renamed over it. Because the entire document is serialized, arbitrary env values such as API keys can become readable by other local users after applying or removing first-party mode; preserve the original mode or create/chmod the replacement to 0600 before renaming it.

AGENTS.md reference: AGENTS.md:L420-L426

Useful? React with 👍 / 👎.

): DesktopFirstPartyApplyResult {
const opencodexConfigDir = options.opencodexConfigDir ?? getConfigDir();
const target = desktopFirstPartyTarget(config, opencodexConfigDir);
if (!claudeInterceptEnabled(config)) return { ok: false, reason: "intercept_disabled", path: "" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse apply when the intercept proxy failed to bind

When the derived proxy port is occupied or another intercept startup step fails, createClaudeInterceptLifecycle catches the error and leaves no live proxy, but this check only verifies that interception is enabled in configuration. A dashboard apply can consequently report success and write a global HTTPS_PROXY pointing at a dead port, breaking Claude Desktop and standalone Claude Code HTTPS traffic; the management apply path should require the live intercept state before changing settings.

AGENTS.md reference: src/AGENTS.md:L15-L20

Useful? React with 👍 / 👎.

Comment on lines +1063 to +1064
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate first-party settings before removing gateway mode

When switching a working gateway installation to first-party mode, the gateway profile is removed before applyDesktopFirstParty can refuse a foreign/unreadable settings file or fail to create the CA. In those expected failure cases the endpoint returns an error only after disabling the previously working mode, and desired state has already been set to ON; preflight or stage the first-party write before performing the destructive pivot, with rollback if the final write fails. The CLI and native-toggle paths use the same unsafe ordering.

AGENTS.md reference: src/AGENTS.md:L15-L20

Useful? React with 👍 / 👎.

Comment on lines +257 to +258
if (!certPem.includes("BEGIN CERTIFICATE")) return null;
return { certPem, keyPem, publicKey, privateKey };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the persisted certificate matches its private key

A persisted certificate is accepted solely because its text contains a PEM marker, without checking that its public key matches ca.key. The two files are replaced separately, so a crash or two concurrent processes interleaving between the key and certificate renames can leave a valid certificate from one authority beside another authority's key; subsequent starts then issue leaves with the key while clients trust the mismatched certificate, permanently failing TLS until the state directory is deleted. Parse and validate the certificate/key pair before returning it, regenerating mismatched pairs.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants