Auto-Logging Accomplishments from Claude Code Sessions
The Problem
I track accomplishments in my Obsidian daily note under an #### Accomplished section. The intent is simple: write down what moved forward today so the weekly review has something to work with. Without it, the review is guesswork — trying to reconstruct a week from memory.
The section is in my daily note template and rarely gets filled in.
The pattern is familiar: I spend an hour in a Claude Code session — editing files, drafting emails, closing out a project — and then move on. By evening, the details have blurred. The Accomplished section stays empty, and the weekly review sees a blank week where a productive one happened.
The irony is that Claude was there for all of it. The session transcript has a complete record of what happened. The problem isn’t missing information — it’s that nobody writes it down.
The Solution
A CLAUDE.md instruction that tells Claude to log accomplishments when a session wraps up. This works when you explicitly end a conversation — “let’s wrap up,” or switching topics. Claude has full context and writes directly to the daily note.
I also tried an automated approach — a SessionEnd hook that fires on /clear, reads the transcript, and extracts accomplishments via Haiku. It worked, but I eventually removed it. More on that below.
How To
Approach 1: CLAUDE.md Instruction
Add this to your project-level CLAUDE.md (the one in your vault root, not the global one):
### Logging Accomplishments to Daily Note
When a substantial work session ends (user says they're done, switches topics),
log what was accomplished to today's daily note (if it exists).
**Where:** Under the `#### Accomplished` section in the daily note.
**Format:** One bullet per thing that meaningfully moved forward — "what changed
today," not every micro-step. Tag with life area in brackets. Link to the project
or file where details live.
- Closed out project review — follow-up email, captured next steps → [Project File](/obsidian/project-file/) [work]The key calibration: grain size matters. The first version I tried logged every micro-step — “generated PDF,” “updated skill file,” “edited three lines in config.” That’s noise. The useful level is “what moved forward” — one line per meaningful outcome, with a link to where the details live. The weekly review needs to scan these quickly, not read a changelog.
The life area tags ([work], [health], [community], etc.) are optional but useful if your weekly review groups by area. Match them to whatever categories you use.
Where the Log Goes
The accomplishments don’t just sit in the daily note looking pretty. My weekly review skill scans all open projects and classifies them by staleness — but staleness is measured by log entries, and the accomplishments are a key input. When Claude logs “closed out project review — follow-up email, next steps → PR-2026-Some Project,” the weekly review sees that project had activity this week. Without the log, the review would flag it as stale even though real work happened.
The life area tags matter here too. The weekly review groups accomplishments by area — work, health, community — to give a picture of where energy actually went, not just where it was planned to go. A week with twenty [work] entries and zero [health] entries tells a story the calendar doesn’t.
The Hook I Tried and Removed
I also built an automated version — a SessionEnd hook that fires when you /clear a session. It:
- Reads the session transcript
- Extracts the conversation text via
jq - Sends it to Claude Haiku for accomplishment extraction
- Writes the result to the daily note
Hook Registration
Create a project-level .claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/on-clear-log-accomplishments.sh"
}
]
}
]
}
}The matcher: "clear" ensures this only fires on /clear, not on other session-end events like closing the terminal.
The Hook Script
The SessionEnd hook receives a JSON payload on stdin with a transcript_path field — the path to the session’s .jsonl transcript file. The script extracts conversation text, sends it to Haiku for accomplishment extraction, validates the output, and writes to the daily note.
#!/bin/zsh
# Hook: SessionEnd (matcher: "clear")
# Runs claude -p in the background so /clear returns immediately.
# 30s timeout prevents hangs.
# --no-session-persistence prevents recursion (no transcript, no hooks).
LOGFILE="/tmp/claude-hook-accomplishments.log"
log() { echo "[$(date '+%H:%M:%S')] $1" >> "$LOGFILE"; }
log "=== Hook fired ==="
# Read hook input from stdin (must happen before backgrounding)
INPUT=$(cat)
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
log "EXIT: No transcript file found"
exit 0
fi
# Check that the daily note utility is available
if ! command -v obsidian-daily-note-append &>/dev/null; then
log "EXIT: obsidian-daily-note-append not found in PATH"
exit 0
fi
# Extract conversation from last 200 lines of transcript.
# Only user and assistant text — skip thinking blocks, tool calls, tool results.
CONVERSATION=$(tail -200 "$TRANSCRIPT_PATH" | jq -r '
select(.message.role == "user" or .message.role == "assistant") |
.message as $msg |
$msg.content[] |
select(.type == "text") |
"\($msg.role): \(.text)"
' 2>/dev/null | tail -100)
if [ -z "$CONVERSATION" ]; then
log "EXIT: No conversation text extracted"
exit 0
fi
log "Extracted ${#CONVERSATION} chars of conversation"
# Save prompt + conversation to temp file for the background process
TMPFILE=$(mktemp /tmp/claude-hook-conv.XXXXXX)
cat > "$TMPFILE" <<'PROMPT_END'
You are an accomplishment extractor. You will receive a conversation
transcript between a user and an AI assistant.
YOUR ONLY JOB: identify what meaningful work the USER accomplished
during this session, and output bullet points summarizing it.
CRITICAL RULES:
- You are NOT the assistant from the conversation. Do not continue
the conversation. Do not generate responses or actions.
- Output ONLY bullet points starting with "- " or the word "SKIP"
- If no meaningful work was accomplished, output exactly: SKIP
- One bullet per thing that meaningfully moved forward — not micro-steps
- Tag with life area in brackets where relevant
- Link to relevant project or file using [wikilinks](/obsidian/wikilinks/) where possible
EXAMPLE of correct output:
- Closed out project review — follow-up email, next steps → [Project File](/obsidian/project-file/) [work]
- Set up automated logging via Claude Code hooks [work]
EXAMPLE of WRONG output (do NOT do this):
- I need your approval to call the API...
- Let me help you with...
CONVERSATION TRANSCRIPT:
PROMPT_END
echo "" >> "$TMPFILE"
echo "$CONVERSATION" >> "$TMPFILE"
# Background: call claude -p, validate, write to daily note, clean up
(
log "Background: calling claude -p with 30s timeout"
RESULT=$(timeout 30s claude -p --model haiku \
--no-session-persistence < "$TMPFILE" 2>>"$LOGFILE") || {
log "Background ERROR: claude -p failed or timed out (exit $?)"
rm -f "$TMPFILE"
exit 0
}
rm -f "$TMPFILE"
log "Background: Haiku returned: $RESULT"
# Validate: must be SKIP, empty, or start with "- "
if [ -z "$RESULT" ] || [ "$RESULT" = "SKIP" ]; then
log "Background: Nothing to log"
exit 0
fi
if ! echo "$RESULT" | head -1 | grep -q '^- '; then
log "Background: Rejected (doesn't look like bullets): $RESULT"
exit 0
fi
# Write to daily note using the shared utility
echo "$RESULT" | obsidian-daily-note-append \
--section "#### Accomplished" \
--before "*Write down" \
--stdin 2>>"$LOGFILE" && \
log "Background DONE: Written to daily note" || \
log "Background ERROR: obsidian-daily-note-append failed"
) &
log "Hook exiting (background process launched)"Make the script executable: chmod +x .claude/hooks/on-clear-log-accomplishments.sh
The Daily Note Utility
The hook delegates file manipulation to a standalone utility: obsidian-daily-note-append. It lives in ~/dev/dotfiles/bin/ alongside other vault scripts and handles finding the daily note, locating the target section, and inserting text at the right position.
# Append a bullet to today's Accomplished section
obsidian-daily-note-append --section "#### Accomplished" "- Did a thing [work]"
# Insert before a specific marker line
obsidian-daily-note-append --section "#### Accomplished" --before "*Write down" "- Entry [work]"
# Pipe from another command
echo "- Piped entry" | obsidian-daily-note-append --section "#### Accomplished" --stdin
# Target a different date
obsidian-daily-note-append --section "#### Accomplished" --date 2026-03-11 "- Backdated [work]"
# Preview without writing
obsidian-daily-note-append --section "#### Accomplished" --dry-run "- Test [work]"The utility reads the vault path from $OBSIDIAN_VAULT (set in ~/.zshenv), so no script needs to hardcode it. The daily note path convention — Planner/Daily Notes/YYYY/MM-Month/YYYY-MM-DD.md — is encoded in one place.
The --before flag is key: it inserts text above a marker line within the section rather than blindly appending at the end. This keeps accomplishment bullets above the “write down 3 good things” prompt in the template.
Separating this into a utility pays off immediately — the hook script uses it, but so can any future script or hook that needs to write to a daily note section. The nightly Toggl summary and Things task sync both solve variations of the same problem (insert structured content into a daily note section), and could eventually share this utility.
Understanding the Transcript Format
Claude Code stores session transcripts as .jsonl files — one JSON object per message. Each object has a message field with role (user or assistant) and content (an array of content blocks). Content blocks have a type: text for visible conversation, thinking for Claude’s reasoning, tool_use for tool calls, and tool_result for tool outputs.
The jq filter in the script extracts only text blocks from user and assistant messages — the actual conversation, stripped of everything else. This keeps the input to Haiku small and focused.
tail -200 grabs the last 200 JSONL lines (not 200 messages — a single exchange can span multiple lines). The | tail -100 on the extracted text provides a second limit on the actual conversation content sent to Haiku. For long sessions, this means you’re working with the tail end — usually the most recent and relevant work.
What I Learned
The CLAUDE.md approach won. I ran both approaches for a few weeks. The hook worked mechanically — it extracted accomplishments and wrote them to the daily note. But the 10-20 second API call on every /clear added friction that didn’t feel proportional to the value. Most sessions are short enough that the CLAUDE.md instruction covers them, and the log entries it produces are better quality because Claude has the full conversation context, not just the tail of a transcript filtered through Haiku.
I removed the hook and kept the CLAUDE.md instruction. The Accomplished section still gets filled in most of the time, which is good enough for the weekly review.
Haiku will roleplay as the original assistant. The first version of the extraction prompt was straightforward: “extract accomplishments from this conversation.” Haiku read the transcript and generated a response as if it were the assistant — including an API approval request that got written straight into the daily note. The fix was explicit negative instructions: “You are NOT the assistant. Do not continue the conversation. Do not generate tool calls.” Plus output validation that rejects anything not starting with - . If you try the hook approach, this is the trap to watch for.
Background execution matters for hooks. The claude -p call takes 10-20 seconds. Without backgrounding, /clear hangs for that duration. The pattern: read stdin in the foreground (before the parent process closes pipes), then fork the slow work into a background subshell with &. Even with backgrounding, that latency is why I eventually dropped it — knowing something is churning in the background after every /clear felt wasteful.
--no-session-persistence prevents recursion. When claude -p runs, it’s a full Claude Code process that could fire its own hooks. --no-session-persistence prevents it from saving a transcript, which means no SessionEnd hook fires when it finishes. Without this flag, your accomplishment-logging hook triggers itself.
Grain size is the real calibration. The first version logged every micro-step. The useful level is “what moved forward” — one line per meaningful outcome, with a link to where details live. The weekly review needs to scan these quickly, not read a changelog.
Caveats
Daily note must already exist. Neither approach creates daily notes — they only append to existing ones. If the note doesn’t exist, the log entry is silently lost.
The CLAUDE.md instruction isn’t automatic. You need to signal that a session is wrapping up — “let’s wrap up,” “log that,” or similar. If you just close the terminal, nothing gets logged. This is the gap the hook was supposed to fill, but the hook’s latency cost wasn’t worth it.
The extraction prompt needs tuning (if you try the hook). The life area tags, wikilink conventions, and “what counts as meaningful” are specific to how you use your vault. The prompt in the script is a starting point — adjust the examples and rules to match your workflow.
The $OBSIDIAN_VAULT convention. The hook script uses #!/bin/zsh so it picks up $OBSIDIAN_VAULT from ~/.zshenv automatically. If you use bash, you’ll need to source the variable explicitly or set it in ~/.bashrc. The daily note utility and the Toggl scripts all use the same variable — define it once, every script finds the vault.