Capture Claude Code with `mitmproxy` — step-by-step guide (with ready-to-run addons & analysis scripts)

September 22, 2025

Capture Claude Code with `mitmproxy` — step-by-step guide (with ready-to-run addons & analysis scripts)

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 pip to install mitmproxy.
  • The claude/Claude Code client that can be pointed at a custom ANTHROPIC_BASE_URL (or any client that calls api.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 UI
  • mitmweb — 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:8000 and forwards to https://api.anthropic.com.
  • Access the UI at http://127.0.0.1:8081 to 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:

  1. Start mitmproxy once — it creates certs at ~/.mitmproxy/mitmproxy-ca-cert.pem.
  2. On macOS: open Keychain AccessFile → Import Items → import ~/.mitmproxy/mitmproxy-ca-cert.pem.
    • Double-click the cert → Trust → set Always Trust for SSL.
  3. 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:

  • system field: 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


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 PROMPT
  • TOOLS
  • MESSAGES
  • RAW JSON RESPONSE or RAW 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 with filename, prompt_excerpt, captured_at, and hash.

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 objects
  • paired_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.mitm to record; --server-replay flows.mitm to 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 .mitm flow files with mitmdump -w flows.mitm instead of plain .log text 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_URL is set to http://localhost:8000 or 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 RESPONSE and 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)

  1. Start mitmproxy with the full addon:
mitmdump -s dump_claude_full.py --mode reverse:https://api.anthropic.com --listen-port 8000
  1. Export the environment and run Claude Code:
export ANTHROPIC_BASE_URL="http://localhost:8000"
claude
# interact with Claude Code like normal
  1. After a session, stop mitmproxy. Review claude_captures/ for timestamped .log files.
  2. Extract unique system prompts:
python3 extract_system_prompts.py --dir claude_captures --out all_system_prompts.txt --index system_prompts_index.csv
  1. Pair requests/responses into JSON:
python3 pair_captures.py   # or run the ready pairing script provided above
  1. 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.