Hooks that log session milestones and research URLs from Claude Code conversations. Designed as a data source for tools like claude-code-history-viewer — adding structured event data alongside the raw JSONL transcripts.
Two log files, both JSONL (one JSON object per line):
| Log file | What it captures | Events |
|---|---|---|
research-log.jsonl |
Every URL fetched or searched during sessions | PostToolUse on WebFetch, WebSearch |
session-milestones.jsonl |
Key moments within sessions | PreCompact, SubagentStop |
Research log — Claude Code sessions often involve deep literature review via WebFetch and WebSearch. This content lives only in conversation context and vanishes when the session ends. The hook captures every URL with metadata for later retrieval.
Session milestones — The session transcript JSONL already records every message, but some moments are structurally significant:
- PreCompact — The context window is full and about to be compressed. This is the point of peak information density in a session. A viewer can use this as a "bookmark here" marker.
- SubagentStop (filtered to
Explore,Plan,general-purpose) — A deep research or planning agent just completed, often involving 20-100+ tool calls. Marks when substantial autonomous work finished.
- Claude Code CLI installed
jqavailable on PATH (brew install jqon macOS)- Python 3 available (used for URL encoding in milestone hook)
Place these two files in ~/.claude/hooks/ and make them executable:
mkdir -p ~/.claude/hooks
# Copy the scripts (see Hook Scripts section below)
chmod +x ~/.claude/hooks/log-research.sh
chmod +x ~/.claude/hooks/log-session-milestones.shEdit ~/.claude/settings.json and add a hooks key (or merge into existing hooks):
{
"hooks": {
"PostToolUse": [
{
"matcher": "WebFetch|WebSearch",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/log-research.sh",
"async": true
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/log-session-milestones.sh",
"async": true
}
]
}
],
"SubagentStop": [
{
"matcher": "Explore|Plan|general-purpose",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/log-session-milestones.sh",
"async": true
}
]
}
]
}
}By default, both scripts write to ~/Documents/dev/. Edit the LOG_FILE variable at the top of each script to change the destination:
# In log-research.sh
LOG_FILE="$HOME/Documents/dev/research-log.jsonl"
# In log-session-milestones.sh
LOG_FILE="$HOME/Documents/dev/session-milestones.jsonl"Hooks are snapshotted at session startup. Start a new session for changes to take effect.
After your first WebFetch or WebSearch, check:
tail -3 ~/Documents/dev/research-log.jsonl | jq .After a context compaction or subagent completion:
tail -3 ~/Documents/dev/session-milestones.jsonl | jq .Three entry types: fetch, search, and search_result.
{
"timestamp": "2026-03-02T18:42:15Z",
"type": "fetch",
"url": "https://arxiv.org/abs/2602.03766",
"prompt": "Extract the key findings about cortical magnification",
"category": "research",
"project": "scrutinizer-www",
"session": "86fc3c77-c2b2-4f17-ac74-2b39751238ef",
"transcript_path": "/Users/you/.claude/projects/-Users-you-myproject/86fc3c77-....jsonl"
}{
"timestamp": "2026-03-02T18:40:02Z",
"type": "search",
"query": "CLAUDE.md best practices tips effective 2026",
"project": "psychodeli-webgl-port",
"session": "86fc3c77-c2b2-4f17-ac74-2b39751238ef",
"transcript_path": "/Users/you/.claude/projects/.../.jsonl"
}{
"timestamp": "2026-03-02T18:40:02Z",
"type": "search_result",
"url": "https://www.humanlayer.dev/blog/writing-a-good-claude-md",
"title": "Writing a good CLAUDE.md | HumanLayer Blog",
"query": "CLAUDE.md best practices tips effective 2026",
"category": "blog",
"project": "psychodeli-webgl-port",
"session": "86fc3c77-c2b2-4f17-ac74-2b39751238ef",
"transcript_path": "/Users/you/.claude/projects/.../.jsonl"
}| Field | Type | Present in | Description |
|---|---|---|---|
timestamp |
ISO 8601 | all | UTC time of the event |
type |
string | all | "fetch", "search", or "search_result" |
url |
string | fetch, search_result | The URL fetched or found in results |
prompt |
string | fetch | The prompt sent to WebFetch (what to extract from the page) |
query |
string | search, search_result | The search query string |
title |
string | search_result | Page title from search results (best-effort) |
category |
string | fetch, search_result | Auto-detected from URL domain (see below) |
project |
string | all | basename of the working directory |
session |
string | all | Claude Code session ID |
transcript_path |
string | all | Absolute path to the session's JSONL transcript file |
Assigned automatically by URL domain pattern matching:
| Category | Matching domains |
|---|---|
research |
arxiv.org, pubmed, pmc.ncbi, jov.arvojournals, biorxiv.org, semanticscholar, springer.com/article, elifesciences.org, researchgate.net/publication, dspace.mit.edu, sciencedirect.com |
docs |
github.com, docs., developer., mdn.*, readthedocs, deepwiki.com |
blog |
medium.com, dev.to, substack.com, *.blog, wordpress |
news |
sciencedaily, arstechnica, theverge, wired.com, techcrunch, news.ycombinator |
reference |
wikipedia.org, stackoverflow.com, stackexchange.com |
other |
Everything else |
The categorization lives in the categorize_url() function in log-research.sh. Edit it to match your domains.
{
"timestamp": "2026-03-02T20:15:33Z",
"milestone": "compaction_auto",
"description": "Context compaction (auto) — session at peak density",
"session_id": "86fc3c77-c2b2-4f17-ac74-2b39751238ef",
"transcript_path": "/Users/you/.claude/projects/-Users-you-myproject/86fc3c77-....jsonl",
"deeplink": "claude-history://session/%2FUsers%2Fyou%2F.claude%2Fprojects%2F...%2F86fc3c77-....jsonl",
"project": "psychodeli-webgl-port",
"event": "PreCompact"
}{
"timestamp": "2026-03-02T19:48:12Z",
"milestone": "agent_Explore",
"description": "Explore agent completed",
"session_id": "86fc3c77-c2b2-4f17-ac74-2b39751238ef",
"transcript_path": "/Users/you/.claude/projects/-Users-you-myproject/86fc3c77-....jsonl",
"deeplink": "claude-history://session/%2FUsers%2Fyou%2F.claude%2Fprojects%2F...%2F86fc3c77-....jsonl",
"project": "scrutinizer-www",
"event": "SubagentStop"
}| Field | Type | Description |
|---|---|---|
timestamp |
ISO 8601 | UTC time of the milestone |
milestone |
string | Machine-readable milestone type (e.g. compaction_auto, agent_Explore) |
description |
string | Human-readable description |
session_id |
string | Claude Code session ID |
transcript_path |
string | Absolute path to the session's JSONL transcript |
deeplink |
string | Proposed claude-history:// protocol URL (not yet implemented) |
project |
string | basename of the working directory |
event |
string | The Claude Code hook event that fired (PreCompact, SubagentStop) |
# Research: all unique URLs fetched today
jq -r 'select(.type=="fetch") | "\(.category)\t\(.url)"' research-log.jsonl | sort -u
# Research: group by category
jq -r 'select(.url) | .category' research-log.jsonl | sort | uniq -c | sort -rn
# Research: everything from a specific session
jq 'select(.session=="86fc3c77-c2b2-4f17-ac74-2b39751238ef")' research-log.jsonl
# Milestones: all compactions (sessions that hit context limits)
jq 'select(.milestone | startswith("compaction"))' session-milestones.jsonl
# Milestones: sessions with the most subagent completions
jq -r 'select(.milestone | startswith("agent")) | .session_id' session-milestones.jsonl | sort | uniq -c | sort -rn
# Cross-reference: what was being researched when context filled up?
SESS=$(jq -r 'select(.milestone=="compaction_auto") | .session_id' session-milestones.jsonl | tail -1)
jq --arg s "$SESS" 'select(.session==$s and .type=="fetch")' research-log.jsonlThe transcript_path field in both logs points to the session's JSONL file:
~/.claude/projects/{encoded-project-path}/{session-uuid}.jsonl
The deeplink field in milestones proposes a claude-history:// URL scheme. Once a viewer implements protocol handling, these become clickable links from any tool that reads the logs. The session UUID in the filename is the primary key.
Source: ~/.claude/hooks/log-research.sh