Skip to content

fix(cli): bypass proxy env for loopback HTTP (hooks, runner control, local MCP)#868

Merged
tiann merged 1 commit into
mainfrom
fix/loopback-proxy-bypass
Jun 10, 2026
Merged

fix(cli): bypass proxy env for loopback HTTP (hooks, runner control, local MCP)#868
tiann merged 1 commit into
mainfrom
fix/loopback-proxy-bypass

Conversation

@tiann

@tiann tiann commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Problem

With a proxy exported in the shell (e.g. Surge/Clash: HTTP_PROXY=http://127.0.0.1:1080) and a NO_PROXY that doesn't literally cover loopback (NO_PROXY=npmjs.org, or the glibc-style 127.* wildcard Bun doesn't match), every loopback HTTP request in the CLI is routed through the proxy. Depending on the proxy's active policy, "127.0.0.1" gets forwarded to a remote node where nothing listens, and the request dies.

This breaks, flakily (depends on proxy policy at that moment):

  • Claude SessionStart hook-forwarder → hook server: the CLI never learns the Claude session id, the transcript scanner never starts, and the web UI stays empty (how this was found)
  • Runner control client (/session-started webhook → "Session webhook timeout", the bug in fix(cli): bypass HTTP_PROXY for runner webhook via raw socket #563)
  • claude → local HAPI MCP server connection
  • autoStartServer health checks

Root cause

Bun honors HTTP_PROXY/HTTPS_PROXY for both fetch and its node:http implementation. Evidence: posting to a dead loopback port returns the proxy's HTTP 503 error page instead of ECONNREFUSED.

Contrary to the analysis in #563, runtime mutation of process.env.NO_PROXY does take effect on Bun 1.3.14 (the version our CI builds with) — verified for both fetch and node:http, including after a prior fetch (no startup caching):

=== control (no mutation):     fetch → HTTP 503 (proxied), node:http → HTTP 503 (proxied)
=== with runtime mutation:     fetch → ConnectionRefused (direct), node:http → ECONNREFUSED (direct)

Fix

ensureLoopbackProxyBypass() appends localhost,127.0.0.1,::1 to NO_PROXY/no_proxy (preserving existing entries, no-op on *), called first thing in runCli(). One fix covers all in-process loopback calls and all child processes (claude, runner daemon, hook-forwarder), which inherit the patched env. Non-loopback traffic keeps using the configured proxy.

This also fixes the #563 reproducer: NO_PROXY="127.*,localhost" becomes 127.*,localhost,127.0.0.1,::1, and Bun matches the literal entry.

Testing

  • 5 new unit tests (proxyEnv.test.ts); cli suite: 972 passed, 1 pre-existing env-dependent failure (apiMachine.test.ts /var vs /private/var, fails on clean main too)
  • E2E under a live Surge proxy with HTTP_PROXY set: hook-forwarder went from proxy-503 to direct ECONNREFUSED on a dead port; against a live session, hook delivery + transcript sync resumed immediately

Supersedes #563 (thanks @XWang20 for the report and reproducer — same disease, this treats all infected call sites instead of one).

Bun's fetch and node:http honor HTTP_PROXY/HTTPS_PROXY env vars, which can
route loopback traffic through a configured proxy (e.g. Surge/Clash). When
NO_PROXY doesn't explicitly exclude localhost, this breaks loopback
communication: SessionStart hooks fail to arrive (transcripts don't sync, web
UI stays empty), runner control client times out, and MCP server connections
fail.

Normalize NO_PROXY at the CLI entrypoint to always cover loopback
(localhost, 127.0.0.1, ::1). Child processes inherit the patched env so their
loopback traffic is covered too. Non-loopback traffic continues using the
configured proxy. Supersedes the runner control client workaround from #563.
@tiann tiann merged commit 17439ae into main Jun 10, 2026
4 checks passed
@tiann tiann deleted the fix/loopback-proxy-bypass branch June 10, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant