Skip to content

Instantly share code, notes, and snippets.

@michaelewens
Last active March 4, 2026 04:59
Show Gist options
  • Select an option

  • Save michaelewens/9a1bc5a97f3f9bbb79453e5b682df462 to your computer and use it in GitHub Desktop.

Select an option

Save michaelewens/9a1bc5a97f3f9bbb79453e5b682df462 to your computer and use it in GitHub Desktop.
"Automatic" logging in Claude Code

Log Reminder Hook for Claude Code

A UserPromptSubmit hook that nudges Claude to log session work before context compaction wipes it out.

The Problem

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.

Why Not a Stop Hook?

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.

The Solution

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.

How It Works

  1. The UserPromptSubmit hook fires when the user sends a message
  2. The script checks when _Log.md was last modified (mtime)
  3. A counter in /tmp/claude-log-reminder/ tracks prompts since that modification
  4. If _Log.md gets updated (by any means — hook, manual edit, or Claude), the counter resets
  5. Every THRESHOLD prompts (default: 5), the reminder fires again

No-op conditions (script exits silently):

  • No CLAUDE_PROJECT_DIR environment variable
  • No _Log.md in 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.

Installation

1. Save the script

mkdir -p ~/.claude/scripts
cp log-reminder.py ~/.claude/scripts/log-reminder.py
chmod +x ~/.claude/scripts/log-reminder.py

2. Add the hooks to ~/.claude/settings.json

Add 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

3. Ensure your projects have _Log.md

The script only activates in projects with a _Log.md file in the root. No log file = no reminders.

Configuration

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

The Script

#!/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()

Companion: The log-entry Skill

The reminder tells Claude to use the log-entry skill — here's what that skill does and how to set it up.

Skill file: ~/.claude/skills/log-entry/SKILL.md

---
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 results

New entries go at the top of the current day's list. New days go at the top of the file.

Slash command: ~/.claude/commands/log.md

# /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)

Design Decisions

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment