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.


Install fails with “Could not find a suitable TLS CA certificate bundle”

Symptom: pipx install halton-meter / uv tool install halton-meter / uvx halton-meter fails with one of:

ERROR: Could not find a suitable TLS CA certificate bundle
SSL: CERTIFICATE_VERIFY_FAILED
ssl.SSLError: [SSL] ... CA cert missing

Cause: A previous Halton Meter (or any mitm tool) left CA env vars exported in your shell pointing at a certifi bundle inside a deleted virtualenv. The installer can’t reach PyPI through them.

Fix: Unset the env vars in the current shell, then retry:

~ — unset stale CA env vars and retry install
$ unset CURL_CA_BUNDLE SSL_CERT_FILE REQUESTS_CA_BUNDLE NODE_EXTRA_CA_CERTS \
              GIT_SSL_CAINFO AWS_CA_BUNDLE HTTPS_PROXY HTTP_PROXY NO_PROXY
$ uvx halton-meter --version       # or your install command of choice

If uvx still serves a stale wheel, force a fresh fetch:

uv cache clean halton-meter && uvx halton-meter --version

halton-meter cloud status shows PAUSED — what to do

As of v0.2.8, pause classification is strict. The recovery depends on the classification — guessing wrong (and re-running cloud connect instead of cloud resume) revokes a still-valid key.

paused_reasonWhat it meansRecover with
paused_unauthorisedA real HTTP 401 on /v1/requests/batch. The token is genuinely invalid.halton-meter cloud resume. If resume reports the token is still bad, then cloud connect to re-pair.
paused_forbiddenHTTP 403 — the workspace owner removed this machine.Ask the workspace owner to re-invite this machine, then halton-meter cloud resume. Do not cloud connect — that revokes the still-valid existing key.
paused_manualYou ran halton-meter cloud pause.halton-meter cloud resume clears it.
last_error set, no pauseA transient error (5xx, timeout, DNS, 429, 422/400). Sync keeps retrying.No action needed. Check ~/.halton-meter/daemon.err.log if it persists.
~ — recover from PAUSED
$ halton-meter cloud status --json      # confirm paused_reason
$ halton-meter cloud resume             # whoami probe; clears pause if 200

resume calls GET /v1/daemon/whoami once with the stored token. On 200 it clears the pause, counters, and last_error. On 401 it reports the key is genuinely revoked — at which point cloud connect is the right next step.


Daemon exits immediately at startup with “loopback bind guard”

Symptom: halton-meter start returns to the prompt without an error message; halton-meter status shows the daemon is not running; daemon log contains:

SystemExit(1) — listen_host=0.0.0.0 is not a loopback address (refusing to bind)

Cause: v0.2.9 added a hard guard so the daemon’s mitmproxy and FastAPI ports cannot bind to non-loopback addresses. The check fires on every boot. It catches accidental 0.0.0.0 / [::] config — which would expose captured request bodies to your LAN.

Fix (intended config): edit ~/.halton-meter/config.toml and set listen_host = "127.0.0.1" and api_host = "127.0.0.1". Then halton-meter start.

Override (container or VPN topology that legitimately needs non-loopback): set the environment variable before start and accept the warning logged on every boot.

~ — override loopback guard
$ HALTON_METER_ALLOW_NON_LOOPBACK=1 halton-meter start
  bind-guard override active — daemon is reachable from non-loopback addresses

Persist the override across reboots by adding HALTON_METER_ALLOW_NON_LOOPBACK=1 to the supervisor’s EnvironmentVariables (launchd plist on macOS, systemd unit on Linux, Task Scheduler XML on Windows).


cloud.transport_error opaque or stuck retrying — placeholder base_url

Symptom: halton-meter cloud status shows last_error: cloud.transport_error on every retry; the daemon never reaches the backend.

Cause: [cloud].base_url in ~/.halton-meter/config.toml contains a placeholder TLD (.test, .example, .invalid, .local). These never resolve; the retry loop burns through its budget on every cycle.

v0.2.5+: placeholder TLDs now raise immediately instead of waiting for the full timeout — the error surfaces the base URL and the config file path inline, so the cause is visible from cloud status.

v0.2.7+: the daemon auto-heals placeholder TLDs on next boot, rewriting base_url to https://api.haltonmeter.com and logging the rewrite once. You don’t need to do anything; restart the daemon:

halton-meter stop && halton-meter start

For older daemons (pre-v0.2.7), edit ~/.halton-meter/config.toml manually and set base_url = "https://api.haltonmeter.com".


Body capture skipped — “addon.body_capture.skip reason=body_too_large”

Symptom: ~/.halton-meter/daemon.out.log contains entries like addon.body_capture.skip reason=body_too_large. The request still shows up in halton-meter report (metadata captured: model, tokens, cost) but the request body and response body are not stored locally.

Cause: v0.2.10 added a 4 MiB size gate on body capture to prevent memory spikes from large multi-modal payloads (PDF uploads, image batches, streaming Vision responses). Bodies above the gate are dropped at the proxy boundary; metadata still captures.

Not a problem. This is the intended behaviour. Cost and attribution are correct; only the raw request/response bytes are missing. If you need to inspect the body for a specific call, capture it through a separate debugging proxy (mitmweb) rather than raising the gate.

To audit how often the gate fires:

grep -c 'body_capture.skip' ~/.halton-meter/daemon.out.log

Cursor / VSCode shows “unattributed” on first request after cold start

Symptom: The first request from a freshly-launched Cursor / VS Code / Kiro / Zed / IntelliJ window lands in halton-meter report as unattributed. Subsequent requests in the same window are attributed correctly.

Cause (pre-v0.2.5): The edge process’s per-edge slug → abspath registry was empty on first contact; the resolver’s Tier 4b / 4c looked up a slug that didn’t exist yet.

v0.2.5+ fix: A lenient one-level scan of ~/Documents and ~ warms the registry on Tier 4b / 4c miss. Cold-start rows are now attributed via the recovery path; telemetry shows 4b_lenient or 4c_lenient in attribution.tier_hit.

If you still see this on v0.2.5+: the project root isn’t under ~/Documents or ~. Add a .haltonrc (project: <slug>) to the project root, or set HALTON_PROJECT=<slug> in the IDE’s launch env.

Backfill historical unattributed rows:

~ — dry-run backfill, then apply
# Dry-run — prints what would change without writing
$ python -m halton_meter.scripts.backfill_misc_attribution
$ python -m halton_meter.scripts.backfill_body_paths

# Apply — rewrites rows in place (DB-only; safe to re-run)
$ python -m halton_meter.scripts.backfill_misc_attribution --apply
$ python -m halton_meter.scripts.backfill_body_paths --apply

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