A UserPromptSubmit hook that nudges Claude to log session work before context compaction wipes it out.
Claude Code instructions like "proactively log your work" in CLAUDE.md are unreliable. Claude gets absorbed in the task and skips logging. When context compaction hits, the session's work history is lost.
A PreCompact hook can't solve this either: by the time compaction triggers, the context window is nearly full. Claude wouldn't have room to reason about what happened and write a meaningful log entry.
The original version used a Stop hook (fires after each Claude response). The problem: Stop hook stdout prints to the user's terminal but is not injected into the model's conversation context. Claude literally never sees the reminder. The counter increments, the warning prints, and nobody acts on it.
UserPromptSubmit hooks fire when the user sends a message, and their stdout is injected as a system reminder visible to Claude. Same script, different hook — now Claude actually responds to the warning.
This hook fires on every user prompt and tracks how many prompts have occurred since _Log.md was last modified. After a configurable threshold (default: 5 prompts), it outputs a warning that Claude sees in the conversation:
⚠️ SESSION LOG OVERDUE: You have made ~5 responses without updating _Log.md.
Before continuing, add a log entry summarizing what you've accomplished this session.
Use the log-entry skill or manually edit _Log.md.
The reminder repeats every N prompts (at 5, 10, 15, etc.) and resets only when _Log.md is modified. Because this message is injected into the conversation as hook output, Claude treats it as actionable input rather than a passive instruction to ignore.
- The
UserPromptSubmithook fires when the user sends a message - The script checks when
_Log.mdwas last modified (mtime) - A counter in
/tmp/claude-log-reminder/tracks prompts since that modification - If
_Log.mdgets updated (by any means — hook, manual edit, or Claude), the counter resets - Every
THRESHOLDprompts (default: 5), the reminder fires again
No-op conditions (script exits silently):
- No
CLAUDE_PROJECT_DIRenvironment variable - No
_Log.mdin the project root - Counter below threshold
- Counter not on a multiple of threshold
Note: The counter is shared across concurrent sessions for the same project. If you have two Claude Code sessions open in the same project directory, prompts in both sessions increment the same counter. The warning will fire in whichever session hits the threshold.
mkdir -p ~/.claude/scripts
cp log-reminder.py ~/.claude/scripts/log-reminder.py
chmod +x ~/.claude/scripts/log-reminder.pyAdd these to your hooks object:
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/log-reminder.py",
"timeout": 5000
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/log-reminder.py --precompact",
"timeout": 5000
}
]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/log-reminder.py --post-compact",
"timeout": 5000
}
]
}
]The three hooks cover different scenarios:
- UserPromptSubmit: Regular periodic reminders (every N prompts)
- PreCompact: Urgent "log now" before context compaction
- SessionStart (compact): Re-inject reminder after compaction in case Claude missed the PreCompact warning
The script only activates in projects with a _Log.md file in the root. No log file = no reminders.
Edit the constant at the top of the script:
THRESHOLD = 5 # Prompts without a log update before reminding (repeats every N)THRESHOLD — how many prompts between reminders:
- 5 (default) for general use and short task sessions
- 10 for longer research/exploration sessions
- 15-20 for marathon sessions where you don't want frequent interruptions
#!/usr/bin/env python3
"""
Hook script to remind Claude to log session work before context is lost.
Modes:
(no args) — UserPromptSubmit hook. Increments counter, reminds every
THRESHOLD prompts. Output is injected into model context.
--precompact — PreCompact hook. If log is overdue, prints urgent reminder
to stdout (injected into context before compaction).
--post-compact — SessionStart(compact) hook. If log is still overdue after
compaction, re-injects reminder into fresh context.
Counter resets when _Log.md is modified. Counter is shared across concurrent
sessions for the same project.
"""
import json
import os
import sys
from pathlib import Path
THRESHOLD = 5
STATE_DIR = Path("/tmp/claude-log-reminder")
def get_state_path(project_dir: str) -> Path:
"""State file keyed to project directory."""
safe_name = project_dir.replace("/", "_").strip("_")
return STATE_DIR / f"{safe_name}.json"
def read_state(state_path: Path) -> dict:
defaults = {"stop_count": 0, "last_log_mtime": 0.0}
try:
data = json.loads(state_path.read_text())
return {**defaults, **data}
except (FileNotFoundError, json.JSONDecodeError):
return defaults
def write_state(state_path: Path, state: dict):
STATE_DIR.mkdir(parents=True, exist_ok=True)
state_path.write_text(json.dumps(state))
def check_log_updated(state, current_mtime, state_path):
"""If log was modified, reset counter and return True."""
if current_mtime != state["last_log_mtime"]:
state = {"stop_count": 0, "last_log_mtime": current_mtime}
write_state(state_path, state)
return True
return False
def mode_prompt(state, state_path, current_mtime):
"""UserPromptSubmit hook — increment counter, remind at multiples of THRESHOLD."""
if check_log_updated(state, current_mtime, state_path):
return
state["stop_count"] += 1
if state["stop_count"] >= THRESHOLD and state["stop_count"] % THRESHOLD == 0:
print(
"⚠️ SESSION LOG OVERDUE: You have made ~{} responses without updating _Log.md. "
"Before continuing, add a log entry summarizing what you've accomplished this session. "
"Use the log-entry skill or manually edit _Log.md.".format(state["stop_count"])
)
write_state(state_path, state)
def mode_precompact(state, state_path, current_mtime):
"""PreCompact hook — if log is overdue, inject urgent reminder before compaction."""
if check_log_updated(state, current_mtime, state_path):
return
if state["stop_count"] >= THRESHOLD:
print(
"⚠️ CONTEXT COMPACTION IMMINENT — LOG NOW: You have made ~{} responses "
"without updating _Log.md and context is about to be compacted. "
"IMMEDIATELY add a log entry summarizing this session's work before "
"details are lost. Use the log-entry skill or manually edit _Log.md.".format(
state["stop_count"]
)
)
def mode_post_compact(state, state_path, current_mtime):
"""SessionStart(compact) hook — re-inject reminder into fresh post-compaction context."""
if check_log_updated(state, current_mtime, state_path):
return
if state["stop_count"] >= THRESHOLD:
print(
"⚠️ SESSION LOG OVERDUE (post-compaction): ~{} responses since last _Log.md update. "
"Context was just compacted — details may have been lost. "
"Add a log entry now summarizing this session's work. "
"Use the log-entry skill or manually edit _Log.md.".format(
state["stop_count"]
)
)
def main():
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
if not project_dir:
sys.exit(0)
log_file = Path(project_dir) / "_Log.md"
if not log_file.exists():
sys.exit(0)
state_path = get_state_path(project_dir)
state = read_state(state_path)
current_mtime = log_file.stat().st_mtime
mode = sys.argv[1] if len(sys.argv) > 1 else "--prompt"
if mode == "--precompact":
mode_precompact(state, state_path, current_mtime)
elif mode == "--post-compact":
mode_post_compact(state, state_path, current_mtime)
else:
mode_prompt(state, state_path, current_mtime)
sys.exit(0)
if __name__ == "__main__":
main()The reminder tells Claude to use the log-entry skill — here's what that skill does and how to set it up.
---
name: log-entry
description: Add timestamped log entries to _Log.md file when user says "Log:" followed by a message
version: 1.1.0
---The skill activates when:
- The user says "Log: [message]"
- The user runs
/log [message] - Claude is reminded by the log-reminder hook
It writes entries to _Log.md at the project root using this format:
## 2026-02-07
- 14:35: Refactored authentication module
- 12:10: Created [[utils/helpers.py]]
## 2026-02-06
- 16:22: Fixed pagination bug in search resultsNew entries go at the top of the current day's list. New days go at the top of the file.
# /log command
Add a timestamped log entry to a `_Log.md` file.
## Usage
/log Your log message here
/log @"Project Name" Your log message here
## Cross-Project Logging
Use the `@"project name"` prefix to log to a different project's `_Log.md`:
/log @"PE Gender" Updated analysis with new dataset
/log @"Teaching" Scheduled meeting with TA for next week
## Default Behavior (No @prefix)
When no `@"project"` prefix is given, log to the current project:
1. **Git root**: Run `git rev-parse --show-toplevel` - if successful, use that directory
2. **CLAUDE.md search**: Look for `CLAUDE.md` in current and parent directories
3. **Fallback**: Use the current working directory
## Format
## YYYY-MM-DD
- HH:MM: Most recent entry
- HH:MM: Earlier entry today
## YYYY-MM-DD
- HH:MM: Previous day's entry
## File References
When referencing files, use Obsidian wiki-link format:
- `[[cases/kiran/file.xlsx]]` (with path from project root)Why UserPromptSubmit instead of Stop?
Stop hook stdout prints to the terminal but is not injected into the model's conversation context. Claude never sees the reminder. UserPromptSubmit output is injected as a system reminder that Claude receives before generating its response.
Why not PreCompact alone?
PreCompact runs a shell command — it can't invoke Claude skills or make Claude reason. And by that point, context is nearly full anyway. The UserPromptSubmit hook fires while Claude still has full context and can act on the reminder.
Why periodic instead of one-shot?
The original v1 design set a reminded = True flag after the first warning, preventing repeats. The problem: if the reminder fires during a conversation that later gets compacted or runs out of context, the flag persists and the next session never gets reminded. Periodic reminders (every N prompts) are more robust — a missed reminder at prompt 5 gets another chance at prompt 10.
Why no session gap reset?
An earlier version reset the counter after 5 minutes of inactivity to detect "new sessions." In practice, this meant stepping away for coffee reset the counter, defeating the purpose. The only meaningful reset is when _Log.md is actually modified — if work happened but wasn't logged, the reminder should persist regardless of time gaps.
Why use mtime instead of counting log entries?
Mtime catches all updates: auto-logged file creations, manual /log entries, and direct edits. It doesn't matter how the log was updated, just that it was.
Shared counter across sessions: The counter state is keyed by project directory, not by session. If you have two Claude Code sessions open in the same project, both increment the same counter. This is intentional — it prevents double-counting (each session doesn't maintain its own separate threshold), though it means the warning fires in whichever session happens to hit the multiple.