Skip to content
Development
Skill

/debug-claude-session

Investigate a specific Claude Code session by session ID — correlate the local transcript (`~/.claude/projects/...jsonl`) with the router's production logs to understand what the client rendered vs. what the upstream served. Use when given a session ID and asked "why did X

From plugin
router
9443 skills10 commands
Install
$ npx -y skills add workweave/router --skill debug-claude-session --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/debug-claude-session

Context preview

The summary Claude sees to decide when to auto-load this skill.

Investigate a specific Claude Code session by session ID — correlate the local transcript (`~/.claude/projects/...jsonl`) with the router's production logs to understand what the client rendered vs. what the upstream served. Use when given a session ID and asked "why did X

SKILL.md

debug-claude-session.SKILL.md
name: debug-claude-session
description: Investigate a specific Claude Code session by session ID — correlate the local transcript (`~/.claude/projects/...jsonl`) with the router's production logs to understand what the client rendered vs. what the upstream served. Use when given a session ID and asked "why did X render?" for a Claude Code conversation routed through the router.

Debugging a Claude Code session

Given a Claude Code **session ID**, pull the local transcript (what the client saw) and the corresponding production cloud logs (which model/provider served it), then correlate them to understand the wire-format translation. The local `.jsonl` is ground truth for *what rendered*; the cloud logs confirm *what the upstream sent*; the `internal/translate` code explains *why the wire shape looks that way*.

Setup: Cloud deployment config

Before starting, create a gitignored config file with your deployment's cloud logging details:

cat > .claude/skills/debug-claude-session/.deployment.json <<'EOF'
{
  "cloud_provider": "gcp",
  "project_id": "your-project-id",
  "region": "us-central1",
  "service_name": "router",
  "log_command_template": "gcloud logging read ... --project {project_id} --format=json"
}
EOF
git add .claude/skills/debug-claude-session/.deployment.json.example
# .deployment.json itself should be gitignored

If `.deployment.json` is missing, the agent will prompt you for these details and walk you through creating it. The file is gitignored and contains no secrets — it's just the service/project/region names needed to construct cloud log queries.

Critical gotchas (read first)

  • **The transcript is the source of truth for rendering.** What the client showed is exactly the assistant `message.content` blocks in the `.jsonl` — not what you assume the model emitted. Always inspect block contents (decode fields, check lengths) before concluding something is "empty" or "corrupt".
  • **Streaming splits one logical turn across multiple `assistant` lines.** Each line may hold a single block; they share a `message.id` and `message.model`. Reconstruct the full turn by collecting all lines with the same `message.id`.
  • **Block content may carry encoded state.** Fields like `signature`, `id`, and `input` often encode provider-specific state (e.g. encrypted reasoning). Decode and inspect before skipping or dismissing a block.
  • **Cloud logs are structured, not free text.** Filter queries on specific JSON fields (e.g. `jsonPayload.decision_model`, `jsonPayload.message`). The exact field names depend on the router's logging schema — ask if unsure.
  • **Correlate by time + model, not request id.** The local transcript has UTC timestamps (`timestamp` field); request IDs are often absent. Use a tight UTC window around transcript timestamps plus the served `decision_model` to find matching cloud log entries.

Workflow

- [ ] 1. Locate the local transcript
- [ ] 2. Extract the assistant blocks showing the symptom
- [ ] 3. Decode block internals (signatures, ids, sizes)
- [ ] 4. Identify model + provider from the transcript
- [ ] 5. Fetch cloud logs for the matching UTC window
- [ ] 6. Correlate transcript + cloud logs
- [ ] 7. Trace to the translation code in internal/translate

1. Locate the local transcript

find ~/.claude/projects -name '<SESSION_ID>*' -type f

You get `<path>/<SESSION_ID>.jsonl` (the transcript, one JSON object per line) and a sibling `<SESSION_ID>/` directory (tool-output spillover). The `.jsonl` file is the ground truth. `wc -l` it — typical sessions are tens to hundreds of lines.

2. Extract the assistant blocks showing the symptom

Each line is a typed event. Scan for `type: "assistant"` entries. Each carries:

  • `message.id` — groups lines from the same logical turn.
  • `message.model` — the served model (e.g. `gpt-5.5`, `claude-opus-4-8`).
  • `message.stop_reason` — how the turn ended (`end_turn`, `tool_use`, `max_tokens`).
  • `message.content[]` — list of blocks (`text`, `thinking`, `tool_use`, `tool_result`).

Adapt this template to search for your symptom (empty blocks, missing content, unexpected stop_reason, etc.):

python3 - <<'EOF'
import json
with open("<path>/<SESSION_ID>.jsonl") as f:
    for i, line in enumerate(f):
        try:
            o = json.loads(line)
        except:
            continue
        if o.get("type") != "assistant":
            continue
        msg = o.get("message", {})
        # Adapt this filter to your symptom:
        for block in msg.get("content", []):
            if isinstance(block, dict) and block.get("type") == "thinking":
                thinking_text = block.get("thinking", "")
                signature = block.get("signature", "")
                if thinking_text == "":  # Your condition here
                    print(f"line {i+1}: model={msg.get('model')} "
                          f"stop_reason={msg.get('stop_reason')} "
                          f"thinking_len={len(thinking_text)} "
                          f"signature_len={len(signature)}")
EOF

Print `model`, `stop_reason`, block type/length — enough to see the pattern at a glance.

3. Decode block internals

Don't assume a short or empty field is meaningless. Many blocks carry encoded provider state. Inspect the actual bytes:

python3 - <<'EOF'
import json, base64
line_num = <LINE>  # From step 2
with open("<path>/<SESSION_ID>.jsonl") as f:
    o = json.loads(f.readlines()[line_num - 1])
for block in o["message"].get("content", []):
    block_type = block.get("type")
    print(f"=== {block_type} ===")
    # Print all fields and their lengths:
    for key, val in block.items():
        if isinstance(val, str):
            print(f"  {key}: len={len(val)} head={val[:80]!r}")
        else:
            print(f"  {key}: {type(val).__name__} {val!r}")
    # If any field looks base64-encoded, try decoding:
    if "signature" in block and block["signature"]:
        try:
            decoded =
Read more
Ships withrouter

Model router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.

Get the whole plugin