| name | mitm-tracker |
| description | Inspect, mock, or reproduce HTTP/HTTPS traffic from a macOS app or iOS Simulator using the mitm-tracker CLI. Use when the user asks to capture network requests, list hosts/APIs the app calls, see what an endpoint returns, override a response with a local file (Map Local), reproduce a captured request as curl, debug TLS-decrypted traffic, set up Touch ID for the proxy, configure the menu-bar tray, or diagnose why mitm-tracker isn't working. Trigger phrases include "what APIs is the app calling", "intercept", "mock this response", "see the request body", "record traffic", "override endpoint", "Map Local", "decrypt HTTPS", "set up Touch ID for record", "tray icon", "mitm-tracker doctor", "why is the proxy broken", or any reference to mitmproxy/Charles Proxy in this workflow. |
mitm-tracker
CLI on top of mitmproxy that captures HTTP(S) flows into per-session SQLite, scopes TLS decryption per profile, and serves Map Local mocks. Designed to be operated by an agent.
Mental model (read first)
Six facts that are not obvious from --help:
-
Workspace is the cwd. mitm-tracker resolves .mitm-tracker/ from Path.cwd() with no walk-up. Running query recent from a subdirectory of a project that has a workspace at the root will silently create a fresh empty workspace there. Always cd to the project root that owns the running session before invoking any command. If the user runs a command and gets no record sessions found despite a session being active, this is almost always the cause.
-
record start daemonizes. It spawns mitmdump with start_new_session=True, returns immediately, and the proxy keeps running across terminal closes, ssh disconnects, etc. Only record stop, kill <pid>, or a Mac reboot stops it. After a crash the macOS proxy may stay pointed at 127.0.0.1:8080 with nothing listening — record stop cleans that up.
-
SSL list changes do not hot-reload; Map Local rules do. The SSL list becomes --allow-hosts for mitmdump at startup. Adding/removing a pattern requires record stop && record start to take effect. Map Local edits (rule changes, body file edits, enable/disable) are picked up on the next request automatically.
-
Wildcards do not match the apex. *.example.com matches api.example.com, v1.api.example.com, etc., but not the bare example.com. Add the apex as a separate entry when both are needed.
-
The mitmproxy CA can become a stale orphan. ~/.mitmproxy/mitmproxy-ca-cert.pem is generated on the first mitmdump run and reused forever. If something deletes that file (a manual cleanup, setup uninstall, etc.), the next record start regenerates the CA with a new fingerprint — but the iOS Simulator still trusts the old one. Symptom: TLS suddenly fails inside the app even though cert status says installed. Fix: re-run mitm-tracker cert install. Cross-check with mitm-tracker doctor (shows the current CA's SHA256 in the Runtime state group).
-
Decrypting HTTPS from the Mac host requires a separate, more dangerous step. mitm-tracker cert install only trusts the CA inside iOS Simulators. To intercept Safari / native apps / OS processes you must run mitm-tracker cert host install — this trusts the mitmproxy CA system-wide on the Mac, for ALL TLS connections (App Store, OS updates, everything). Always reverse it with mitm-tracker cert host uninstall when done. The matching private key at ~/.mitmproxy/mitmproxy-ca.pem becomes a high-value file while host trust is active. The command auto-replaces stale managed CAs (handles the regen scenario from #5), only removes what we installed at uninstall time, and runs security verify-cert -p ssl to confirm the trust setting actually took effect.
When to invoke this skill
- "What endpoints is this app hitting?" →
query hosts, query recent
- "Why is this request failing / slow?" →
query failures, query slow, query show <seq>
- "Reproduce this request outside the app" →
query curl <seq>
- "Make
/api/users return an empty list" → maplocal from-flow <seq> then edit the body
- "Capture network traffic from the simulator" →
record start
- "Capture traffic from a real iPhone / physical device" →
device start (Workflow A2)
- "I added a new SSL host but I don't see it decrypted" → restart record (gotcha #3)
Workflow A — first-time setup in a new project
cd /path/to/project-root
mitm-tracker doctor
mitm-tracker setup install
mitm-tracker doctor
mitm-tracker cert install
mitm-tracker profile create <name> --use
mitm-tracker ssl add "*.api.example.com"
mitm-tracker ssl add "auth.example.com"
mitm-tracker record start
The simulator inherits the macOS system proxy automatically — no manual proxy config inside iOS Settings.
setup install is idempotent and cheap to re-run; if both Touch ID and sudo cache are already configured, it returns without invoking sudo. After running it once, subsequent record start/record stop typically share a single fingerprint tap (sudo cache is scoped to networksetup for 60 min).
Workflow A2 — capture from a physical iOS device
For a real iPhone/iPad (not the simulator), use device start instead of record start. There are two transports:
--transport wireguard (recommended) — captures every app, including native apps and QUIC/HTTP3. The Mac runs a WireGuard server; the device connects with the WireGuard app and all its traffic is routed through mitm-tracker. This is the only way to capture apps that ignore the Wi-Fi proxy (most native apps do — same approach Charles/Proxyman iOS use).
--transport wifi-proxy (default) — Safari and proxy-aware apps only. Simpler (no extra app), but native apps using their own networking or QUIC won't appear.
cd /path/to/project-root
mitm-tracker ssl add "*.api.example.com"
mitm-tracker device start --transport wireguard
device start differs from record start: it binds to the LAN so the phone can reach it, and it does not change this Mac's system proxy (no sudo/networksetup prompt — the phone is the client). Proxy and help page are daemonized and tied together: device start returns immediately, and device stop (or record stop, or the tray Stop button) tears down both. State persists help_pid/help_port/lan_ip/proxy_mode in state.json. The WireGuard key material lives in runtime/wireguard.conf (reused across runs).
The on-device steps cannot be automated; the user does them by hand from the setup page (which is laid out as Step 1 — Connect, with a tab per transport, and Step 2 — Trust the certificate):
- WireGuard: install the WireGuard app, scan the QR (Add a tunnel → from QR code), turn the tunnel ON, allow the VPN. (Or for wifi-proxy: Settings → Wi-Fi → Configure Proxy → Manual → the printed IP/port — tap Copy on the page for the values.)
- Download the profile from the page → Settings → General → VPN & Device Management → install the mitm-tracker CA.
- Settings → General → About → Certificate Trust Settings → enable FULL TRUST for mitmproxy. This is the #1 cause of "app shows connection error / nothing decrypts": the profile is installed but full trust is OFF, so every TLS handshake fails. iOS never auto-trusts a manually installed root for SSL.
The help page also renders best-effort prefs:root=… Settings deeplinks, but Apple blocks those from Safari on iOS 17+, so treat the written steps as authoritative. Use mitm-tracker doctor to confirm the LAN address, that the proxy is LAN-bound, and that the macOS firewall is not blocking the incoming connection.
Workflow B — inspect a running capture
cd /path/to/project-root
mitm-tracker record status --json
mitm-tracker query recent --limit 20 --json
mitm-tracker query hosts --json
mitm-tracker query failures --json
mitm-tracker query show 42 --json
If query recent returns count: 0 but record status shows captured_count > 0, retry — there is a small SQLite flush lag.
Workflow C — mock a response (Map Local)
mitm-tracker query recent --host api.example.com --limit 20
mitm-tracker maplocal from-flow 42 --description "force empty list"
$EDITOR <body_path>
mitm-tracker maplocal list --json
mitm-tracker maplocal disable <id>
mitm-tracker maplocal enable <id>
mitm-tracker maplocal remove <id>
The addon strips length-dependent headers (Content-Length, ETag, Last-Modified, Transfer-Encoding, Content-Encoding) on synthesized responses, so editing the body never desyncs the wire format. Cache headers are also rewritten so the client cannot serve a stale copy.
Workflow D — reproduce a captured request
mitm-tracker query curl 42
mitm-tracker query curl 42 --single-line
mitm-tracker query curl 42 --body-dir /tmp
Header case is preserved exactly; HTTP version is explicit. Run as-is in another terminal to hit the real server.
Workflow E — clean up
mitm-tracker record stop
mitm-tracker release --older-than 24h --dry-run
mitm-tracker release --older-than 7d
The active session and any session whose mitmdump is still running are protected.
Workflow F — continuous visual status (optional)
pipx install -e ".[tray]"
cd /path/to/the/app/repo
mitm-tracker tray install
After install, the icon appears immediately and on every subsequent login. Icon: 🟢 running, 🔴 stopped, 🟡 zombie (PID dead but state says running). Menu shows active profile + SSL host count, workspace path, and Start/Stop record actions. Useful for spotting the zombie-state failure mode without polling record status manually.
When the running session is a physical-device session (device start), the status line shows the reachable LAN address (e.g. Running (device LAN 192.168.1.44:<port>) — 8080 for wifi-proxy, 51820 for wireguard) and two extra menu items appear: Copy device setup link (copies http://<ip>:<help-port>/ via pbcopy) and Open device setup page (opens it in the Mac browser). They are disabled for ordinary loopback record sessions.
Click "Quit tray" to exit cleanly: if a record session is RUNNING or CRASHED, the tray runs record stop first (one Touch ID tap, restores system proxy, kills mitmdump) before tearing itself down. Plain "Stop record" stops the daemon but keeps the tray alive. The tray sets NSApplicationActivationPolicyAccessory at runtime so it doesn't appear in the Dock or Cmd-Tab.
Other actions:
mitm-tracker tray status — print install/load state, current PID, watched workspace
mitm-tracker tray uninstall — remove the LaunchAgent (disables auto-launch)
mitm-tracker tray run — open foreground without registering a LaunchAgent
- To switch the watched workspace:
cd <new-repo> && mitm-tracker tray install (replaces the existing plist)
Command reference
| Command | Purpose | Key flags |
|---|
profile {create,use,list,show,delete} | Profile lifecycle (each has own SSL list + Map Local rules) | --use on create activates immediately |
ssl {add,remove,list} | Manage TLS-decryption hosts for a profile | --profile <name> to target a non-active profile |
maplocal {add,from-flow,list,show,edit,enable,disable,remove} | Local response overrides | from-flow <seq> clones a captured response |
cert {install,status,simulators} | Install mitmproxy CA into booted simulator(s) | iOS 26 uses TrustStore.sqlite3 (sha256); legacy keychain (sha1) still supported |
cert host {install,uninstall,status} | Trust the mitmproxy CA system-wide on the Mac (DANGEROUS) | Run --yes to skip the confirmation banner, --force to re-run when previous attempt left trust missing. Always reverse with cert host uninstall |
record {start,stop,status,logs} | Capture session lifecycle | --keep-cache to disable the default cache-stripping; --port N to override 8080 |
device {start,status,stop} | Physical iOS device: route over the LAN + serve cert/setup page | --transport wireguard (every app, incl. QUIC; device needs the WireGuard app) or wifi-proxy (Safari/proxy-aware only). Binds to 0.0.0.0, leaves the Mac's own proxy untouched. Daemonizes proxy + setup page; device stop/record stop/tray stop both. --help-port N (8888), --wireguard-port N (51820). Same session/state as record |
query {recent,failures,slow,hosts,show,sql,curl,sessions,use} | Inspect captured flows | --json on every subcommand; query use <session> switches active DB |
release [--older-than 24h] [--dry-run] | Delete stale capture databases | --no-keep-active to allow deleting the active one |
tray {run,install,uninstall,status} | macOS menu bar indicator (🟢/🔴/🟡); requires [tray] extra | tray install registers a LaunchAgent for auto-launch on login; tray run is foreground only |
setup {install,uninstall,status} | One-shot configurator: tray + Touch ID + 60min sudo cache scoped to networksetup | macOS Sonoma+ only. Idempotent. --skip-touch-id/--skip-sudo-cache/--skip-tray for granularity |
doctor | Health check across system, tools, setup state, runtime | Use to diagnose "why doesn't X work" before guessing. Prints fix: hints for each non-OK check; exit 0/2/3 by severity |
Every subcommand accepts --json. Exit codes: 0 success, 1 usage error, 2 invalid state, 3 system failure.
SQL escape hatch
query sql runs read-only SELECTs against the flows table. Useful for token-efficient custom aggregations — only the columns you need:
mitm-tracker query sql "SELECT host, AVG(duration_total_ms) AS avg_ms, COUNT(*) AS n FROM flows GROUP BY host ORDER BY n DESC"
mitm-tracker query sql "SELECT seq, method, host, path, response_status_code FROM flows WHERE response_status_code >= 400"
Useful columns: seq, method, host, path, response_status_code, duration_total_ms, tls_decrypted, error_msg, request_body_raw, response_body_raw. The *_body_raw columns can be huge; avoid SELECT * when you don't need them.
When something is broken
Run mitm-tracker doctor first. It tells you exactly what's missing and gives the command to fix it. Beats guessing.
Gotchas checklist
no record sessions found → wrong cwd. cd to the project root that owns the workspace.
- HTTPS host shows up as
CONNECT only, no path/body → host not in the active profile's SSL list, or SSL list was edited without restarting record.
- App still serves cached responses after a Map Local change → ensure record was started without
--keep-cache (the default strips cache). Force-quitting the app is rarely needed because the addon neutralizes Cache-Control/ETag/Last-Modified/conditional headers.
record status shows running: false, crashed: true → daemon died but proxy state is dirty. Run record stop to clean up before record start.
query show <seq> --json is huge → it dumps request_body_raw and response_body_raw. Prefer query sql selecting only the columns you need, or pipe through jq to drop the body fields.
- Apex domain not matched →
*.example.com does not cover example.com. Add both.
- Cert install on a non-booted simulator →
cert install only targets booted devices. Boot the simulator first (xcrun simctl boot <udid>).
- App's TLS suddenly broke after a cleanup → the mitmproxy CA was likely regenerated. Run
mitm-tracker cert install to push the new CA. See mental model #5.
record stop returned exit 0 but the system proxy is still on 127.0.0.1:8080 → no, it doesn't anymore: the new code returns EXIT_SYSTEM on partial failure and emits a structured error on stderr that the tray surfaces as rumps.alert. If you still see this, the user is running an old build — pipx reinstall mitm-tracker and re-run setup install.
- Pre-Sonoma macOS (< 14.0) →
setup install will write /etc/pam.d/sudo_local but Touch ID will not actually trigger because pre-Sonoma's /etc/pam.d/sudo does not include sudo_local. mitm-tracker doctor warns on the macOS version check; tell the user to upgrade or skip with --skip-touch-id.
- Mac browser HTTPS still broken even with mitmproxy CA "installed" →
cert install only trusts the CA inside iOS Simulators. For Safari / Chrome / native apps to trust it, run mitm-tracker cert host install separately (system-wide trust, dangerous, reverse with cert host uninstall).
- Physical iPhone can't reach the proxy → with
device start, check mitm-tracker doctor: the proxy must be LAN-bound (0.0.0.0, not loopback), the phone must be on the same Wi-Fi, and the macOS firewall must not be blocking incoming connections (allow python/mitmdump). The phone needs the IP printed by device start, not 127.0.0.1.
- Physical iPhone HTTPS fails after installing the profile → installing the
.mobileconfig is not enough on iOS. The user must also enable Settings → General → About → Certificate Trust Settings → full trust for the mitmproxy cert. iOS never auto-trusts a manually installed root for SSL.
- Physical device: app shows "connection error" / nothing decrypts under WireGuard, but the WireGuard tunnel shows a handshake and data transfer → the cert's FULL TRUST toggle is OFF. The log shows
Client TLS handshake failed ... does not trust the proxy's certificate for every host (incl. third parties), 0 successes. Fix: Settings → General → About → Certificate Trust Settings → toggle ON mitmproxy. (If only the app's own hosts fail but third-party hosts decrypt, that's real certificate pinning — can't bypass without patching the app.)
- Physical app traffic never reaches the proxy at all (Wi-Fi proxy mode) → the app ignores the Wi-Fi proxy (native networking / QUIC). Switch to
device start --transport wireguard, which routes all traffic regardless.
device start deeplink buttons do nothing on the phone → expected on iOS 17+; Apple blocks prefs:root=… Settings links from Safari. Tell the user to follow the written steps on the page instead.
- Trust model: while a device session runs, the setup page hands out the CA and (in WireGuard mode) a private-key
.conf over plain HTTP on the LAN — anyone on that network can grab them. Only use on a trusted network and stop the session when done. Key material is stored 0600 under runtime/. This is intentional (same trade-off as Charles/Proxyman), not a bug to "fix".
Profile-scoped configuration
Each profile owns its own:
profiles/<name>/ssl.json — TLS decryption list
profiles/<name>/maplocal.json — Map Local rules
profiles/<name>/maplocal-bodies/ — response bodies + per-rule headers
Switching profiles (profile use <name>) does not restart the running record. The SSL list of the currently running mitmdump is whatever was active at record start. To apply a new profile to live capture: record stop && record start (after profile use).
JSON-mode contract
All commands accept --json and emit a stable shape. Errors in JSON mode go to stderr as {"error": "<code>", "message": "..."}. Prefer --json when parsing programmatically.