Capture Claude Code with `mitmproxy` — step-by-step guide (with ready-to-run addons & analysis scripts)
Intercept Claude Code traffic with mitmproxy: step-by-step setup, custom addons, and Python scripts that log exactly what the CLI sends to Anthropic's API.
Table of contents
Want to see what Claude Code actually sends to Anthropic? This post walks you through a practical, repeatable workflow to intercept, save, and analyze Claude Code traffic using mitmproxy. You’ll get:
- A step-by-step setup for reverse-proxying Claude Code through
mitmproxy. - A ready-to-run addon that extracts hidden system prompts (the “IMPORTANT: …” rules).
- A full-featured addon that captures requests + responses (system prompts, tool defs, user messages, and streamed/partial responses) into timestamped logs.
- Small analyzer tools to extract system prompts and to pair requests with responses into JSON for replay/analysis.
- Security, privacy, and legal cautions.
Short TL;DR: run mitmproxy in reverse mode, set
ANTHROPIC_BASE_URL=http://localhost:8000, trust the mitm CA while testing, run Claude Code — and the addons below will save the payloads for you.
Safety & ethics (read first)
- Only intercept traffic you own or are explicitly authorized to inspect. Intercepting other users’ traffic is illegal/unethical.
- Captured logs will contain API keys, tokens, and secrets. Treat them as sensitive. Redact before sharing.
- After testing, remove the mitmproxy CA from your system trust store. Don’t trust it permanently.
- If a client uses TLS pinning, you’ll be blocked — do not attempt to bypass pinning for systems you don’t own.
Prerequisites
- macOS or Linux (commands given for macOS; Linux is similar).
- Python 3.8+ (for mitmproxy addons).
- Homebrew (optional) or
pipto install mitmproxy. - The
claude/Claude Code client that can be pointed at a customANTHROPIC_BASE_URL(or any client that callsapi.anthropic.com).
1- Install mitmproxy
macOS (Homebrew):
brew install mitmproxy
or with pip (virtualenv recommended):
python3 -m venv venv
source venv/bin/activate
pip install mitmproxy
Tools you now have:
mitmproxy— interactive terminal UImitmweb— web UI (default web inspector on :8081)mitmdump— headless/script mode (great for running addons)
2— Start mitmproxy in reverse mode
Claude Code normally calls https://api.anthropic.com. Run mitmproxy as a reverse proxy that forwards to the real Anthropic API:
mitmweb --mode reverse:https://api.anthropic.com --listen-port 8000
- This listens on
http://localhost:8000and forwards tohttps://api.anthropic.com. - Access the UI at
http://127.0.0.1:8081to inspect flows live.
3— Trust the mitmproxy CA (for HTTPS)
mitmproxy generates a local CA to sign TLS certificates for intercepted hosts. To avoid TLS errors, you must trust it temporarily:
- Start mitmproxy once — it creates certs at
~/.mitmproxy/mitmproxy-ca-cert.pem. - On macOS: open Keychain Access →
File → Import Items→ import~/.mitmproxy/mitmproxy-ca-cert.pem.- Double-click the cert → Trust → set
Always Trustfor SSL.
- Double-click the cert → Trust → set
- For Firefox or iOS/Android, import the CA into their certificate stores (Firefox uses its own cert store).
Important: When done testing, remove the mitmproxy CA from trust stores.
4— Point Claude Code at mitmproxy
Set the Anthropic base URL to your mitmproxy address before launching Claude Code:
export ANTHROPIC_BASE_URL="http://localhost:8000"
claude
Now Claude Code → mitmproxy → Anthropic API.
5— What you will capture
Typical things you’ll capture in requests:
systemfield: hidden system prompt / policy rules (the “IMPORTANT: …” text).tools: the tool schema (search, git, edit, etc.).messages: user / assistant messages.
Responses may arrive streamed and fragmented (partial JSON chunks). mitmproxy exposes both request and response bodies so you can save them.
6— Ready-to-run addon: extract system prompts only
Create dump_claude_prompts.py with this content. It extracts system fields and appends them to a text file.
# dump_claude_prompts.py
from mitmproxy import http
import json
OUTPUT_FILE = "claude_system_prompts.txt"
def request(flow: http.HTTPFlow):
if "anthropic.com" not in flow.request.pretty_host:
return
try:
body = flow.request.get_text()
data = json.loads(body)
if "system" in data:
system_prompt = data["system"]
with open(OUTPUT_FILE, "a", encoding="utf-8") as f:
f.write("\n=== New System Prompt Captured ===\n")
f.write(system_prompt)
f.write("\n----------------------------------\n")
print(f"[+] Captured system prompt ({len(system_prompt)} chars)")
except Exception as e:
print(f"[!] Error parsing request: {e}")
Run it:
mitmdump -s dump_claude_prompts.py --mode reverse:https://api.anthropic.com --listen-port 8000
Output: claude_system_prompts.txt containing each captured system block.
7— Extended addon: capture system prompts + tool definitions
If you want both the system prompts and the tools definitions captured in a structured text file, use:
# dump_claude_prompts_and_tools.py
from mitmproxy import http
import json
OUTPUT_FILE = "claude_prompts_and_tools.txt"
def request(flow: http.HTTPFlow):
if "anthropic.com" not in flow.request.pretty_host:
return
try:
body = flow.request.get_text()
data = json.loads(body)
with open(OUTPUT_FILE, "a", encoding="utf-8") as f:
f.write("\n==============================\n")
f.write(" NEW CLAUDE REQUEST CAPTURED\n")
f.write("==============================\n\n")
if "system" in data:
f.write("### SYSTEM PROMPT ###\n")
f.write(data["system"].strip())
f.write("\n\n")
if "tools" in data:
f.write("### TOOL DEFINITIONS ###\n")
for tool in data["tools"]:
name = tool.get("name", "<no name>")
desc = tool.get("description", "<no description>")
params = json.dumps(tool.get("input_schema", {}), indent=2)
f.write(f"- Tool: {name}\n")
f.write(f" Description: {desc}\n")
f.write(f" Parameters:\n{params}\n\n")
f.write("----------------------------------\n")
print("[+] Captured Claude system + tools")
except Exception as e:
print(f"[!] Error parsing request: {e}")
Run:
mitmdump -s dump_claude_prompts_and_tools.py --mode reverse:https://api.anthropic.com --listen-port 8000
Output: claude_prompts_and_tools.txt
8— Full-featured addon: capture requests + responses (structured, timestamped logs)
This is the full addon that creates a timestamped .log file per request/response pair. Save as dump_claude_full.py.
# dump_claude_full.py
from mitmproxy import http
import json, os, time
OUTPUT_DIR = "claude_captures"
os.makedirs(OUTPUT_DIR, exist_ok=True)
def _safe_json_parse(text: str):
try:
return json.loads(text)
except Exception:
return None
def _write_section(f, title: str, content: str):
f.write(f"\n### {title} ###\n")
f.write(content.strip() if content else "<empty>")
f.write("\n")
def request(flow: http.HTTPFlow):
if "anthropic.com" not in flow.request.pretty_host:
return
ts = time.strftime("%Y%m%d-%H%M%S")
filename = os.path.join(OUTPUT_DIR, f"claude_{ts}_{flow.id}.log")
body = flow.request.get_text()
data = _safe_json_parse(body)
with open(filename, "w", encoding="utf-8") as f:
f.write("=====================================\n")
f.write(f" CLAUDE REQUEST {time.ctime()}\n")
f.write("=====================================\n")
if data and "system" in data:
_write_section(f, "SYSTEM PROMPT", data["system"])
if data and "tools" in data:
tool_dump = []
for tool in data["tools"]:
name = tool.get("name", "<no name>")
desc = tool.get("description", "<no description>")
params = json.dumps(tool.get("input_schema", {}), indent=2)
tool_dump.append(f"- {name}: {desc}\n Params: {params}")
_write_section(f, "TOOLS", "\n".join(tool_dump))
if data and "messages" in data:
messages = []
for msg in data["messages"]:
role = msg.get("role", "?")
content = msg.get("content", "")
messages.append(f"{role.upper()}: {content}")
_write_section(f, "MESSAGES", "\n".join(messages))
f.write("\n--- Waiting for response ---\n")
flow.metadata["dump_file"] = filename
print(f"[+] Captured request → {filename}")
def response(flow: http.HTTPFlow):
if "anthropic.com" not in flow.request.pretty_host:
return
filename = flow.metadata.get("dump_file")
if not filename:
ts = time.strftime("%Y%m%d-%H%M%S")
filename = os.path.join(OUTPUT_DIR, f"claude_{ts}_{flow.id}.log")
text = flow.response.get_text()
data = _safe_json_parse(text)
with open(filename, "a", encoding="utf-8") as f:
f.write("\n=====================================\n")
f.write(f" CLAUDE RESPONSE {time.ctime()}\n")
f.write("=====================================\n")
if data:
formatted = json.dumps(data, indent=2, ensure_ascii=False)
_write_section(f, "RAW JSON RESPONSE", formatted)
else:
_write_section(f, "RAW TEXT RESPONSE", text)
f.write("\n========== END OF CAPTURE ==========\n")
print(f"[+] Appended response → {filename}")
Run the capture:
mitmdump -s dump_claude_full.py --mode reverse:https://api.anthropic.com --listen-port 8000
Output: one .log file per request in claude_captures/. Each file contains:
SYSTEM PROMPTTOOLSMESSAGESRAW JSON RESPONSEorRAW TEXT RESPONSE(stream fragments included)
9— Analyzer: extract & deduplicate system prompts
Use this script extract_system_prompts.py to scan the claude_captures/ logs, extract the system prompt sections, deduplicate by SHA-256, and write:
all_system_prompts.txt— human readable prompts (one per unique prompt)system_prompts_index.csv— index withfilename,prompt_excerpt,captured_at, andhash.
extract_system_prompts.py (save and run):
#!/usr/bin/env python3
import os, re, hashlib, csv, argparse, datetime, json
SYSTEM_HEADING = "### SYSTEM PROMPT ###"
DIVIDER = "\n=== PROMPT ===\n"
def extract_system_from_text(text):
pattern = re.compile(r"###\s*SYSTEM PROMPT\s*###\s*(.*?)\s*(?=###\s*\w+\s*###|$)", re.S|re.I)
m = pattern.search(text)
if m:
return m.group(1).strip()
return None
def scan_directory(dirname):
prompts = []
for root, _, files in os.walk(dirname):
for fname in sorted(files):
if not fname.lower().endswith(".log"):
continue
path = os.path.join(root, fname)
with open(path, "r", encoding="utf-8") as fh:
txt = fh.read()
prompt = extract_system_from_text(txt)
if prompt:
h = hashlib.sha256(prompt.encode("utf-8")).hexdigest()
captured_at = ""
m = re.search(r"claude_(\d{8}-\d{6})", fname)
if m:
try:
captured_at = datetime.datetime.strptime(m.group(1), "%Y%m%d-%H%M%S").isoformat()
except:
pass
prompts.append({"filename": path, "prompt": prompt, "hash": h, "captured_at": captured_at})
return prompts
def save_prompts(prompts, out_txt="all_system_prompts.txt", index_csv="system_prompts_index.csv"):
seen = {}
for p in prompts:
h = p["hash"]
if h not in seen or (p["captured_at"] and p["captured_at"] < seen[h]["captured_at"]):
seen[h] = p
unique = list(seen.values())
unique.sort(key=lambda x: x["captured_at"] or "")
with open(out_txt, "w", encoding="utf-8") as fh:
for u in unique:
fh.write(DIVIDER)
fh.write(f"Source file: {u['filename']}\nCaptured at: {u['captured_at']}\n\n")
fh.write(u["prompt"].strip())
fh.write("\n")
with open(index_csv, "w", newline='', encoding="utf-8") as csvf:
writer = csv.writer(csvf)
writer.writerow(["filename", "captured_at", "prompt_excerpt", "hash"])
for u in unique:
excerpt = u["prompt"].replace("\n", " ")[:200]
writer.writerow([u["filename"], u["captured_at"], excerpt, u["hash"]])
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--dir", default="claude_captures")
parser.add_argument("--out", default="all_system_prompts.txt")
parser.add_argument("--index", default="system_prompts_index.csv")
args = parser.parse_args()
prompts = scan_directory(args.dir)
if not prompts:
print("No system prompts found.")
else:
save_prompts(prompts, out_txt=args.out, index_csv=args.index)
print(f"Saved {len(prompts)} prompt entries to {args.out} and {args.index}")
Run:
python3 extract_system_prompts.py --dir claude_captures --out all_system_prompts.txt --index system_prompts_index.csv
10— Pair request + response into JSON (for replay/analysis)
If you prefer machine-friendly pairing (one JSON per capture + a master JSON), use the pairing script below. It reads .log files produced by dump_claude_full.py, parses sections, and writes:
paired_captures.json— master array of capture objectspaired_jsons/*.json— per-capture JSON files
pairing script (example implementation):
# pair_captures.py (concept)
# Pseudo: read .log files from claude_captures, parse headings using regex,
# construct object: {source_file, captured_at, system_prompt, tools (list), messages, raw_response, response_json}
# write per-capture JSONs into paired_jsons/, and a master paired_captures.json.
# (see the earlier detailed pairing code in this guide — you can copy it verbatim or adapt)
You can then:
- Use the per-capture JSON to construct replay requests (curl, httpie) or to feed into a local testing harness.
- Convert captures into mitmproxy server-replay format if you want to replay exact traffic offline.
11— Replaying captures (quick note)
- mitmproxy supports server-replay using recorded flows (
mitmdump -w flows.mitmto record;--server-replay flows.mitmto replay). - The JSON pairing output is meant for programmatic analysis or custom replay scripts (curl/requests). To do true request/response replay in mitmproxy format, consider recording
.mitmflow files withmitmdump -w flows.mitminstead of plain.logtext dumps.
12— Secrets redaction & cleanup
Before sharing or archiving logs:
- Remove or redact header values (Authorization, Cookies). Example (Python snippet):
# naive header redaction example
if "Authorization" in flow.request.headers:
flow.request.headers["Authorization"] = "<REDACTED>"
- Remove CA trust from Keychain after testing to restore system security.
- Keep captured files in an encrypted directory if they will persist.
13— Troubleshooting & tips
- TLS errors → probably didn’t trust the mitm CA or the client pins certs.
- No captured traffic → ensure
ANTHROPIC_BASE_URLis set tohttp://localhost:8000or the client is configured to use an HTTP proxy. - Fragmented JSON responses → Anthropic often streams partial JSON; your parsers should handle partial/invalid JSON gracefully (try to capture
RAW TEXT RESPONSEand then reconstruct). - Certificate in mobile devices → import mitm CA into device trust store; for iOS get provisioning complications.
- Large logs → rotate logs or compress them. Consider storing only extracted fields (system, tools, messages) to reduce size.
14— Example workflow (full run)
- Start mitmproxy with the full addon:
mitmdump -s dump_claude_full.py --mode reverse:https://api.anthropic.com --listen-port 8000
- Export the environment and run Claude Code:
export ANTHROPIC_BASE_URL="http://localhost:8000"
claude
# interact with Claude Code like normal
- After a session, stop mitmproxy. Review
claude_captures/for timestamped.logfiles. - Extract unique system prompts:
python3 extract_system_prompts.py --dir claude_captures --out all_system_prompts.txt --index system_prompts_index.csv
- Pair requests/responses into JSON:
python3 pair_captures.py # or run the ready pairing script provided above
- Redact secrets and remove mitm CA from trust (cleanup).
Final notes
This workflow is how researchers and engineers have examined clients like Claude Code to understand:
- how system prompts / constraints are delivered to the model, how streaming partial JSON is handled, and what policies the client attaches to each request.
For AI agents:get_post/posts/index.json/Capture-Claude-Code-with-mitmproxy-step-by-step-guide-with-addons-analysis-scripts.md
Free newsletter
Stay on the Nerd Track
Two emails a week: a Monday deep dive on one topic and a Thursday digest of courses, guides, posts, podcasts and jobs.