Operations · 01 macOS only

Troubleshooting

Diagnoses and fixes for every known failure mode — cert errors, daemon loops, missing captures, and more.

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

Run halton-meter doctor first — it covers the most common failure modes with copy-pasteable fixes. If doctor doesn’t resolve the issue, email operator@haltonlabs.com with the output of halton-meter doctor --json attached.


Browser sites break (NET::ERR_CERT_AUTHORITY_INVALID)

Symptom: Chrome or Safari shows a certificate error (NET::ERR_CERT_AUTHORITY_INVALID) on sites like claude.ai or any HTTPS site after init.

Cause: You are in --full mode (system proxy enabled). Chrome enforces Certificate Transparency (CT) on HSTS-pinned domains. When traffic is routed through the mitmproxy CA, Chrome may reject the cert — even if macOS’s Keychain trusts it — because the CA cert is self-signed and not CT-logged.

Fix (recommended): Switch to --apps mode, which covers IDEs and terminals without enabling the system proxy:

~ — switch to --apps
$ halton-meter init --apps

After running this, restart your browser. Browser traffic will no longer go through the proxy (no cert errors), and your IDEs will still be metered.

Emergency rollback if browser is broken:

~ — emergency rollback
$ halton-meter reset-proxy      # immediately disables system proxy
$ halton-meter init --apps        # re-init in safe mode

Alternative fix (keep --full, use at your own risk): Manually trust the mitmproxy CA in Chrome:

  1. Open chrome://settings/certificates
  2. Import ~/.mitmproxy/mitmproxy-ca-cert.pem into Authorities
  3. Trust it for “Identifying websites”

This may still fail on CT-pinned domains. The --apps fix is cleaner.


git push fails with SSL certificate error

Symptom: git push (or git fetch, git clone) fails with:

fatal: unable to get local issuer certificate

or:

SSL certificate problem: unable to get local issuer certificate

Cause: Git (especially Homebrew-installed git linked against OpenSSL) reads GIT_SSL_CAINFO to find the CA bundle, not SSL_CERT_FILE. In versions before v0.1.5, GIT_SSL_CAINFO was not included in the injected env vars.

v0.1.5+ fix (permanent): This is already fixed. Re-run init to ensure the shell rc block is up to date:

~ — re-run init
$ halton-meter init --apps      # or whichever mode you use

Open a new terminal. The new rc block includes GIT_SSL_CAINFO, CURL_CA_BUNDLE, and AWS_CA_BUNDLE.

Immediate workaround (before re-init):

GIT_SSL_CAINFO="$SSL_CERT_FILE" git push

Or use halton-meter run which always injects all nine env vars:

halton-meter run git push

Affected tools and their env vars:

ToolEnv var needed
git (OpenSSL)GIT_SSL_CAINFO
curlCURL_CA_BUNDLE
AWS CLI / Boto3AWS_CA_BUNDLE
Python requests / httpxREQUESTS_CA_BUNDLE
Node.jsNODE_EXTRA_CA_CERTS
Python (generic SSL)SSL_CERT_FILE

All six are set by halton-meter run, and all six are exported in the shell rc block for --apps and --full modes.


No captures in halton-meter report

Symptom: halton-meter report shows “No captures yet” even though you’ve made API calls.

Start with a quick health check:

~ — health check
$ halton-meter status           # is the daemon HEALTHY?
$ halton-meter doctor           # any row amber/red?

Cause 1: Daemon is not running.

~ — start daemon
$ halton-meter start
# then retry your API call

Cause 2: env-only mode — you didn’t use halton-meter run.

In env-only mode, only commands launched with halton-meter run <cmd> are metered. A bare claude in your terminal is not captured.

~ — env-only vs apps capture
$ halton-meter run claude       # this is metered
$ claude                        # this is NOT metered in env-only mode

Switch to --apps mode if you want bare terminal invocations to be metered automatically.

Cause 3: --apps mode, but you haven’t opened a new terminal since init.

The shell rc block (.zshrc/.bashrc) is sourced only in new shells. Your current terminal still has the old env. Open a new terminal tab or window. Verify the env is set:

~ — verify env
$ echo $HTTPS_PROXY
# should print: http://127.0.0.1:8081

Cause 4: IDE is Spotlight/Dock-launched but hasn’t been restarted since init.

Windsurf, Cursor, VS Code, and similar Electron/Node IDEs inherit the launchctl user-domain env at launch time. If you opened the IDE before init --apps, it doesn’t have the proxy env.

Fix: quit the IDE completely (Cmd-Q), then relaunch it from Spotlight or the Dock. Do not relaunch from your terminal with code . — that inherits the terminal’s env, not launchctl’s.

To verify the IDE has the env: open the IDE’s built-in terminal and run echo $HTTPS_PROXY. It should print http://127.0.0.1:8081.

Cause 5: --full mode, but Node.js ignores the macOS system proxy.

Node.js (and therefore Claude Code, Cursor, Windsurf, and all Electron apps) does NOT read the macOS system proxy panel. It only respects HTTPS_PROXY env. The system proxy alone captures nothing from these tools. Switch to --apps mode:

halton-meter init --apps

Cause 6: Process is hardened / uses a pinned CA.

Some apps (corporate security tools, hardened binaries) pin their CA bundle and ignore all env vars. These cannot be intercepted without modifying the binary. This is expected and not a bug.


Init self-test fails immediately

Symptom: During halton-meter init, the self-test reports “Connection refused” or fails within the first second.

Cause: The daemon process takes a few seconds to start and bind its port (cold-start can take 6–8 seconds on a slow disk). Old single-shot health checks returned failure before the daemon was ready.

v0.1.5+ fix: The self-test now retries every 500ms for up to 30 seconds. If you’re on a version before v0.1.5, upgrade:

~ — upgrade
$ pipx upgrade halton-meter
$ halton-meter init

If the self-test still times out after 30 seconds:

~ — doctor --curl
$ halton-meter doctor --curl
# Look at the "daemon" and "port" rows

Common additional causes: another process already holds port 8081, 8090, or 8765 (see Port 8081, 8090, or 8765 already in use); disk permissions issue on ~/.halton-meter/.

Check daemon logs:

tail -n 100 ~/.halton-meter/daemon.err.log

Daemon won’t start / keeps respawning

Symptom: halton-meter status shows the daemon is not running, or launchctl list | grep halton shows repeated non-zero exits.

Step 1: Check logs

~ — daemon logs
$ tail -n 100 ~/.halton-meter/daemon.err.log   # structured JSON / console events
$ tail -n 100 ~/.halton-meter/edge.err.log     # edge events

Step 2: Try starting manually to see the error

~ — start and verify
$ halton-meter start
# wait 5 seconds, then:
$ halton-meter status

Step 3: Check for port conflicts

~ — check ports
$ lsof -nP -iTCP:8081 -sTCP:LISTEN     # edge
$ lsof -nP -iTCP:8090 -sTCP:LISTEN     # daemon mitmproxy
$ lsof -nP -iTCP:8765 -sTCP:LISTEN     # FastAPI

If something else is holding 8081, 8090, or 8765, port discovery in daemon/halton_meter/port_alloc.py renegotiates to a nearby fallback and persists the chosen tuple to ~/.halton-meter/effective-ports.json. Pin specific values in ~/.halton-meter/config.toml if you need stable ports.

Step 4: Stale launchctl env (recursive proxy)

If your logs show the daemon is connecting to itself (proxy loop), see Stale launchctl env.

Step 5: Full reset

~ — full reset
$ halton-meter stop
$ halton-meter uninstall
$ halton-meter init --apps      # or your preferred mode

Stale launchctl env causing recursive proxy failure

Symptom: The daemon starts then immediately exits. Logs show connection errors to http://127.0.0.1:8081. halton-meter doctor shows HTTPS_PROXY set in the launchctl domain pointing at the edge port.

Cause: A previous --apps or --full install set HTTPS_PROXY in the launchctl user domain. The daemon spawned by launchd inherited this env var, causing it to try to route its own health check through itself — a proxy loop — leading to immediate failure and exit. launchd then respawned it, creating a loop.

Fix:

~ — clear stale launchctl env
# Clear the stale launchctl env vars
$ launchctl unsetenv HTTPS_PROXY
$ launchctl unsetenv HTTP_PROXY
$ launchctl unsetenv NO_PROXY
$ launchctl unsetenv NODE_EXTRA_CA_CERTS
$ launchctl unsetenv SSL_CERT_FILE
$ launchctl unsetenv REQUESTS_CA_BUNDLE
$ launchctl unsetenv GIT_SSL_CAINFO
$ launchctl unsetenv CURL_CA_BUNDLE
$ launchctl unsetenv AWS_CA_BUNDLE

# Now re-init (env-only mode first to verify clean start)
$ halton-meter stop
$ halton-meter init
$ halton-meter status

mitmproxy CA cert serial warning

Symptom: Warning message during init or in daemon logs:

CryptographyDeprecationWarning: Parsed a negative or zero serial number,
which is disallowed by RFC 5280.

Cause: An old mitmproxy CA cert exists in ~/.mitmproxy/ that was generated by an older version of mitmproxy with a non-positive serial number.

Fix: Delete the old cert directory and let init regenerate it:

~ — regenerate mitmproxy CA
$ halton-meter stop
$ sudo rm -rf ~/.mitmproxy
$ halton-meter init --apps      # or your preferred mode

The admin password dialog will appear again for the new cert trust step.


Port 8081, 8090, or 8765 already in use

Symptom: Init completes but halton-meter status shows the daemon is using a different port (e.g., 8081/8766). Or init fails because it can’t bind.

What happens automatically: Halton Meter renegotiates to nearby fallbacks via port_alloc.py. The chosen tuple is persisted to ~/.halton-meter/effective-ports.json and runtime.toml so every component (edge, watchdog, status, doctor) reads the same source of truth. If everything in range is busy, init fails.

Check which port the daemon is actually on:

~ — check actual port
$ halton-meter status
# Look for "proxy-port" and "api-port" rows

The actual port is saved to ~/.halton-meter/runtime.toml and used by all commands automatically. You don’t need to configure anything — halton-meter run, halton-meter report, etc., all read the runtime config.

If you need port 8081 freed:

~ — free port 8081
$ lsof -nP -iTCP:8081 -sTCP:LISTEN
# Identify the PID, then:
$ kill <PID>
$ halton-meter stop && halton-meter start

Mode sentinel drift (status shows wrong mode)

Symptom: halton-meter status shows a different mode than you expect, or shows install-mode: gui when you think you’re in --full mode.

Cause: The mode sentinel file (~/.halton-meter/install-mode) and the actual system state (system proxy, launchctl env, shell rc) can drift if init was interrupted, or if you’re on a version before v0.1.5’s reconciler.

Fix: Re-run init for the mode you want. v0.1.5+ reconciles all four state surfaces (proxy, launchctl env, shell rc, sentinel) in every direction:

~ — re-init to fix mode
$ halton-meter init --apps      # or your preferred mode

The gui sentinel value is automatically migrated to full on next init. No manual file editing needed.


--non-interactive cannot elevate error

Symptom: Init fails with exit code 2 and a message like:

--non-interactive cannot elevate: osascript unavailable and sudo not pre-cached

Cause: You’re running halton-meter init --non-interactive (e.g., from a script or CI), but neither osascript (the macOS dialog) nor a pre-cached sudo credential is available.

Fix option 1 — run interactively:

halton-meter init --apps

The macOS admin dialog appears. Type your password and click OK.

Fix option 2 — pre-cache sudo then run non-interactively:

~ — pre-cache sudo then init
$ sudo -v                        # enter your password to cache credentials
$ halton-meter init --apps --non-interactive

The sudo -v credential cache lasts for a few minutes (configurable in /etc/sudoers). Run init immediately after.


Cert trust check fails (verify-cert returns non-zero)

Symptom: Init reports cert trust failure. halton-meter doctor shows the cert is not trusted.

What the check does: security verify-cert -c ~/.mitmproxy/mitmproxy-ca-cert.pem -p ssl — a real SecTrust evaluation with no override flags. Non-zero means the cert is genuinely not trusted by macOS.

Cause: You cancelled the macOS password dialog.

The cert was added to Keychain but not trusted. Rerun init and approve the dialog:

halton-meter init --apps

Cause: Keychain entry exists but is untrusted (marked “Never Trust”).

  1. Open Keychain Access (Cmd+Space → “Keychain Access”)
  2. Search for “mitmproxy”
  3. Double-click the cert → expand “Trust” → set “When using this certificate” to “Always Trust”
  4. Close (your password is requested to save)

Then verify:

~ — verify cert trust
$ security verify-cert -c ~/.mitmproxy/mitmproxy-ca-cert.pem -p ssl
$ echo $?    # should print 0

Cause: Old cert in Keychain from a previous install.

The cert was regenerated (e.g., after sudo rm -rf ~/.mitmproxy) but Keychain still has the old one trusted while the new one is untrusted.

Fix: delete the old Keychain entry and rerun init:

~ — delete stale keychain cert
# List all mitmproxy certs
$ security find-certificate -a -c mitmproxy
# Delete stale entries (one at a time if multiple)
$ security delete-certificate -c mitmproxy
# Then re-init
$ halton-meter init --apps

launchd supervisor loop (daemon spawns, exits, repeats)

Symptom: The daemon process repeatedly starts and exits. launchctl list | grep halton shows a non-zero exit count that keeps climbing.

Diagnosis:

~ — diagnose supervisor loop
# Check exit reason
$ launchctl list com.haltonlabs.meter
# Look at LastExitStatus

# Check daemon log
$ cat ~/.halton-meter/daemon.err.log | tail -20

Cause 1: Preflight self-refusal loop (fixed in v0.1.5)

The daemon’s preflight check saw its own launchctl registration and refused to start (returned exit 1), causing launchd to respawn it — infinite loop.

Fix: upgrade to v0.1.5+:

~ — upgrade to v0.1.5+
$ pipx upgrade halton-meter
$ halton-meter uninstall
$ halton-meter init --apps

In v0.1.5+, the daemon self-recognizes via XPC_SERVICE_NAME=com.haltonlabs.meter and skips the preflight check when running as the launchd service.

Cause 2: Stale launchctl HTTPS_PROXY env (proxy loop)

See Stale launchctl env.

Cause 3: Port permanently blocked

If every port in the discovery range (around 8081 / 8090 / 8765) is busy, the daemon fails at bind time and exits. Free one and run halton-meter stop && halton-meter start.


Captured traffic missing after reboot

Symptom: halton-meter report shows rows before the reboot but nothing after, even though the daemon appears healthy.

Most likely cause: The launchd plist is installed but env vars aren’t set in new shells.

After a reboot:

  1. Open a fresh terminal
  2. Check: echo $HTTPS_PROXY
  3. If empty and you’re in --apps mode, source your rc manually: source ~/.zshrc (or ~/.bashrc)
  4. If still empty, re-run halton-meter init --apps — the rc block may have been removed

Less common cause: launchd didn’t start the daemon plist after reboot (happens if the plist was installed while logged in as a different user, or if /Library/LaunchAgents vs ~/Library/LaunchAgents is wrong).

Check:

~ — verify launchd registration
$ launchctl list | grep halton
# Should show com.haltonlabs.meter

If absent:

~ — reinstall launchd plist
$ halton-meter uninstall
$ halton-meter init --apps

Claude Code (terminal-launched) not captured

Symptom: You run claude in a terminal and make API calls, but halton-meter report shows nothing.

ModeExpected behaviourFix
env-onlyNOT captured unless you use halton-meter run claudeUse halton-meter run claude
appsCaptured IF the terminal was opened after initOpen a new terminal; verify echo $HTTPS_PROXY
fullCaptured via env var (same as apps); system proxy doesn’t help Node.jsOpen new terminal; verify env

Quickest fix for any mode:

halton-meter run claude

halton-meter run injects all nine env vars directly into Claude Code’s process at exec time — it always works regardless of mode.


Claude Code (IDE-launched) not captured

Symptom: Claude Code inside Windsurf, Cursor, or VS Code is not metered. halton-meter run claude works fine.

Cause: The IDE was launched before halton-meter init --apps ran, so it doesn’t have HTTPS_PROXY in its env. Or the IDE is launched from a terminal (code .) rather than from Spotlight/Dock, so it inherits the terminal’s pre-init env.

Fix:

  1. Ensure you’ve run halton-meter init --apps (not just init)
  2. Quit the IDE completely: Cmd-Q (not just close the window)
  3. Relaunch from Spotlight (Cmd-Space → type the IDE name) or the Dock — NOT from your terminal
  4. Inside the IDE, open the integrated terminal and run echo $HTTPS_PROXY — it should print http://127.0.0.1:8081
  5. Make an API call, then check halton-meter report

Why Spotlight/Dock vs terminal matters: launchctl setenv (which --apps mode uses) sets env vars in the launchd user domain. Apps launched by launchd (from Spotlight or Dock) inherit this domain. Apps launched from your terminal inherit the terminal’s env — which only has the vars if you’ve opened a new terminal after init.


Full uninstall / clean slate

If you also need a way back from a totally wiped machine, restoring rollups from a synced copy is the Cloud answer; locally the steps below are destructive.

If something is badly broken and you want to start over:

~ — full uninstall and reinstall
# Stop the daemon
$ halton-meter stop

# Remove the install (preserves db.sqlite by default)
$ halton-meter uninstall

# For a completely clean start (also deletes db.sqlite and logs):
$ halton-meter uninstall --purge --include-logs
$ sudo rm -rf ~/.mitmproxy

# Re-install
$ pipx reinstall halton-meter
$ halton-meter init --apps

Command reference

Command reference
# Install / reinstall / switch modes
halton-meter init                          # env-only (default)
halton-meter init --apps                   # apps mode
halton-meter init --full                   # full mode (system proxy)
halton-meter init --apps --no-shell-rc     # apps but skip rc modification
halton-meter init --non-interactive        # no GUI dialog; needs sudo pre-cached

# Run a command with metering env injected
halton-meter run <cmd> [args...]           # exec cmd with all proxy env vars
halton-meter run -- <cmd> --flag           # use -- to pass flags to the command
halton-meter run --shell                   # interactive metered subshell

# Daemon control
halton-meter start                         # start via launchd
halton-meter stop                          # stop
halton-meter stop && halton-meter start    # restart (no dedicated subcommand)

# Status and diagnostics
halton-meter status                        # HEALTHY / INCONSISTENT / BROKEN
halton-meter status --json                 # machine-readable
halton-meter doctor                        # full diagnostic with copy-paste fixes

# Usage data
halton-meter report                        # show captured rows
halton-meter report --json                 # machine-readable

# Cleanup
halton-meter uninstall                     # remove plists, restore proxy state
halton-meter uninstall --purge             # also delete config + sentinels
halton-meter uninstall --purge --include-logs  # also delete db.sqlite
halton-meter reset-proxy                   # emergency: disable system proxy