JQ for Claude - Session History Queries
Use this skill when users ask about past conversations, previous sessions, or conversation history.
Session File Location
Claude Code stores session history as JSONL files at:
~/.claude/projects/<encoded-project-path>/<session-id>.jsonl
Path encoding: Project paths use dashes instead of slashes.
/home/user/my-projectbecomes-home-user-my-project
Find the Project Directory
To find sessions for a specific project path:
# Encode project path (replace / with -)
PROJECT_PATH="/home/user/my-project"
ENCODED=$(echo "$PROJECT_PATH" | sed 's|^/|-|; s|/|-|g')
SESSION_DIR="$HOME/.claude/projects/$ENCODED"
# List sessions sorted by modification time
ls -lt "$SESSION_DIR"/*.jsonl 2>/dev/null | head -10
List All Project Directories
ls -lt ~/.claude/projects/ | head -20
JSONL File Structure
Each session file contains one JSON object per line:
| Line Type | Key Fields | Description |
|---|---|---|
summary | type, summary, leafUuid | Session metadata (first line) |
user | type, timestamp, message | User messages |
assistant | type, timestamp, message | Claude responses |
system | type, message | System prompts |
file-history-snapshot | type, snapshot | File state snapshots |
Message Content Structure
User and assistant messages have this structure:
{
"type": "user",
"timestamp": "2026-01-10T22:32:07.522Z",
"message": {
"role": "user",
"content": "your message text here"
},
"sessionId": "uuid",
"uuid": "message-uuid"
}
Note: message.content can be either:
- A string:
"content": "hello" - An array:
"content": [{"type": "text", "text": "hello"}]
Common jq Patterns
Extract User Messages with Timestamps
cat session.jsonl | jq -r '
select(.type == "user") |
.timestamp + " | " + (
if .message.content | type == "string"
then .message.content[0:300]
else (.message.content[0].text // "N/A")[0:300]
end
)
' | grep -v "N/A" | grep -v "command-name" | grep -v "local-command"
Count Message Types in a Session
cat session.jsonl | jq -r '.type' | sort | uniq -c
Get Session Start Time
head -5 session.jsonl | jq -r 'select(.type == "user") | .timestamp' | head -1
Find Sessions by Content
Search across all sessions in a project for a keyword:
SESSION_DIR="$HOME/.claude/projects/-home-user-my-project"
for f in "$SESSION_DIR"/*.jsonl; do
if grep -q "keyword" "$f" 2>/dev/null; then
echo "=== $f ==="
cat "$f" | jq -r 'select(.type == "user" and (.message.content | tostring | test("keyword"; "i"))) | .timestamp + " | " + (.message.content | tostring)[0:200]' 2>/dev/null
fi
done
List Recent Sessions with First User Message
SESSION_DIR="$HOME/.claude/projects/-home-user-my-project"
for f in $(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | grep -v "agent-" | head -5); do
echo ""
echo "=== $(basename $f) ==="
cat "$f" | jq -r '
select(.type == "user") |
.timestamp + " | " + (
if .message.content | type == "string"
then .message.content[0:150]
else (.message.content[0].text // "N/A")[0:150]
end
)
' 2>/dev/null | grep -v "N/A" | grep -v "command-name" | head -3
done
Filtering Tips
Exclude Agent Sessions
Agent/subagent sessions have filenames starting with agent-:
ls -lt "$SESSION_DIR"/*.jsonl | grep -v "agent-"
Filter by Date
Sessions include ISO timestamps. Filter with jq:
cat session.jsonl | jq -r '
select(.type == "user" and .timestamp > "2026-01-10") |
.timestamp + " | " + (.message.content | tostring)[0:100]
'
Exclude System Messages
Filter out command invocations and system noise:
grep -v "command-name" | grep -v "local-command" | grep -v "N/A"
Example Workflow
When a user asks "what did I ask you in our last few sessions?":
-
Identify the project path (current working directory or ask user)
-
Encode and locate sessions:
ENCODED=$(echo "$PWD" | sed 's|^/|-|; s|/|-|g') SESSION_DIR="$HOME/.claude/projects/$ENCODED" -
List recent non-agent sessions:
ls -lt "$SESSION_DIR"/*.jsonl | grep -v "agent-" | head -5 -
Extract user messages from each:
cat "$SESSION_DIR/<session-id>.jsonl" | jq -r ' select(.type == "user") | .timestamp + " | " + ( if .message.content | type == "string" then .message.content[0:300] else (.message.content[0].text // "N/A")[0:300] end ) ' | grep -v "N/A" | grep -v "command-name" | head -5 -
Present results in a clean, organized format grouped by session with timestamps.
Troubleshooting
No sessions found: Verify the project path encoding matches exactly. Check ls ~/.claude/projects/ for the correct directory name.
jq parse errors: Some lines may have malformed JSON. Add 2>/dev/null to suppress errors.
Empty content: Some user messages (like tool approvals) have array content without text. Filter with grep -v "N/A".
