---
title: Capture Claude Code with `mitmproxy` — step-by-step guide (with ready-to-run addons & analysis scripts)
description: '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.'
url: https://nerdleveltech.com/Capture-Claude-Code-with-mitmproxy-step-by-step-guide-with-addons-analysis-scripts
language: en
date: '2025-09-22'
updated: '2025-09-22'
category: llm-integration
tags:
  - Claude Code
  - AI
  - reverse-engineering
  - mitmproxy
  - Anthropic
  - API debugging
  - proxy
  - Python
markdown_url: https://nerdleveltech.com/Capture-Claude-Code-with-mitmproxy-step-by-step-guide-with-addons-analysis-scripts.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/Capture-Claude-Code-with-mitmproxy-step-by-step-guide-with-addons-analysis-scripts'
terms: https://nerdleveltech.com/llms.txt
---

# 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):
```bash
brew install mitmproxy
```

or with pip (virtualenv recommended):
```bash
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:

```bash
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 Access** → `File → 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:

```bash
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.

```python
# 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:
```bash
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:

```python
# 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:
```bash
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`.

```python
# 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:
```bash
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):

```python
#!/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:
```bash
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):**

```python
# 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):

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

[^mitmproxy-docs]: mitmproxy Documentation. https://docs.mitmproxy.org/
