Claude Code Session Data Exploration

Structured queries against ~/.claude/history.jsonl and per-project session files. Every Claude Code session leaves a trace — these patterns extract signal from it.

Data Layout

Claude Code stores session data in two locations:

Path Contents

~/.claude/history.jsonl

Global session index — one JSON object per line (timestamp, project, display)

~/.claude/projects/<encoded-path>/*.jsonl

Full conversation content per session — messages, tool calls, results

Before querying, understand the shape of the data.

inspect a single record
head -1 ~/.claude/history.jsonl | jq '.'
available keys
head -1 ~/.claude/history.jsonl | jq 'keys'
total records
wc -l ~/.claude/history.jsonl
per-project session file sizes — ranked by line count
find ~/.claude/projects -name "*.jsonl" \
  | xargs wc -l | sort -rn | head -20

Session Counts & Inventory

total session count
jq -s 'length' ~/.claude/history.jsonl
sessions by project — ranked
jq -s 'group_by(.project)
  | map({project: .[0].project, sessions: length})
  | sort_by(.sessions) | reverse' \
  ~/.claude/history.jsonl
unique projects worked on
jq -s '[.[].project] | unique | sort' \
  ~/.claude/history.jsonl
sessions with pasted content
jq -s '[.[] | select(.display | test("Pasted"))] | length' \
  ~/.claude/history.jsonl

Activity Timeline

activity log — human-readable dates
jq -s 'sort_by(.timestamp)
  | .[]
  | {date: (.timestamp/1000 | strftime("%Y-%m-%d %H:%M")),
     project: .project,
     display: .display}' \
  ~/.claude/history.jsonl | head -100
first and last session timestamps
jq -s 'sort_by(.timestamp)
  | {first: (.[0]  | {date: (.timestamp/1000 | strftime("%Y-%m-%d %H:%M")), display: .display}),
     last:  (.[-1] | {date: (.timestamp/1000 | strftime("%Y-%m-%d %H:%M")), display: .display})}' \
  ~/.claude/history.jsonl
most active days
jq -s '[.[] | .timestamp/1000 | strftime("%Y-%m-%d")]
  | group_by(.)
  | map({date: .[0], count: length})
  | sort_by(.count) | reverse' \
  ~/.claude/history.jsonl
sessions per day histogram
jq -s 'group_by(.timestamp/1000 | strftime("%Y-%m-%d"))
  | map({date: .[0].timestamp/1000 | strftime("%Y-%m-%d"),
         count: length})
  | sort_by(.date)' \
  ~/.claude/history.jsonl
activity by day of week
jq -s '[.[] | .timestamp/1000 | strftime("%A")]
  | group_by(.)
  | map({day: .[0], count: length})
  | sort_by(.count) | reverse' \
  ~/.claude/history.jsonl
peak coding hours
jq -s '[.[] | .timestamp/1000 | strftime("%H")]
  | group_by(.)
  | map({hour: .[0], count: length})
  | sort_by(.hour)' \
  ~/.claude/history.jsonl

Search & Filter

sessions matching a keyword (case-insensitive)
jq -s '.[] | select(.display | test("ISE|QRadar|python"; "i"))
  | {date: (.timestamp/1000 | strftime("%Y-%m-%d")),
     display: .display,
     project: .project}' \
  ~/.claude/history.jsonl
sessions in a date range
jq -s '.[]
  | select(.timestamp/1000 | strftime("%Y-%m-%d") >= "2026-04-01")
  | {date: (.timestamp/1000 | strftime("%Y-%m-%d %H:%M")),
     display: .display}' \
  ~/.claude/history.jsonl
word frequency in session displays
jq -s '[.[].display]
  | join(" ")
  | ascii_downcase
  | split(" ")[]' \
  ~/.claude/history.jsonl \
  | sort | uniq -c | sort -rn | head -30
search full session content for a keyword
grep -rl "keyword" ~/.claude/projects/ \
  | xargs jq -r 'select(.type == "say") | .text' 2>/dev/null \
  | grep -i "keyword" | head -20

Deep Dive — Full Session Content

The per-project .jsonl files contain the full conversation: user messages, assistant responses, tool calls, and results. The encoded path replaces / with - and prepends a -.

most recent session file for this project
ls -t ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/*.jsonl \
  | head -1
read the most recent session
jq '.' "$(ls -t ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/*.jsonl | head -1)" \
  | head -100
extract only assistant messages
jq 'select(.type == "say") | .text' \
  "$(ls -t ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/*.jsonl | head -1)" \
  | head -50
tool calls made in a session
jq 'select(.type == "tool_use") | {tool: .name, input: .input}' \
  "$(ls -t ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/*.jsonl | head -1)"
session duration estimate (first to last message)
jq -s '{
  start: (.[0].timestamp/1000 | strftime("%H:%M")),
  end:   (.[-1].timestamp/1000 | strftime("%H:%M")),
  messages: length
}' "$(ls -t ~/.claude/projects/-home-evanusmodestus-atelier--bibliotheca-domus-captures/*.jsonl | head -1)"