Installing a local HTTPS proxy certificate on macOS for dev tools

The five-minute keychain step that every local proxy setup guide skips.

If a coding agent CLI starts throwing "unable to verify the first certificate" or "self signed certificate in certificate chain" right after you point it at a local proxy, the fix is almost always the same: your proxy's root CA certificate exists on disk but macOS hasn't been told to trust it. Open Keychain Access, drag the .pem or .crt file into the System keychain, double-click it, expand Trust, and set "When using this certificate" to Always Trust. That single toggle is what most setup guides gloss over, and it's the whole reason the request is failing.

Why a local proxy needs its own certificate at all

Any tool that sits between your CLI and the model provider's API has to see the plaintext request to route it, cache it, or log it. Since the provider's endpoint is HTTPS, the proxy terminates the real TLS connection, re-signs the traffic with its own certificate, and re-encrypts it to your machine. That's a textbook man-in-the-middle pattern, which is exactly what it should be when you're the one running the proxy. The problem is that your OS and every HTTP client on it ships with a fixed list of certificate authorities it already trusts, and your proxy's self-generated CA isn't on that list. Until you add it, every TLS handshake looks like tampering, and the client correctly refuses to proceed.

This is different from a normal "trust this website" prompt in a browser. A coding agent CLI is a non-interactive process making requests over raw TLS, usually through Node's or Python's own certificate handling rather than the browser sandbox, so there's no click-through dialog to bail you out. You have to install trust at the OS level, and in some cases separately at the runtime level.

Installing the certificate via Keychain Access

  • Locate the CA file your proxy generated. It's usually a .pem or .crt in the proxy's config directory, often printed to the terminal on first run with the exact path.
  • Open Keychain Access (Spotlight, then type Keychain Access).
  • In the sidebar, select System, not login. A CA that trusts requests for every process on the machine needs to live in the System keychain.
  • Drag the certificate file into the System list, or use File > Import Items.
  • You'll be prompted for your admin password, since modifying System trust settings requires it.
  • Find the new certificate in the list, double-click it to expand the trust panel.
  • Under Trust, set "When using this certificate" to Always Trust, then close the panel. macOS asks for your password again to confirm.

If you'd rather skip the GUI, the same result comes from one command in Terminal, run with the full path to your certificate file:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /path/to/proxy-ca.pem

That command adds the cert to the System keychain and marks it as a trusted root in one step, which is faster if you're scripting a setup or reinstalling on a new machine. Either path produces the same trust state, so pick whichever you'd actually run twice.

When keychain trust isn't enough: runtime-specific trust stores

Most CLIs built on Node.js or Python don't fully defer to the macOS keychain. Node's TLS module has historically shipped its own bundled CA list and ignored the system store unless told otherwise, and some Python HTTP clients (requests, httpx with certifi) bundle their own certificate file too. If you've completed the keychain step above and one specific tool still fails while everything else works, it's very likely using a private trust store. The fix is usually an environment variable that points that specific runtime at your CA file:

  • NODE_EXTRA_CA_CERTS=/path/to/proxy-ca.pem — tells Node to add your CA on top of its bundled list, without replacing it.
  • SSL_CERT_FILE=/path/to/proxy-ca.pem — the general OpenSSL-level variable, picked up by many Python and Go tools.
  • REQUESTS_CA_BUNDLE=/path/to/proxy-ca.pem — specifically for Python's requests library and things built on it.

Set the one relevant to your failing tool in your shell profile so it survives new terminal sessions, then open a fresh shell before retesting. A stale shell with the old environment is the most common reason someone swears the fix "didn't work."

Troubleshooting checklist

  • Confirm it's actually the System keychain, not login. A CA trusted only in your login keychain won't apply to background processes or other users.
  • Re-check the Trust panel after import. Adding a cert without setting Always Trust leaves it present but not trusted, which produces the exact same error as not installing it at all.
  • Restart the terminal and the CLI process. Some tools cache the certificate chain at startup.
  • If only one tool fails, suspect a bundled trust store and reach for the environment variable route above rather than re-doing the keychain step.
  • Check the proxy is actually running on the port your CLI is configured to hit. A refused connection produces a different error than a trust failure, and it's easy to misdiagnose one as the other at 11pm.

Where this fits if you're running Probe0

I built Probe0 as a local proxy for coding agent CLIs, and the certificate step above is the one-time setup it asks for. Install the proxy, trust the certificate once with the steps in this post, and every agent on the machine (Claude Code, Codex, Cursor) routes through it without touching each tool's individual config. There's no server on the other end reading your traffic; the proxy runs on your machine and the MITM pattern exists only so it can apply caching, local model routing, and spend limits before a request leaves for the cloud.

Once trust is set up, the proxy can do the things a plain pass-through can't: route eligible requests to a model already running in Ollama or LM Studio and retry weak answers on the cloud automatically, try a cheaper model first, serve exact and semantic cache hits from local storage, collapse duplicate simultaneous calls into one upstream request, and hold a hard spend cap per day or per run. The semantic cache specifically refuses to match anything carrying tool calls, since a wrong hit there doesn't just waste a lookup, it corrupts the run. None of that requires touching Claude Code's, Codex's, or Cursor's own settings, because the interception already happened at the certificate layer you just installed.

It's macOS-only right now and built for one developer's machine, not a shared team gateway, so if you need multi-user routing or a hosted control plane, a proxy like LiteLLM or a managed gateway is the better fit. If what you actually want is your own laptop's agent traffic caught, cached, and capped without editing three different config files, the certificate step above is the only setup cost.

Related