Skip to content

Instantly share code, notes, and snippets.

@johnlindquist
Last active December 10, 2025 18:07
Show Gist options
  • Select an option

  • Save johnlindquist/23fac87f6bc589ddf354582837ec4ecc to your computer and use it in GitHub Desktop.

Select an option

Save johnlindquist/23fac87f6bc589ddf354582837ec4ecc to your computer and use it in GitHub Desktop.
Claude Code Hooks: How to Auto-Refresh Context Every N Prompts

Claude Code Hooks: Auto-Refreshing Context Every N Prompts

This explains how to build a system that automatically re-injects important context (like a tools list) into Claude Code conversations every N prompts.

The Problem

Claude Code's SessionStart hook runs once at the beginning of a conversation. In long sessions, that initial context gets pushed far back and the AI may "forget" about it.

Example: You inject a list of 222 custom tools at session start. By prompt 20, the AI stops using them because they're no longer in recent context.

The Solution

Use the UserPromptSubmit hook to:

  1. Track how many prompts have occurred
  2. Every N prompts, re-inject the important context

Step 1: Track Prompt Count

First, we need persistent storage for session statistics. Create a stats module:

// ~/.claude/hooks/src/stats.ts

export interface SessionStats {
  promptCount: number
  lastPromptTime: number
}

const statsCache = new Map<string, SessionStats>()

export async function getSessionStats(sessionId: string): Promise<SessionStats> {
  // Check memory cache first
  if (statsCache.has(sessionId)) {
    return statsCache.get(sessionId)!
  }
  
  // Try to load from disk
  const statsFile = \`/tmp/claude-session-\${sessionId}-stats.json\`
  try {
    const file = Bun.file(statsFile)
    if (await file.exists()) {
      const stats = await file.json()
      statsCache.set(sessionId, stats)
      return stats
    }
  } catch {}
  
  // Return defaults
  return { promptCount: 0, lastPromptTime: 0 }
}

export async function updateSessionStats(
  sessionId: string,
  updater: (stats: SessionStats) => void
): Promise<SessionStats> {
  const stats = await getSessionStats(sessionId)
  updater(stats)
  
  // Save to disk and cache
  const statsFile = \`/tmp/claude-session-\${sessionId}-stats.json\`
  await Bun.write(statsFile, JSON.stringify(stats))
  statsCache.set(sessionId, stats)
  
  return stats
}

Step 2: Create the UserPromptSubmit Hook

This hook runs every time the user sends a message:

// ~/.claude/hooks/UserPromptSubmit.ts

import type { UserPromptSubmitHookInput, HookJSONOutput } from "@anthropic-ai/claude-agent-sdk"
import { updateSessionStats } from "./src/stats"

const input = await Bun.stdin.json() as UserPromptSubmitHookInput

// Increment prompt count
const stats = await updateSessionStats(input.session_id, (stats) => {
  stats.promptCount = (stats.promptCount || 0) + 1
  stats.lastPromptTime = Date.now()
})

const messages: string[] = []

// Check if we should inject a reminder
const FREQUENCY = 3  // Every 3 prompts
const START_AFTER = 3 // Don't show until prompt 3

if (stats.promptCount >= START_AFTER && stats.promptCount % FREQUENCY === 0) {
  messages.push(\`<reminder type="tools">
**Available Tools Refresh** (prompt \${stats.promptCount})

You have access to these tool categories:
- beads: Task tracking (beads_add, beads_list, beads_close)
- chrome: Browser automation (chrome_screenshot, chrome_click)
- gemini: AI research (gemini_research, gemini_summarize)
- github: Issue management (github_create_issue, github_list_issues)

...and many more. Use mcp__cm__list_tools({ category }) to explore.

Use mcp__cm__call_tool({ name: "tool_name", args: {...} }) to execute.
</reminder>\`)
}

// Output the hook result
const output: HookJSONOutput = {
  hookSpecificOutput: {
    hookEventName: "UserPromptSubmit",
    additionalContext: messages.join("\n")
  }
}

console.log(JSON.stringify(output))

Step 3: Register the Hook

Add to your `~/.claude/settings.json`:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bun run $HOME/.claude/hooks/UserPromptSubmit.ts"
          }
        ]
      }
    ]
  }
}

Step 4: What Gets Injected

Here's exactly what happens at each prompt:

Prompt 1: User asks "Help me fix this bug"

``` [No reminder injected - too early] ```

Prompt 2: User asks "What does this function do?"

``` [No reminder injected - too early] ```

Prompt 3: User asks "Can you refactor this?"

```xml UserPromptSubmit hook additional context: Available Tools Refresh (prompt 3)

You have access to these tool categories:

  • beads: Task tracking (beads_add, beads_list, beads_close)
  • chrome: Browser automation (chrome_screenshot, chrome_click)
  • gemini: AI research (gemini_research, gemini_summarize)
  • github: Issue management (github_create_issue, github_list_issues)

...and many more. Use mcp__cm__list_tools({ category }) to explore.

Use mcp__cm__call_tool({ name: "tool_name", args: {...} }) to execute. ```

Prompt 4-5: No injection

Prompt 6: Next reminder

```xml UserPromptSubmit hook additional context: Available Tools Refresh (prompt 6) ... ```


Advanced: Multiple Rotating Reminders

You can cycle through different reminders:

const reminders = [
  { id: "tools", generate: () => "Your tools list..." },
  { id: "context", generate: () => "Session check-in..." },
  { id: "protocol", generate: () => "Protocol reminders..." },
]

// Calculate which reminder to show
const cycleNumber = Math.floor(stats.promptCount / FREQUENCY)
const reminderIndex = (cycleNumber - 1) % reminders.length
const reminder = reminders[reminderIndex]

messages.push(reminder.generate())

This gives you:

  • Prompt 3: tools
  • Prompt 6: context
  • Prompt 9: protocol
  • Prompt 12: tools (cycles back)

Key Takeaways

  1. SessionStart = one-time injection at conversation start
  2. UserPromptSubmit = runs on every user message (use for periodic injection)
  3. Track state in `/tmp/` files keyed by `session_id`
  4. Use modulo (`promptCount % N === 0`) for periodic triggers
  5. Output via `additionalContext` in the hook JSON response

The injected content appears as `` blocks that Claude sees but aren't shown to the user in the UI.

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