Capture Claude Code with `mitmproxy` — step-by-step guide (with ready-to-run addons & analysis scripts)
September 22, 2025
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("parameters", {}), 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("parameters", {}), 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.