Skip to content

Instantly share code, notes, and snippets.

@dawehner
Created March 18, 2026 11:47
Show Gist options
  • Select an option

  • Save dawehner/7f06ddba07161dc39d3a63ab7e9012a1 to your computer and use it in GitHub Desktop.

Select an option

Save dawehner/7f06ddba07161dc39d3a63ab7e9012a1 to your computer and use it in GitHub Desktop.
claude-thinking-viewer: Browse and extract thinking tokens from Claude Code sessions
#!/usr/bin/env bash
set -euo pipefail
CLAUDE_DIR="$HOME/.claude/projects"
# Colors
BOLD='\033[1m'
DIM='\033[2m'
CYAN='\033[36m'
YELLOW='\033[33m'
GREEN='\033[32m'
RESET='\033[0m'
usage() {
echo -e "${BOLD}claude-thinking-viewer${RESET} — Browse thinking tokens from Claude Code sessions"
echo ""
echo "Usage: $(basename "$0") [--dump <file>] [--raw]"
echo ""
echo "Options:"
echo " --dump <file> Write thinking tokens to a file instead of paging"
echo " --raw Output raw thinking text (no headers/formatting)"
echo " -h, --help Show this help"
}
# Parse args
DUMP_FILE=""
RAW=false
while [[ $# -gt 0 ]]; do
case "$1" in
--dump) DUMP_FILE="$2"; shift 2 ;;
--raw) RAW=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
if [[ ! -d "$CLAUDE_DIR" ]]; then
echo "Error: Claude Code projects directory not found at $CLAUDE_DIR"
exit 1
fi
# Step 1: Pick a project
echo -e "${BOLD}Select a project:${RESET}"
echo ""
projects=()
while IFS= read -r dir; do
projects+=("$dir")
done < <(find "$CLAUDE_DIR" -mindepth 1 -maxdepth 1 -type d | sort)
# Build display names from directory names
for i in "${!projects[@]}"; do
dirname=$(basename "${projects[$i]}")
# Convert dashes back to path separators for readability
display="${dirname//-//}"
# Count sessions
count=$(find "${projects[$i]}" -maxdepth 1 -name '*.jsonl' 2>/dev/null | wc -l | tr -d ' ')
printf " ${CYAN}%3d${RESET}) %-60s ${DIM}(%s sessions)${RESET}\n" "$((i+1))" "$display" "$count"
done
echo ""
read -rp "Project number: " project_choice
if [[ -z "$project_choice" ]] || [[ "$project_choice" -lt 1 ]] || [[ "$project_choice" -gt "${#projects[@]}" ]]; then
echo "Invalid selection."
exit 1
fi
project_dir="${projects[$((project_choice-1))]}"
# Step 2: Pick a session (show most recent first)
echo ""
echo -e "${BOLD}Select a session:${RESET}"
echo ""
sessions=()
while IFS= read -r f; do
sessions+=("$f")
done < <(ls -t "$project_dir"/*.jsonl 2>/dev/null)
if [[ ${#sessions[@]} -eq 0 ]]; then
echo "No sessions found in this project."
exit 1
fi
# Show up to 20 most recent
limit=20
show=${#sessions[@]}
if [[ $show -gt $limit ]]; then
show=$limit
fi
for i in $(seq 0 $((show-1))); do
file="${sessions[$i]}"
fname=$(basename "$file" .jsonl)
size=$(du -h "$file" | cut -f1 | tr -d ' ')
mod=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$file" 2>/dev/null || stat -c "%y" "$file" 2>/dev/null | cut -d. -f1)
# Quick check: does it have thinking tokens?
has_thinking=$(python3 -c "
import json, sys
found = False
for line in open('$file'):
try:
obj = json.loads(line)
if obj.get('type') == 'assistant' and 'message' in obj:
for block in obj['message'].get('content', []):
if block.get('type') == 'thinking' and block.get('thinking'):
found = True
break
if found: break
except: pass
print('Y' if found else 'N')
" 2>/dev/null || echo "?")
# Get first user message as preview
preview=$(python3 -c "
import json, sys
for line in open('$file'):
try:
obj = json.loads(line)
if obj.get('type') == 'human' and 'message' in obj:
for block in obj['message'].get('content', []):
if isinstance(block, dict) and block.get('type') == 'text':
text = block['text'].strip().replace('\n', ' ')[:60]
if text and not text.startswith('<'):
print(text)
sys.exit(0)
break
except: pass
print('(no preview)')
" 2>/dev/null || echo "(error)")
if [[ "$has_thinking" == "Y" ]]; then
thinking_marker="${GREEN}[T]${RESET}"
else
thinking_marker="${DIM}[ ]${RESET}"
fi
printf " ${CYAN}%3d${RESET}) %s ${YELLOW}%-19s${RESET} %5s ${DIM}%s${RESET} %s\n" \
"$((i+1))" "$thinking_marker" "$mod" "$size" "${fname:0:8}…" "$preview"
done
if [[ ${#sessions[@]} -gt $limit ]]; then
echo -e " ${DIM}... and $((${#sessions[@]} - limit)) older sessions${RESET}"
fi
echo ""
echo -e " ${GREEN}[T]${RESET} = has thinking tokens"
echo ""
read -rp "Session number: " session_choice
if [[ -z "$session_choice" ]] || [[ "$session_choice" -lt 1 ]] || [[ "$session_choice" -gt "$show" ]]; then
echo "Invalid selection."
exit 1
fi
selected="${sessions[$((session_choice-1))]}"
# Step 3: Extract and display thinking tokens
echo ""
echo -e "${BOLD}Extracting thinking tokens from $(basename "$selected")...${RESET}"
echo ""
extract_thinking() {
local file="$1"
local raw_mode="$2"
python3 -c "
import json, sys
raw = '$raw_mode' == 'true'
block_num = 0
for line_num, line in enumerate(open('$file'), 1):
try:
obj = json.loads(line)
if obj.get('type') == 'assistant' and 'message' in obj:
for block in obj['message'].get('content', []):
if block.get('type') == 'thinking' and block.get('thinking'):
block_num += 1
text = block['thinking']
if raw:
print(text)
print()
else:
print('=' * 80)
print(f' THINKING BLOCK #{block_num} ({len(text)} chars)')
print('=' * 80)
print()
print(text)
print()
except json.JSONDecodeError:
pass
if block_num == 0:
print('No thinking tokens found in this session.')
else:
if not raw:
print('=' * 80)
print(f' Total: {block_num} thinking blocks')
print('=' * 80)
"
}
if [[ -n "$DUMP_FILE" ]]; then
extract_thinking "$selected" "$RAW" > "$DUMP_FILE"
lines=$(wc -l < "$DUMP_FILE" | tr -d ' ')
size=$(du -h "$DUMP_FILE" | cut -f1 | tr -d ' ')
echo -e "${GREEN}Written to ${DUMP_FILE}${RESET} (${lines} lines, ${size})"
else
extract_thinking "$selected" "$RAW" | ${PAGER:-less -R}
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment