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:
| Variable | Reader |
|---|---|
SSL_CERT_FILE | OpenSSL CLI, Python ssl |
REQUESTS_CA_BUNDLE | Python requests, httpx |
NODE_EXTRA_CA_CERTS | Node.js — Claude Code, Cursor, Windsurf |
GIT_SSL_CAINFO | git (Homebrew OpenSSL build) |
CURL_CA_BUNDLE | curl (in addition to system trust) |
AWS_CA_BUNDLE | AWS SDK / boto3 |
HTTPS_PROXY | every 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:
$ 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:
$ 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. Ifgit pushfails withunable to get local issuer certificate, open a new terminal, or re-runhalton-meter initto refresh the rc block. - AWS SDK / boto3 — reads
AWS_CA_BUNDLE. Written by the daemon. - Go SDKs — Go’s
crypto/tlsreads system trust on macOS via Security.framework.--fullmode → trusted via the system keychain.--appsand env-only modes will not see the CA from a Go binary unless you point the binary at the certifi bundle manually (SSL_CERT_FILEis honoured by some Go libraries, not all). - Java — uses its own
cacertstruststore 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:
$ 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:
# 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.