/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
$ npx -y skills add workweave/router --skill debug-claude-session --agent claude-codeHow 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.mdname: 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 gitignoredIf `.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)}")
EOFPrint `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
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 gitignoredIf `.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)}")
EOFPrint `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 =Model router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.
Repo: workweave/router
Other skills on router.
- /fix-pr-reviews
Fetches feedback from a GitHub PR — review-thread comments, ad-hoc PR comments (including those posted as a review's body without a thread), and bot/advisory review submissions — and fixes all of them as they appear (comments before CI). Bot comment-length nits apply verbatim;
Open skill - /test-claude-locally
Run the Weave router locally in docker compose and drive it with `claude -p` to reproduce and verify routing/translation behavior for a specific upstream model (e.g. GLM-5.1, DeepSeek, Qwen). Use when verifying a router fix end-to-end, reproducing a prod routing bug, confirming
Open skill

