Skip to content

feat(claude): local intercept proxy — CA, CONNECT proxy, TLS listener, server wiring - #5301

Closed
devin-ai-integration[bot] wants to merge 4 commits into
devfrom
devin/1789891373-claude-intercept-core
Closed

devin-ai-integration[bot] wants to merge 4 commits into
devfrom
devin/1789891373-claude-intercept-core

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the runtime half of Claude Desktop first-party mode (1/3 in the stack). Nothing writes to ~/.claude/settings.json in this PR, so no client is pointed at the proxy yet — but the proxy pair itself is on by default on a hub (claudeCode.intercept.enabled !== false, off for runtimeRole: "client" and when claudeCode.enabled is false). PR 2 relies on that default so a fresh install resolves to first-party without extra config; set claudeCode.intercept.enabled: false to opt out. On its own this PR therefore creates the local CA and binds a loopback CONNECT listener on port + 100 that nothing connects to.

  • src/claude/intercept/local-ca.ts — ECDSA P-256 local CA, persisted under <configDir>/claude-intercept/{ca.pem,ca.key} (atomic write, 0600, regenerated if corrupt). Issues per-host leaf certs. Never installed into the OS trust store; Claude Code trusts it process-locally via NODE_EXTRA_CA_CERTS.
  • src/claude/intercept/connect-proxy.ts — raw HTTP CONNECT proxy. CONNECT api.anthropic.com:443 is handed to the TLS listener; any other host is blind-tunneled; non-CONNECT, malformed, oversized-header and local-target requests are rejected. Local targets cover 127/8, ::1, 0.0.0.0, ::, their IPv4-mapped IPv6 forms (::ffff:127.0.0.1, ::ffff:7f00:1), *.localhost, and numeric resolver shorthands (127.1, 0x7f000001, 2130706433).
  • src/claude/intercept/listener.ts — TLS termination with the issued leaf. POST /v1/messages and POST /v1/messages/count_tokens are rewritten to the loopback OpenCodex origin and served by the existing Claude Messages routing (path/query/headers/body/streaming preserved); every other path is relayed byte-for-byte to https://api.anthropic.com, so subscription OAuth traffic keeps working. isClaudeInterceptedPath is the single source of that list; serve-options.ts reuses it for the ingress allow-check.
  • src/claude/intercept/settings.ts — ownership-aware settings.json.env writer for HTTPS_PROXY / NODE_EXTRA_CA_CERTS: preserves unrelated keys, refuses to overwrite foreign (corporate) proxy/CA values, replaces stale OpenCodex-owned loopback values, never rewrites an unreadable file. (Consumed by PR 2.)
  • src/claude/intercept/runtime.ts + src/server/index/claude-intercept-lifecycle.ts — starts/stops the proxy with the server lifecycle, fire-and-forget so startServer stays synchronous; bind failure is logged as a warning, not fatal. startServer(0) (ephemeral) starts no proxy unless claudeCode.intercept.port is explicit, since the derived port would be unstable.
  • Config: claudeCode.intercept.{enabled,port} typed and schema-validated.

Verification

  • bun run typecheck
  • bun test tests/claude-integration/claude-intercept-*.test.ts tests/server/claude-intercept-integration.test.ts tests/lab/core-lab-boundary.test.ts tests/config
  • bun run privacy:scan, bun run structure:check, bun scripts/file-size-ratchet.ts

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed (structure docs here; user docs land in PR 3).
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults — CA key is 0600, proxy binds loopback only, local CONNECT targets (incl. IPv4-mapped) are refused, no OS trust-store changes.

Link to Devin session: https://app.devin.ai/sessions/4ec9c3528d424ced9e4a4da906168ad1
Open in Devin Desktop: https://app.devin.ai/desktop/session/4ec9c3528d424ced9e4a4da906168ad1?variant=devin
Requested by: @lidge-jun

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 4642dab4-a1d7-48ef-8374-8de99c390fa4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@devin-ai-integration
devin-ai-integration Bot added this pull request to stack #5304 September 20, 2026 08:31
@github-actions github-actions Bot added the enhancement New feature or request label Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 64 / 80

이 PR은 Claude Desktop을 first-party로 두는 스택의 1/3입니다. Claude Code가 ANTHROPIC_BASE_URL을 바꾸지 않고도 api.anthropic.com으로 가는 메시지를 opencodex가 보게 하려고, 로컬 CONNECT 프록시와 TLS 종료 리스너를 붙입니다. 흐름은 이렇습니다. Claude Code가 HTTPS_PROXY로 CONNECT를 열면, api.anthropic.com:443만 로컬 TLS 소켓으로 이어 붙이고, 나머지 호스트는 그대로 통과시킵니다. TLS 쪽에서는 POST /v1/messages와 POST /v1/messages/count_tokens만 루프백 주소로 바꿔 기존 라우터에 넘기고, 그 외 경로는 진짜 Anthropic으로 바이트 단위로 중계합니다. 인증서는 node:crypto만으로 ECDSA P-256 CA/리프를 만들고 <OPENCODEX_HOME>/claude-intercept/에 저장하며, OS 신뢰 저장소에는 넣지 않습니다. settings.json에 HTTPS_PROXY/NODE_EXTRA_CA_CERTS를 쓰는 헬퍼도 들어 있지만, 이 PR에서는 실제로 쓰지 않고 PR 2가 가져갑니다. 서버는 fire-and-forget으로 프록시를 띄우고, 바인드 실패는 경고만 남깁니다. ephemeral startServer(0)은 안정 포트가 없어서 intercept.port를 명시하지 않으면 프록시를 안 켭니다. base는 dev이고, 위 스택은 #5302(모드)·#5303(GUI/문서)이 이어집니다. 단위·프록시·설정·startServer 통합 테스트가 꽤 두껍습니다.

라인 - PR 본문 vs claudeInterceptEnabled: 본문은 claudeCode.intercept.enabled가 켜져야 프록시가 뜬다고 하지만, 코드는 enabled !== false라서 허브에서는 설정이 없어도 기본 ON입니다. types/structure 문서도 기본 ON입니다. 이 PR만 머지해도 허브는 CA를 만들고 port+100 CONNECT 프록시를 띄웁니다(설정 파일 쓰기는 안 함).
라인 - connect-proxy.ts isLoopbackTarget: localhost/127.0.0.1/::1/127.*만 막습니다. CONNECT [::ffff:127.0.0.1]:포트 같은 IPv4-mapped 루프백은 통과해 로컬로 터널이 열릴 수 있습니다. 바인드는 127.0.0.1이라 원격 악용 면적은 작지만, 같은 머신 프로세스가 우회할 여지는 있습니다.
라인 - 가로채는 경로 목록이 listener.ts의 INTERCEPTED_PATHS와 serve-options.ts의 claudeInterceptRouteAllowed에 각각 있습니다. 한쪽만 고치면 한쪽은 중계·한쪽은 404처럼 어긋날 수 있습니다.
라인 - settings.ts는 이 PR에서 런타임이 호출하지 않습니다. 스택 설계상 맞지만, 기본 ON과 맞물리면 “아직 사용자에게 안 보인다”는 본문 표현과 어긋납니다. 프록시/CA는 이미 살아 있습니다.
라인 - 호스티드 CI: tip f6d64ac에서 Analyze(js/ts)·changes 등 일부가 아직 pending입니다. 초록 확인 후 머지하는 편이 안전합니다. 이 환경에서는 로컬 풀 스위트를 돌리지 않았습니다.

메인테이너의 판단이 필요한 지점

허브에서 intercept를 기본 ON으로 둘지, PR 2가 settings를 쓰기 전까지는 기본 OFF로 둘지. PR 본문·릴리즈 노트와 코드를 어느쪽에 맞출지. IPv4-mapped(및 유사) 루프백 CONNECT를 지금 막을지, 루프백만 바인드이니 후속으로 둘지. #5302/#5303과 같이 스택 순서로만 머지할지.

너의 추천

방향과 보안 기본값(루프백 전용, CA 0600, OS trust 미설치, 실패 시 degrade, settings 소유권 검사)은 좋습니다. 머지 전에 (1) 기본 ON/OFF를 본문·타입 주석·실제 기본값 중 하나로 맞추고, (2) IPv4-mapped 루프백 CONNECT 거부를 좁게 넣는 쪽을 권합니다. 경로 허용 목록은 한곳으로 모으거나 공유 상수를 쓰면 드리프트를 줄입니다. CI tip 통과 후 #5302 → #5303 순으로 이어 주세요. 로컬 풀 스위트는 이 환경에서 돌리지 마세요.

이 댓글은 grok-bot이 작성했습니다

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions
github-actions Bot marked this pull request as draft September 20, 2026 09:21
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Addressed in 7d154b0:

  • IPv4-mapped loopback CONNECT — isLoopbackTarget now goes through a net.BlockList (127/8, ::1, 0.0.0.0, ::), which also matches the mapped forms (::ffff:127.0.0.1, ::ffff:7f00:1), plus *.localhost and numeric resolver shorthands (127.1, 0x7f000001, 2130706433). Covered in claude-intercept-proxy.test.ts.
  • Duplicated path list — serve-options.ts now calls isClaudeInterceptedPath from listener.ts; there is one list.
  • Default ON vs PR body — the code/types/structure docs are the intended contract: intercept is on by default on a hub so PR feat(claude-desktop): first-party mode as default, gateway profile as explicit opt-in #5302's first-party default works with zero config. The PR body was the one out of line; rewritten to say so explicitly (and that this PR alone binds the port+100 listener with no client pointed at it). claudeCode.intercept.enabled: false is the opt-out.

Merge order stays #5301 → #5302 → #5303.

lidge-jun and others added 4 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>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789891373-claude-intercept-core branch from 7d154b0 to 117d257 Compare September 20, 2026 09:40
@lidge-jun

Copy link
Copy Markdown
Owner

추가 리뷰 · 우선순위 42 / 80

이전 리뷰(우선순위 64) 이후 tip이 117d257로 올라왔습니다. Devin이 답한 세 가지가 이 커밋에 들어 있습니다. (1) isLoopbackTarget이 net.BlockList로 127/8·::1·0.0.0.0·::를 보고, IPv4-mapped(::ffff:127.0.0.1, ::ffff:7f00:1)와 *.localhost, 숫자 단축형(127.1, 0x7f000001, 2130706433)까지 막습니다. 프록시 통합 테스트와 단위 테이블이 같이 늘었습니다. (2) serve-options.ts의 claudeInterceptRouteAllowed가 listener.ts의 isClaudeInterceptedPath만 쓰므로 경로 목록이 하나로 모였습니다. (3) PR 본문이 허브 기본 ON(enabled !== false)·opt-out·이 PR만으로는 클라이언트가 안 붙는다는 뜻을 코드·types·structure와 맞게 고쳐 두었습니다. 방향은 그대로 좋고, 첫 리뷰에서 막혔던 보안·드리프트·문서 불일치는 실질적으로 해소됐습니다. 이 PR은 여전히 스택 1/3이라 settings.json 쓰기는 PR 2(#5302) 몫이고, 허브는 설정 없이도 CA를 만들고 port+100 CONNECT를 띄웁니다.

라인 - connect-proxy.ts isLoopbackTarget 숫자 단축 정규식: isIP가 아닌 0x…/숫자.숫자 형태는 전부 루프백으로 거절합니다. 루프백 우회는 잘 막지만, 정수형 비루프백(예: 192.168.0.1의 십진 정수)도 같이 막힐 수 있습니다. Claude Code가 그런 authority를 쓸 일은 거의 없고 fail-closed라 당장 위험은 작습니다.
라인 - 허브 기본 ON: 이 PR만 머지해도 리스너/CA는 살아 있고 클라이언트는 아직 안 붙습니다. 본문에 명시됐고 PR 2 전제라 설계상 맞지만, 포트 +100 충돌·불필요 바인드는 운영에서 한 번 보면 좋습니다.
라인 - 호스티드 CI: tip 117d257에서 macos·test 샤드·gates 등이 아직 pending입니다. 초록 확인 후 머지하는 편이 안전합니다. 리뷰 readiness 체크리스트도 아직 0/4입니다. 이 환경에서는 로컬 풀 스위트를 돌리지 않았습니다.

메인테이너의 판단이 필요한 지점

숫자 단축 거절을 “루프백만”으로 더 좁힐지, 지금처럼 넓게 fail-closed로 둘지. 허브 기본 ON + 빈 리스너를 이 PR에서 그대로 둘지, 아니면 PR 2와 같이 올릴지. draft 체크리스트·CI tip 통과를 머지 게이트로 둘지.

너의 추천

이전 세 지적은 반영된 것으로 보고, 남은 우선순위는 낮춥니다. tip CI가 초록이면 #5301 → #5302 → #5303 순으로 이어 가도 됩니다. 숫자 단축 과거절은 후속 미세 조정으로 충분합니다. 로컬 풀 스위트는 이 환경에서 돌리지 마세요.

이 댓글은 grok-bot이 작성했습니다

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Agreed on all three. The numeric-shorthand rule is intentionally fail-closed: any non-isIP all-numeric authority is rejected rather than parsed into an integer and range-checked, because Claude Code never dials api.anthropic.com that way and a permissive parser is the bug surface. Happy to narrow it to true loopback in a follow-up if it ever bites. Hub default-ON with the empty port+100 listener stays here so #5302 can rely on it; opt-out is claudeCode.intercept.enabled: false. Waiting on tip CI before moving the stack.

@lidge-jun

Copy link
Copy Markdown
Owner

Superseded by #5319, merged to dev as e3ac5bea0427cb759b08c7bf691ded6a2392c002, with attribution in the branch commit.

The chain was carried as a GitHub native stack, which stops accepting ordinary merges once its bottom lands by squash. Its tip already contained both ancestors and branched cleanly from dev, so the three parts were consolidated onto the current dev as one branch instead. Only the two test-layout registries conflicted, resolved as the union of both entry sets.

Closing as superseded rather than stale.

@lidge-jun lidge-jun closed this Sep 20, 2026
@lidge-jun
lidge-jun deleted the devin/1789891373-claude-intercept-core branch September 20, 2026 11:56
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.

1 participant