From 0117fd7ef94919a6aecd07e3645486530187e5f4 Mon Sep 17 00:00:00 2001 From: Mark Ayers Date: Tue, 24 Feb 2026 14:03:53 -0500 Subject: [PATCH 1/4] feat: scope hook logs to per-session directories Derive session directory from $CLAUDE_SESSION_ID (last 8 chars) so each session writes to its own logs// directory instead of shared flat files. Replace size-based log rotation with 7-day age-based cleanup of session directories, consistent with debug and shell-snapshot retention. Co-Authored-By: Claude Opus 4.6 --- README.md | 17 ++++++++++------- hooks/log-git-commands.sh | 7 +++++-- hooks/log-hook-event.sh | 7 +++++-- hooks/session-cleanup.sh | 19 +++++++++++++++++++ settings.json | 9 +++++++++ 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100755 hooks/session-cleanup.sh diff --git a/README.md b/README.md index d7ed796..ab735bf 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Don't install this. Just steal what you like. | `file-history/` | Change tracking for edited files | | `session-env/` | Environment snapshots per session | | `sessions/` | Session state and conversation data | -| `logs/` | Session and commit history logs | +| `logs/` | Per-session hook and git command logs | | `debug/` | Session debug output | | `shell-snapshots/` | Shell environment captures | | `cache/` | Temporary cached data | @@ -191,20 +191,23 @@ This configuration includes 11 hooks: - **load-session-context.sh** - Injects git repository context at session start - **evaluate-session.js** - Analyzes session for learnings and patterns -#### Notification Hooks (Notification) +#### Cleanup Hooks (SessionEnd) -- **notify-idle.sh** - macOS notification when Claude is ready for input +- **session-cleanup.sh** - Removes session log directories older than 7 days and cleans stale debug/snapshot files on session exit ## Common Operations ### Review Recent Activity ```bash -# Check recent session logs -tail -n 50 logs/session-log.txt +# List session log directories +ls logs/ -# View commit history -cat logs/commit-log.txt +# Check hook events for a session (last 8 chars of session ID) +tail -n 50 logs//hook-events.log + +# View git commands for a session +cat logs//git-commands.log ``` ### Inspect Project Metadata diff --git a/hooks/log-git-commands.sh b/hooks/log-git-commands.sh index 4cd2c33..7815497 100755 --- a/hooks/log-git-commands.sh +++ b/hooks/log-git-commands.sh @@ -9,8 +9,11 @@ description=$(jq -r '.tool_input.description // "No description"' 2>/dev/null || # Only log git/gh/dot commands if echo "$command" | grep -qE '^(git|gh|dot)\s'; then timestamp=$(date '+%Y-%m-%d %H:%M:%S') - mkdir -p ~/.claude/logs - echo "[$timestamp] $command | $description" >>~/.claude/logs/git-commands.log + session_id="${CLAUDE_SESSION_ID: -8}" + session_id="${session_id:-default}" + log_dir=~/.claude/logs/"$session_id" + mkdir -p "$log_dir" + echo "[$timestamp] $command | $description" >>"$log_dir"/git-commands.log fi exit 0 # Never block, just log diff --git a/hooks/log-hook-event.sh b/hooks/log-hook-event.sh index 37f220f..df3e157 100755 --- a/hooks/log-hook-event.sh +++ b/hooks/log-hook-event.sh @@ -4,7 +4,10 @@ event_name="$1" timestamp=$(date '+%Y-%m-%d %H:%M:%S') -mkdir -p ~/.claude/logs +session_id="${CLAUDE_SESSION_ID: -8}" +session_id="${session_id:-default}" +log_dir=~/.claude/logs/"$session_id" +mkdir -p "$log_dir" input=$(cat) tool="" @@ -23,6 +26,6 @@ if [ -n "$tool_input" ] && [ "$tool_input" != "null" ]; then line="$line input=$tool_input" fi -echo "$line" >>~/.claude/logs/hook-events.log +echo "$line" >>"$log_dir"/hook-events.log exit 0 diff --git a/hooks/session-cleanup.sh b/hooks/session-cleanup.sh new file mode 100755 index 0000000..f783078 --- /dev/null +++ b/hooks/session-cleanup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# SessionEnd cleanup - rotate logs and archive stale session data +# Note: Intentionally no 'set -euo pipefail' - hooks must always exit 0 + +log_dir=~/.claude/logs + +# Remove session log directories older than 7 days +find "$log_dir" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true + +# Remove legacy flat log files from before session-scoped logging +rm -f "$log_dir/hook-events.log" "$log_dir/hook-events.log.1" "$log_dir/git-commands.log" 2>/dev/null || true + +# Remove debug files older than 7 days +find ~/.claude/debug -name "*.txt" -mtime +7 -delete 2>/dev/null || true + +# Remove stale shell snapshots older than 7 days +find ~/.claude/shell-snapshots -type f -mtime +7 -delete 2>/dev/null || true + +exit 0 diff --git a/settings.json b/settings.json index bb3b30b..08aaf50 100644 --- a/settings.json +++ b/settings.json @@ -284,6 +284,15 @@ } ], "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/session-cleanup.sh", + "timeout": 10 + } + ] + }, { "hooks": [ { From 00b43ada991f6629be42c1fae463e93438c286a7 Mon Sep 17 00:00:00 2001 From: Mark Ayers Date: Tue, 24 Feb 2026 14:17:57 -0500 Subject: [PATCH 2/4] fix: extract session ID from stdin JSON instead of env var CLAUDE_SESSION_ID is not exposed as an environment variable to hooks. The session_id is available in the JSON payload on stdin. Read stdin first, then extract session_id from the parsed JSON. Co-Authored-By: Claude Opus 4.6 --- hooks/log-git-commands.sh | 9 ++++++--- hooks/log-hook-event.sh | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/hooks/log-git-commands.sh b/hooks/log-git-commands.sh index 7815497..a4b7f39 100755 --- a/hooks/log-git-commands.sh +++ b/hooks/log-git-commands.sh @@ -3,13 +3,16 @@ # # Note: Intentionally no 'set -euo pipefail' - hooks must always exit 0 -command=$(jq -r '.tool_input.command // empty' 2>/dev/null || echo "") -description=$(jq -r '.tool_input.description // "No description"' 2>/dev/null || echo "No description") +input=$(cat) +command=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "") +description=$(echo "$input" | jq -r '.tool_input.description // "No description"' 2>/dev/null || echo "No description") # Only log git/gh/dot commands if echo "$command" | grep -qE '^(git|gh|dot)\s'; then timestamp=$(date '+%Y-%m-%d %H:%M:%S') - session_id="${CLAUDE_SESSION_ID: -8}" + # Extract session ID from stdin JSON (last 8 chars) + session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) + session_id="${session_id: -8}" session_id="${session_id:-default}" log_dir=~/.claude/logs/"$session_id" mkdir -p "$log_dir" diff --git a/hooks/log-hook-event.sh b/hooks/log-hook-event.sh index df3e157..1d4fee7 100755 --- a/hooks/log-hook-event.sh +++ b/hooks/log-hook-event.sh @@ -4,15 +4,18 @@ event_name="$1" timestamp=$(date '+%Y-%m-%d %H:%M:%S') -session_id="${CLAUDE_SESSION_ID: -8}" -session_id="${session_id:-default}" -log_dir=~/.claude/logs/"$session_id" -mkdir -p "$log_dir" input=$(cat) tool="" tool_input="" +# Extract session ID from stdin JSON (last 8 chars) +session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) +session_id="${session_id: -8}" +session_id="${session_id:-default}" +log_dir=~/.claude/logs/"$session_id" +mkdir -p "$log_dir" + if [ -n "$input" ]; then tool=$(echo "$input" | jq -r '.tool // .tool_name // empty' 2>/dev/null) tool_input=$(echo "$input" | jq -c '.tool_input // empty' 2>/dev/null) From 66118afc4e6b2917de0907e6a34936460269098b Mon Sep 17 00:00:00 2001 From: Mark Ayers Date: Tue, 24 Feb 2026 14:23:28 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?stale=20descriptions=20and=20missing=20legacy=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update session-cleanup.sh header comment to match actual behavior - Add git-commands.log.1 to legacy file cleanup - Fix README log-git-commands.sh description (no longer logs to stderr) Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- hooks/session-cleanup.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ab735bf..bd75d0b 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ This configuration includes 11 hooks: #### Logging Hooks (PreToolUse, PostToolUse, and others) -- **log-git-commands.sh** - Logs all git/gh commands to stderr for tracking +- **log-git-commands.sh** - Logs all git/gh commands to per-session log files - **log-hook-event.sh** - Companion logger on every lifecycle event for observability #### Formatting Hooks (PostToolUse) diff --git a/hooks/session-cleanup.sh b/hooks/session-cleanup.sh index f783078..881252c 100755 --- a/hooks/session-cleanup.sh +++ b/hooks/session-cleanup.sh @@ -1,5 +1,5 @@ #!/bin/bash -# SessionEnd cleanup - rotate logs and archive stale session data +# SessionEnd cleanup - delete logs and session data older than 7 days # Note: Intentionally no 'set -euo pipefail' - hooks must always exit 0 log_dir=~/.claude/logs @@ -8,7 +8,7 @@ log_dir=~/.claude/logs find "$log_dir" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true # Remove legacy flat log files from before session-scoped logging -rm -f "$log_dir/hook-events.log" "$log_dir/hook-events.log.1" "$log_dir/git-commands.log" 2>/dev/null || true +rm -f "$log_dir/hook-events.log" "$log_dir/hook-events.log.1" "$log_dir/git-commands.log" "$log_dir/git-commands.log.1" 2>/dev/null || true # Remove debug files older than 7 days find ~/.claude/debug -name "*.txt" -mtime +7 -delete 2>/dev/null || true From 0715d1d479c83822869a0262f3d20c31ad70c073 Mon Sep 17 00:00:00 2001 From: Mark Ayers Date: Tue, 24 Feb 2026 14:26:47 -0500 Subject: [PATCH 4/4] fix: sanitize session ID and guard rm against dash-prefixed dirs Strip non-alphanumeric characters from session_id to prevent path traversal. Add -- to rm -rf in find -exec to handle directory names starting with dashes. Co-Authored-By: Claude Opus 4.6 --- hooks/log-git-commands.sh | 1 + hooks/log-hook-event.sh | 1 + hooks/session-cleanup.sh | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hooks/log-git-commands.sh b/hooks/log-git-commands.sh index a4b7f39..ac27bf9 100755 --- a/hooks/log-git-commands.sh +++ b/hooks/log-git-commands.sh @@ -13,6 +13,7 @@ if echo "$command" | grep -qE '^(git|gh|dot)\s'; then # Extract session ID from stdin JSON (last 8 chars) session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) session_id="${session_id: -8}" + session_id="${session_id//[^a-zA-Z0-9]/_}" session_id="${session_id:-default}" log_dir=~/.claude/logs/"$session_id" mkdir -p "$log_dir" diff --git a/hooks/log-hook-event.sh b/hooks/log-hook-event.sh index 1d4fee7..82492e0 100755 --- a/hooks/log-hook-event.sh +++ b/hooks/log-hook-event.sh @@ -12,6 +12,7 @@ tool_input="" # Extract session ID from stdin JSON (last 8 chars) session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null) session_id="${session_id: -8}" +session_id="${session_id//[^a-zA-Z0-9]/_}" session_id="${session_id:-default}" log_dir=~/.claude/logs/"$session_id" mkdir -p "$log_dir" diff --git a/hooks/session-cleanup.sh b/hooks/session-cleanup.sh index 881252c..461f481 100755 --- a/hooks/session-cleanup.sh +++ b/hooks/session-cleanup.sh @@ -5,7 +5,7 @@ log_dir=~/.claude/logs # Remove session log directories older than 7 days -find "$log_dir" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true +find "$log_dir" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf -- {} + 2>/dev/null || true # Remove legacy flat log files from before session-scoped logging rm -f "$log_dir/hook-events.log" "$log_dir/hook-events.log.1" "$log_dir/git-commands.log" "$log_dir/git-commands.log.1" 2>/dev/null || true