Operations · 02 macOS only

How clients see your CA

Each LLM client trusts certificates differently. This page is the per-client matrix for the Halton Meter mitmproxy CA.

macOS 12+ · Python 3.11+ Reading time 2 min Updated May 11, 2026

Halton Meter terminates HTTPS at a local mitmproxy and re-signs traffic with a self-generated CA at ~/.mitmproxy/mitmproxy-ca-cert.pem. For your tools to accept that certificate, each one has to find it — and every client looks in a slightly different place. This page is the per-client matrix.

If you’re not sure which client is failing, run halton-meter doctor first. It walks every layer (cert generation, keychain trust, certifi patch, env-var injection, system proxy state) and prints the layer that’s wrong.


The short version

The daemon writes seven CA-related env vars (in --apps and --full modes; in env-only mode they are present only inside halton-meter run). Different clients read different vars:

VariableReader
SSL_CERT_FILEOpenSSL CLI, Python ssl
REQUESTS_CA_BUNDLEPython requests, httpx
NODE_EXTRA_CA_CERTSNode.js — Claude Code, Cursor, Windsurf
GIT_SSL_CAINFOgit (Homebrew OpenSSL build)
CURL_CA_BUNDLEcurl (in addition to system trust)
AWS_CA_BUNDLEAWS SDK / boto3
HTTPS_PROXYevery above tool — points at 127.0.0.1:8081

The CA bundle pointed to by every var is the running Python’s certifi bundle, which halton-meter init patches by appending the mitmproxy CA. The patch is detected via a marker line; re-running init is idempotent.


Per-client matrix

curl

Reads: CURL_CA_BUNDLE env var, then macOS keychain (via Security.framework on macOS), then its own bundled cacert.pem.

--apps mode: the daemon writes CURL_CA_BUNDLE pointing at the patched certifi bundle. curl finds the CA on first request.

--full mode: same as --apps, plus the system proxy is enabled. curl traffic is redirected to 127.0.0.1:8081 and intercepted there.

env-only mode: CURL_CA_BUNDLE is set only inside halton-meter run; a bare curl invocation will not see the var. Wrap the call:

~ — curl under env-only
$ halton-meter run -- curl https://api.anthropic.com/v1/messages \
  -H 'x-api-key: $ANTHROPIC_API_KEY' \
  -d '{"model":"claude-haiku-4-5","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'

If curl fails with SSL certificate problem: unable to get local issuer certificate, your CURL_CA_BUNDLE is unset or stale. Open a new terminal so the rc block sources, or fall back to --cacert ~/.halton-meter/ca.pem.


Python — requests, httpx, urllib3

Reads: REQUESTS_CA_BUNDLE (requests, httpx), SSL_CERT_FILE (everything that reaches the underlying ssl module).

Both vars point at the same patched certifi bundle. This is on purpose: import certifi; print(certifi.where()) is the canonical “where does Python look?” check, and the daemon patches that exact file so even SDKs that ignore env vars and call certifi.where() directly (the Anthropic SDK does this) still work.

Verifying:

~ — verify Python trust
$ python -c 'import certifi; print(certifi.where())'
  /Users/you/.local/pipx/venvs/halton-meter/lib/python3.12/site-packages/certifi/cacert.pem
$ halton-meter run -- python -c 'import requests; print(requests.get(...).status_code)'
200

If certifi.where() returns a path the daemon never patched (e.g. a separate venv’s bundle), re-run halton-meter init from that interpreter. The patch is per-Python.


Node.js — Claude Code, Cursor, Windsurf, the Anthropic SDK for Node

Reads: NODE_EXTRA_CA_CERTS env var. Node merges that file’s PEMs into its built-in trust store at startup.

--apps mode: the daemon writes NODE_EXTRA_CA_CERTS to the launchctl user domain and to your shell rc. Spotlight/Dock-launched apps inherit it via launchctl; terminal-launched node processes pick it up from the rc on a new shell.

Restart required. Node reads the var once at startup. After halton-meter init --apps, quit and reopen Claude Code, Cursor, Windsurf, etc. — a cmd-R reload is not enough.

env-only mode: wrap the launch with halton-meter run:

halton-meter run claude

Browsers — Chrome, Safari, Firefox

Read: the operating system keychain (Chrome, Safari) or their own NSS database (Firefox). SSL_CERT_FILE and friends are ignored.

--full mode is the only mode where browser traffic is metered. The daemon writes the CA into the macOS System keychain via security add-trusted-cert so Chrome and Safari accept it.

--apps mode intentionally does not enable the system proxy. Browsers route directly; the CA is never used; HSTS-pinned domains keep working.

CT pinning caveat. Even in --full mode, Chrome enforces Certificate Transparency on a small set of HSTS-pinned domains and may reject the (un-CT-logged) mitmproxy CA. If you see NET::ERR_CERT_AUTHORITY_INVALID on claude.ai, fall back to --apps. See Browser sites break in troubleshooting.

Firefox. Uses its own NSS truststore; --full mode does not populate it. Manually import the cert at about:preferences#privacy → View Certificates → Authorities, or accept that Firefox traffic will not be metered.


git, AWS SDK, Go SDKs, Java

  • git — Homebrew git linked against OpenSSL reads GIT_SSL_CAINFO. The daemon writes it. If git push fails with unable to get local issuer certificate, open a new terminal, or re-run halton-meter init to refresh the rc block.
  • AWS SDK / boto3 — reads AWS_CA_BUNDLE. Written by the daemon.
  • Go SDKs — Go’s crypto/tls reads system trust on macOS via Security.framework. --full mode → trusted via the system keychain. --apps and env-only modes will not see the CA from a Go binary unless you point the binary at the certifi bundle manually (SSL_CERT_FILE is honoured by some Go libraries, not all).
  • Java — uses its own cacerts truststore at $JAVA_HOME/lib/security/cacerts. The daemon does not patch this today. If you need to meter a JVM client, import the CA manually:
~ — Java truststore (manual)
$ sudo keytool -import -trustcacerts \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -alias halton-meter-mitmproxy \
  -file ~/.mitmproxy/mitmproxy-ca-cert.pem

JVM coverage is not on the daemon’s roadmap; if it’s a blocker, file an issue.


Verifying trust

Five one-liners that exercise each layer:

~ — verify trust per layer
# 1. macOS keychain trust (full mode only)
$ security verify-cert -c ~/.mitmproxy/mitmproxy-ca-cert.pem -p ssl
# 2. Python certifi path
$ python -c 'import certifi; print(certifi.where())'
# 3. Python requests handshake
$ halton-meter run -- python -c 'import requests; print(requests.get(...).status_code)'
# 4. curl handshake
$ halton-meter run -- curl -sI https://api.anthropic.com | head -1
# 5. Node handshake (Claude Code / Cursor / Windsurf use this path)
$ halton-meter run -- node -e 'require("https").get("https://api.anthropic.com", r => console.log(r.statusCode))'

Each one prints a 200-class response on success. Any failure tells you which layer is misconfigured: a 1 from security verify-cert means keychain trust is missing (re-run halton-meter init --full); a Python SSL error means certifi is unpatched (re-run halton-meter init from the same interpreter); a Node unable to verify the first certificate error means NODE_EXTRA_CA_CERTS isn’t in that process’s environment (quit and relaunch the parent app, or wrap with halton-meter run).


What’s next

If the matrix didn’t catch your client, walk the broader debugging tree in the troubleshooting guide — it covers cert errors, daemon loops, missing captures, and per-tool edge cases.