Add session-logger hook for durable session notes#14
Open
karanb192 wants to merge 5 commits into
Open
Conversation
added 5 commits
April 18, 2026 10:51
Single script that handles SessionStart, PostToolUse, and Stop events to write a human-readable markdown log of every Claude Code session: timestamps, cwd, git repo/branch, files touched, and bash commands run. - One install, three registrations (SessionStart + PostToolUse + Stop). - Filename: YYYY-MM-DD_HHmm_<sid-short>.md — sortable + greppable. - Session resume: appends a "Resumed at" marker instead of overwriting. - Bash commands truncated to the first line (200 chars) to avoid logging multi-line command bodies that may contain secrets. - Configurable log directory via top-of-file SESSION_LOG_DIR constant (matches existing SAFETY_LEVEL pattern) or CC_SESSION_LOG_DIR env var. Point it at an Obsidian vault or iCloud folder for cross-device sync. Tests: 18 new, all passing (unit + stdin/stdout integration).
Stop fires at the end of every Claude turn (many times per session), which would write duplicate "Session End" sections and repeatedly overwrite the ended: timestamp. SessionEnd fires once when the session actually ends. - Rename handleStop → handleSessionEnd. - Drop Stop from the event dispatch (add an explanatory comment). - Idempotent finalize: skip if the "ended:" frontmatter line is already filled, so accidentally registering against Stop is harmless. - Update README + install snippet to use SessionEnd. - Tests: add idempotency test and a regression test confirming Stop events do not finalize the note. Bump test badge to 282.
Update the install-example comment in session-logger.js to register PostToolUse with "async": true. PostToolUse fires after every tool call, so making it non-blocking keeps the hot path off Claude's critical path. SessionStart stays sync (note must exist before PostToolUse tries to append); SessionEnd stays sync (finalization must complete before session termination). Ref: https://code.claude.com/docs/en/hooks — async/asyncRewake fields.
Two correctness/safety fixes surfaced in review: - Concurrency: PostToolUse runs with "async": true, so parallel tool calls invoke the script concurrently. The note write is a read-modify-write, so concurrent appends lost entries (last writer wins) — reproduced ~2/10 dropped under 10 parallel calls. Wrap every note write (appendUnderSection + SessionEnd finalize) in a cross-process lockfile (withLock) with stale-lock stealing and a best-effort unlocked fallback. Now 20/20 concurrent entries preserved. - Secrets: single-line truncation didn't protect single-line inline secrets (AWS_SECRET=… aws …, Bearer tokens, mysql --password=…). Add best-effort redactSecrets() over bash commands, masking sensitive env assignments, --password/--token flags, Bearer tokens, and well-known key prefixes (ghp_, xox…, sk-, AKIA…). Conservative — leaves ordinary commands (e.g. cp -pr) untouched. Header/README wording made honest about the best-effort guarantee. Nits: fix the no-op `.filter(l => l !== null)` (ternary now returns null), align the SessionEnd `ended:` regex ([ \t]* in both check and replace), correct the file header (SessionEnd, not Stop). Tests: +6 (redaction unit + e2e, concurrency no-lost-updates). Badge 282 → 288. Full suite 286/288 (2 pre-existing notify-permission webhook-env failures, unrelated).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new hook — session-logger — that writes a human-readable markdown log of every Claude Code session: timestamps, cwd, git repo/branch, files touched, and bash commands run.
The repo's own
CONTRIBUTING.mdand README have hadsession-summaryon the "Ideas for new hooks" list since day one. This ships it, scoped more broadly as a full session logger (covers start, during, and end of session).Why
Claude Code sessions are ephemeral. You finish a session, switch repos, and two days later can't remember which session touched which file or ran which command. This hook gives you a durable, greppable (and Obsidian-friendly) record.
What it does
SessionStart→ creates a new note at<log-dir>/YYYY-MM-DD_HHmm_<sid-short>.mdwith YAML frontmatter (session_id,cwd,git_repo,git_branch,started,ended) and two pre-seeded sections: Files Touched and Commands Run.PostToolUse(Edit | Write | Read | Bash) → appends an entry to the right section.SessionEnd→ fills inended:in frontmatter and appends a final git status.Design choices
SessionEnd, notStop.Stopfires at the end of every Claude turn (many times per session);SessionEndfires once when the session actually ends. The handler is also idempotent — ifended:is already filled, it no-ops, so accidentally registering againstStopis harmless.PostToolUsewrites synchronously during the session, so if the terminal is killed beforeSessionEndfires, all activity up to the last tool call is preserved — the note just doesn't get a finalended:timestamp.hook_event_name.YYYY-MM-DD_HHmm_<sid-short>.md— chronologically sortable in any file explorer, greppable by session id suffix.SessionStartwith the same id, appends aResumed atmarker rather than overwriting.SESSION_LOG_DIRconstant (matches the existingSAFETY_LEVELpattern), overridable viaCC_SESSION_LOG_DIRenv var. Point it at an Obsidian vault or iCloud folder for cross-device sync.{}output.Changes
hook-scripts/session/session-logger.js— new hook (~195 LOC).hook-scripts/tests/session/session-logger.test.js— 20 new tests, all passing (includes an explicit regression test thatStopevents do not finalize the note, and an idempotency test for duplicateSessionEnd).README.md— added the new "Session" section to the hooks table, removedsession-summaryfrom the Ideas list (it's now shipped), bumped test badge 262 → 282.Test plan
node --test hook-scripts/tests/session/session-logger.test.js— 20/20 pass.npm test— 280/282 pass (the 2 failures are pre-existingnotify-permissionSLACK_WEBHOOK env issues onmain, unrelated to this PR — verified by stashing untracked changes and re-running).Update — review fixes (commit
Serialize concurrent note writes + redact inline secrets)Addressed two issues found in review, plus nits:
PostToolUse"async": true, parallel tool calls invoke the script concurrently and the read-modify-write lost entries (reproduced ~2/10 dropped under 10 parallel calls). All note writes now go through a cross-process lockfile (withLock) with stale-lock stealing. Verified 20/20 concurrent entries preserved, no leftover.lockfiles.redactSecrets()over bash commands (sensitive env assignments,--password/--tokenflags,Bearertokens, well-known key prefixes likeghp_,xox…,sk-,AKIA…). Conservative — leaves ordinary commands likecp -pruntouched. Header/README wording corrected to be honest that this is best-effort, not a guarantee..filter(l => l !== null), aligned theended:regex between check and replace, corrected the file header (SessionEnd, notStop).Tests: +6 (redaction unit + e2e, concurrency no-lost-updates) → 288 total, badge bumped.
npm test→ 286/288 (the 2 failures are the pre-existingnotify-permissionwebhook-env tests onmain, unrelated).