Skip to content

feat(proxy): let hosts bypass the proxy via a configurable no_proxy list - #222

Merged
huhamhire merged 1 commit into
devfrom
feat/no-proxy
Sep 8, 2026
Merged

huhamhire merged 1 commit into
devfrom
feat/no-proxy

Conversation

@huhamhire

Copy link
Copy Markdown
Owner

Why

The proxy is a single global egress, but part of what the app reaches commonly lives inside the network the proxy leads out of — a self-hosted code platform, its git remote, an internal model server. Routing those through the proxy wastes a hop at best and makes them unreachable at worst, and turning the proxy off is not a fix, since the LLM egress still needs it.

Only loopback was bypassed, and only as built-in behaviour, so there was no way to express any of this. The design doc had left the gap open as a to-do ("if an intranet platform is collaterally harmed by the proxy … add a switch as needed later"); this fills it, and more generally than a per-platform toggle would.

What

Settings → Proxy now takes a Direct connections list, applying to every outbound path at once: the platform REST fetch (avatars and attachments included), git over HTTPS, pr-agent, and the local CLIs. One rule for a self-hosted platform covers both its REST API and its git remote.

The one design constraint that shaped everything

The same config is consumed by two structurally different egress classes:

  • subprocess (git / pr-agent / local CLIs) receives the rules as the NO_PROXY environment variable and interprets them with its own library — the app does not get to decide how git matches a host;
  • in-process (platform REST via undici) must evaluate them itself, since ProxyAgent does not honour NO_PROXY.

So the syntax mirrors the conventional NO_PROXY, and the supported subset is kept to what those implementations share. Anything richer would apply in-process and not in the subprocess, making the same config bypass on one egress while proxying on the other — a divergence nearly impossible to diagnose from the UI. That is also why CIDR ranges are deliberately not supported: only some implementations honour them.

Supported: a domain (covers its subdomains), an IP literal, *, case-insensitive, :port ignored, leading dot accepted as equivalent.

Matching lives in shared/no-proxy.ts and is used by both paths, so they cannot drift apart. Loopback stays built in and is prepended to whatever the user configured — a local service must never be proxied, and that guarantee should not depend on the user having typed it.

Normalization happens in the setProxy controller rather than in the form, so the stored value is canonical however the config arrived (IPC or a hand-edited config.yaml), and what the user reads back is what is actually matched.

Docs

  • arch — 99-core/03-networking-proxy.md: added why a bypass list is not optional and why the syntax must mirror NO_PROXY; the "platform caught in the crossfire" caveat now points at this list instead of deferring a separate toggle.
  • guide — 03-proxy.md + zh-CN gain a "Direct connections" section with a matching-rules table; 04-config-reference.md + its mirror document the no_proxy field and the yaml example. Both locales in sync.
  • CHANGELOG in both languages.

docs/ as a whole does not satisfy prettier (33 files, already true before this branch — verified against HEAD), so the five touched files were left in the repository''s existing style rather than reformatted, which would have buried the change under unrelated reflow.

Verification

lint / typecheck / test / build all pass. 14 new tests cover the matching semantics, including the cases that are easy to get subtly wrong:

  • evil-example.com must not be bypassed by a rule for example.com (the suffix has to fall on a label boundary);
  • 10.0.0.50 does not match 10.0.0.5;
  • a CIDR range is not silently treated as a match;
  • and a composed set asserting local addresses stay direct even when the user configured nothing.

Behaviour verified by the author in a real proxied environment against an intranet host.

🤖 Generated with Claude Code

The proxy is a single global egress, but part of what the app reaches commonly
lives inside the network the proxy leads out of -- a self-hosted code platform,
its git remote, an internal model server. Routing those through the proxy wastes
a hop at best and makes them unreachable at worst, and turning the proxy off is
not a fix, since the LLM egress still needs it. Only loopback was bypassed, and
only as built-in behaviour, so there was no way to express any of this.

Settings now take a bypass list, applying to every outbound path at once: the
platform REST fetch, git over HTTPS, pr-agent and the local CLIs.

The syntax mirrors the conventional NO_PROXY on purpose. The subprocess egresses
are handed these rules as the environment variable and interpret them with their
own libraries -- the app does not get to decide how git matches a host -- so any
richer syntax would apply in-process and not in the subprocess, making the same
config bypass on one egress while proxying on the other. Hence the portable
subset (domain plus subdomains, IP literal, `*`, case-insensitive, port ignored)
and the deliberate exclusion of CIDR ranges, which only some implementations
honour. Matching lives in shared/no-proxy.ts, used by both paths so they cannot
drift apart, and loopback is prepended to whatever the user configured: a local
service must never be proxied, and that guarantee should not depend on the user
having typed it.

Normalization happens in the setProxy controller rather than the form, so the
value stored is canonical however the config arrived, and what the user reads
back is what is actually matched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@huhamhire huhamhire added enhancement New feature or request documentation Improvements or additions to documentation labels Sep 8, 2026
@huhamhire
huhamhire merged commit c4f5c00 into dev Sep 8, 2026
3 checks passed
@huhamhire huhamhire mentioned this pull request Sep 9, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant