# Egress and corporate proxies
Everything the client sends outward — cloud forwarding, policy-bundle sync, update checks and downloads, telemetry, and the model relay's upstream call — goes through one egress route. Loopback never does: the hook's POST to the daemon and the CLI's calls to the local admin endpoints bypass the proxy unconditionally, along with anything `no_proxy` matches.
## Zero-config on most corporate machines
`openlatch init` finds the route on its own where it can: it probes what is already configured, then walks the OS's own settings (Windows WinINET/WinHTTP and PAC, macOS `SCDynamicStore` and CFNetwork, GNOME `gsettings`), and only asks — once, on a terminal, unless `--yes` was passed — when none of them reach the platform. A headless failure exits `1` with a JSON report of every candidate it tried. Whatever it finds is written to `[proxy]` in `config.toml`:
```toml
[proxy]
mode = "auto" # auto | manual | direct
url = "http://proxy.corp:8080" # http, https, socks5, socks5h — never userinfo
auth = "auto" # auto | none | basic | negotiate
no_proxy = ".corp.example,10.0.0.0/8"
ca_bundle = "/etc/ssl/corp-root.pem" # merged on top of the OS roots
allow_direct = true # false = never fall back to a direct connection
# source is written by discovery — do not hand-edit it
```
Three consequences worth knowing before you deploy at scale:
- **Whatever made `init` work is written down.** The daemon runs under a supervisor with none of your shell's variables, so a route that lived only in `OPENLATCH_PROXY` would leave the daemon going direct an hour later. `[proxy] url` and `[proxy] source` record it.
- **Credentials never reach `config.toml`.** The `[proxy]` table has no password key: the value goes to the OS keychain, with an encrypted-file fallback on headless hosts, and every place a proxy URL is printed or logged is masked as `user:*****@host`.
- **Enabling the model relay adds loopback `NO_PROXY` entries to Claude Code's config**, and leaves them behind on teardown. Codex's `config.toml` has no environment block, so **no bypass is written for it at all** — on a proxied host, export `NO_PROXY=127.0.0.1,localhost` in the shell that launches `codex`, or its traffic leaves for the corporate proxy instead of the local listener. [configuration.md](configuration.md#what-the-daemon-writes-into-the-agents-config) explains why.
## Commands
`openlatch init` takes the route as flags — `--proxy`, `--ca-bundle`, `--no-proxy`, `--proxy-mode`, `--proxy-auth`, `--proxy-spn`, `--dry-run` — listed in [cli.md](cli.md#init-flags). After install:
| Command | What it does |
| ------- | ------------ |
| `openlatch system proxy status` | The egress route in effect, in both contexts. Works with the daemon down. |
| `openlatch system proxy discover [--force]` | Re-runs the OS discovery ladder, probes each candidate, persists the winner with its `source`. Refuses to overwrite a route you set by hand without `--force`. |
| `openlatch system proxy set <url> [--ca-bundle P] [--no-proxy L] [--spn S]` | Pins a route: writes `mode = "manual"` **and** `source = "manual"`, which is what stops discovery and self-heal from replacing it. Prompts for the credential on a `407`. |
| `openlatch system proxy clear` | Go direct: `mode = "direct"`, route cleared, credential deleted. |
| `openlatch system proxy test [--url T]` | Probes the chain hop by hop. Exit `0` / `7` / `1`. |
`openlatch doctor` names the failing hop in its Connection section — the hop `Cloud` travels over, reported separately so a dead proxy and a dead platform cannot render as the same failure.
## The `[proxy]` table
Resolved per key, environment beating file. The environment equivalents (`OPENLATCH_PROXY` and friends) and where the standard `https_proxy` family sits in the ladder are in [configuration.md](configuration.md#environment-variables).
| Key | Meaning |
| --- | ------- |
| `mode` | `auto` walks the resolver ladder, `manual` uses exactly what is configured and is never overwritten, `direct` never uses a proxy. |
| `url` | `http://`, `https://`, `socks5://` or `socks5h://`. Never carries a username or password — a password in a plaintext file on disk is refused at parse. |
| `username` | The account to authenticate as. |
| `auth` | `auto` probes the proxy once at start-up and picks a scheme; `basic`, `negotiate` and `none` are explicit and skip the probe. NTLM is deliberately absent. |
| `no_proxy` | Additive bypass list, Go grammar (`example.com`, `.example.com`, `10.0.0.0/8`, `*`). |
| `ca_bundle` | A PEM bundle merged **on top of** the OS trust store, for a TLS-inspecting proxy whose CA is not installed system-wide. Never a replacement. |
| `allow_direct` | `false` refuses to fall through to an unproxied connection when no proxy resolves. |
| `spn` | Kerberos service principal override. Defaults to `HTTP/<proxy-host>`. |
| `http1_only` | Force HTTP/1.1 for an inspection proxy that mishandles HTTP/2. |
| `pac_url` | An explicit PAC URL, handed to the OS resolver (WinHTTP, CFNetwork). Refused on Linux with `OL-1225`: this client ships no JavaScript engine. |
## Kerberos / Negotiate
Negotiate rides the `proxy-negotiate` feature, which every released binary ships. It uses the logged-on identity — SSPI on Windows, GSSAPI on macOS and Linux, loaded at first use so a host without Kerberos still starts and still reports why the transport is unavailable. NTLM is refused rather than fallen back to: Microsoft deprecated it in 2024, and a client that quietly downgrades to it is doing the opposite of what it was deployed for.
**Crash reports now get through on a Kerberos-only proxy.** They used to be the one carve-out on this page: the previous crash reporter built its own HTTP client inside its own crate, that client took a proxy *URL* and nothing else, and there was nowhere to hand it a SPNEGO token. Crash reports are now built and sent by the client itself, through the same egress path as everything else, so they take the same Negotiate transport, the same CA bundle and the same bypass rules. The limitation is gone rather than relocated.
The blocking status probe is unchanged and still degrades — `openlatch status`'s cloud check falls back on a Kerberos-only proxy, and `openlatch doctor` still reports through the async path.
## Egress requirements
What to give a network team. Everything but the first row is optional and has a switch that turns it off.
<!-- CANONICAL COPY. This table is the ONE sanctioned duplicate of the "Customer
egress requirements table" in the Proxy Support PRD's frozen contract. On any
divergence the PRD wins — change it there first, then mirror it here. Do not
edit a cell to match observed behaviour. -->
| FQDN | Port | Purpose | Required | Disable switch | TLS inspection stance |
| --- | --- | --- | --- | --- | --- |
| `app.openlatch.ai` | 443 | Policy bundle, event delivery, auth | **Required** | — | Works when the inspection CA is OS-trusted or in `ca_bundle` |
| `api.anthropic.com` | 443 | Model-relay upstream (the agent's own traffic) | Required when model relay enabled | `[model_relay] enabled = false` | **Exempt from response buffering / SSE scanning** — buffered streams break agent responses (OL-1228) |
| `api.openai.com` | 443 | Model-relay upstream for **Codex CLI** on an API-key plan (`openai-responses`) | Required when model relay enabled **and Codex is installed** | `[model_relay] enabled = false` | **Exempt from response buffering / SSE scanning** — same reason as the Anthropic row (OL-1228) |
| `chatgpt.com` | 443 | Model-relay upstream for **Codex CLI** on a **ChatGPT subscription** — the client forwards to `/backend-api/codex`, a different origin the API-key row does not cover | Required when model relay enabled **and Codex signs in with a ChatGPT plan** | `[model_relay] enabled = false` | **Exempt from response buffering / SSE scanning** (OL-1228) |
| `registry.npmjs.org` (or `OPENLATCH_NPM_REGISTRY`) | 443 | Update check + binary download | Optional | `[update]` config | Works with OS-trusted CA |
| PostHog host (`OPENLATCH_POSTHOG_HOST`) | 443 | Product telemetry incl. `proxy_configured`, **and crash reports** | Optional | Two independent opt-outs, same host: `openlatch system telemetry` for product telemetry, `OPENLATCH_CRASH_REPORTING=0` (or `[crashreport] enabled = false`) for crash reports | Works |
| OpenDNS / Google DNS | UDP 53 | Public-IP detection (enrichment) | Optional | Existing enrichment config | N/A — not HTTP, never proxied |
| `github.com` + release CDN | 443 | npm `postinstall` binary fallback (installer only) | Optional | Pre-install the platform package | npm/curl proxy config applies — outside the client |
**Codex opens two possible upstreams, not one, and which one is live depends on how the developer signed in.** `codex login` with an API key routes to `api.openai.com`; a ChatGPT subscription routes to `chatgpt.com/backend-api/codex`. A developer can switch between them at any time without touching OpenLatch, so an estate running Codex should open both rather than infer one from today's login state.
Process name for proxy-vendor steering (Netskope/Zscaler custom app definitions): `openlatch` / `openlatch.exe`.
## Troubleshooting
`openlatch doctor` and `openlatch system proxy test` both stop at the failing hop and print one of these. The remedy is the code's, not a guess.
| Code | What happened | Remedy |
| ---- | ------------- | ------ |
| `OL-1220` | No proxy is configured and the direct connection to the platform failed at the TCP/DNS level — egress never had a proxy hop to blame | Check outbound DNS resolution and firewall rules for the host running the daemon; or `openlatch system proxy set <url>` |
| `OL-1221` | A proxy is configured but did not answer: the TCP connect failed, or the `CONNECT` timed out or was refused | Verify the proxy host and port are correct and reachable from this machine before suspecting the destination |
| `OL-1222` | The proxy answered `407` and either rejected the credentials or had none offered. Distinct from `OL-1201` — that is the platform rejecting our API key; this is the proxy's own gate | Reconfirm the **proxy** credential specifically: `openlatch system proxy set <url>`, or check the Kerberos ticket with `klist` |
| `OL-1223` | The proxy URL names a scheme this build cannot speak — anything outside `http`/`https`/`socks5`/`socks5h`, or `negotiate` on a build without the Kerberos feature | Fix the scheme in the proxy URL, or use a build with the feature that scheme requires |
| `OL-1224` | The TLS handshake failed certificate verification. On an enterprise network this is almost always an inspecting proxy re-signing with a CA that is neither in the OS store nor in `ca_bundle` | Point `ca_bundle` at the interception root, or install it in the OS trust store. Never disable verification |
| `OL-1225` | A PAC or WPAD source is configured but the OS could not evaluate it — the PAC URL did not resolve or serve a script, or the platform has no PAC engine | Fix `pac_url`; on Linux, set `[proxy] url` instead. Informational unless the fallback beneath it also fails |
| `OL-1226` | The proxy configuration itself is malformed. Unlike every other code here it **fails startup**: a config the client cannot parse is an authoring bug, not a network state | The message names the exact key or variable — fix that one |
| `OL-1227` | `allow_direct = false` and no usable proxy was found. Egress refuses rather than falling back silently, because on such a network a silent direct connection is an exfiltration path | Fix the proxy configuration, or set `allow_direct = true` if direct egress from this host is acceptable |
| `OL-1228` | The tunnel works but the proxy buffers the stream: an SSE response piles up behind its buffer and lands in one burst. The request still succeeds, so this is a warning | Look for a response-buffering or content-inspection setting on the proxy for the streaming endpoint, and exempt `api.anthropic.com` |
| `OL-1229` | `negotiate` was selected but no usable ticket could be minted — no credential cache, an expired TGT, or an SPN that does not resolve. The proxy was never offered a token at all | Host-side: run `kinit`, confirm the machine is domain-joined, or override the SPN with `--proxy-spn` |
| `OL-1302` | The route in `config.toml` is not the route the running daemon loaded. `system proxy set` writes the file; the live process keeps its old route until it restarts — so the proxy you just configured is carrying nothing yet. Reported as a warning, never a pass | `openlatch restart`. If the two still differ afterwards it is not a pending edit but an environment split — on Windows a service account resolves different variables from an interactive shell |