Repository navigation
feat(proxy): let hosts bypass the proxy via a configurable no_proxy list - #222
Merged
Merged
Conversation
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>
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.
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:
NO_PROXYenvironment variable and interprets them with its own library — the app does not get to decide how git matches a host;ProxyAgentdoes not honourNO_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,:portignored, leading dot accepted as equivalent.Matching lives in
shared/no-proxy.tsand 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
setProxycontroller rather than in the form, so the stored value is canonical however the config arrived (IPC or a hand-editedconfig.yaml), and what the user reads back is what is actually matched.Docs
NO_PROXY; the "platform caught in the crossfire" caveat now points at this list instead of deferring a separate toggle.no_proxyfield and the yaml example. Both locales in sync.docs/as a whole does not satisfy prettier (33 files, already true before this branch — verified againstHEAD), 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/buildall pass. 14 new tests cover the matching semantics, including the cases that are easy to get subtly wrong:evil-example.commust not be bypassed by a rule forexample.com(the suffix has to fall on a label boundary);10.0.0.50does not match10.0.0.5;Behaviour verified by the author in a real proxied environment against an intranet host.
🤖 Generated with Claude Code