This explains how to build a system that automatically re-injects important context (like a tools list) into Claude Code conversations every N prompts.
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.
Use the UserPromptSubmit hook to:
- Track how many prompts have occurred
- Every N prompts, re-inject the important context
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
}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))Add to your `~/.claude/settings.json`:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bun run $HOME/.claude/hooks/UserPromptSubmit.ts"
}
]
}
]
}
}Here's exactly what happens at each prompt:
``` [No reminder injected - too early] ```
``` [No reminder injected - too early] ```
```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. ```
```xml UserPromptSubmit hook additional context: Available Tools Refresh (prompt 6) ... ```
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)
- SessionStart = one-time injection at conversation start
- UserPromptSubmit = runs on every user message (use for periodic injection)
- Track state in `/tmp/` files keyed by `session_id`
- Use modulo (`promptCount % N === 0`) for periodic triggers
- 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.