Hooks — Claude Code automation
Claude-native (Tier A)
Hooks are a Claude Code feature — shell commands the harness fires on lifecycle events. Single-agent (Tier B) clients (Codex, Gemini, Cursor, Copilot) have no hook system and must not fabricate one (there is no hooks.json for Codex). The context-watch and safety behaviours below are Claude-specific. See the agents standard for how other tools handle the equivalent concerns.
Claude Code supports shell hooks that fire in response to lifecycle events. HCS uses one user-level hook: a UserPromptSubmit hook that monitors session context size and triggers a structured memory dump when the context approaches the limit, plus a set of repo-level safety/audit hooks.
check-context.ps1 — context watch hook
Event: UserPromptSubmit — fires on every user prompt before Claude Code processes it Location (running): ~/.claude/hooks/check-context.ps1Location (source): scripts/hooks/check-context.ps1 in this repo Timeout: 30 seconds
What it does
- Reads the hook input JSON from stdin — this includes the path to the current session transcript
- Walks backward through the transcript to find the most recent assistant turn with token usage data
- Calculates total context =
input_tokens+cache_creation_input_tokens+cache_read_input_tokens - If the total exceeds 90,000 tokens, injects a
[CONTEXT-WATCH]system reminder into the session - Falls back to character-count estimation (
total_chars / 4) if no usage data is found in the transcript - Always exits 0 — never blocks a prompt, never throws an error to the user
What the reminder does
When [CONTEXT-WATCH] fires, Claude Code writes a comprehensive memory dump to the project's auto-memory directory before responding to the user's actual message. The dump covers:
- In-flight work: branches, files modified, PR/issue numbers, mid-implementation state
- Decisions made and their reasoning
- Bugs found and fixes applied (non-obvious ones only)
- Blockers and open questions
- Verification status
- User feedback observed this session
After the dump, Claude Code ends its turn with:
Memory saved (N files updated). Run /clear when ready — your next session will auto-load.The user then runs /clear manually. The hook never clears the session automatically.
Why /clear instead of compaction
Session compaction (the default Claude Code behavior when context fills up) produces a lossy summary that the model treats as whole context. This leads to subtle loss of reasoning detail. The explicit /clear + structured memory approach is honest about what has been kept — the next session loads the memory files and knows exactly what was saved and what was not.
Hook configuration in settings.json
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "pwsh.exe -NoProfile -ExecutionPolicy Bypass -File \"C:/Users/KristopherTurner/.claude/hooks/check-context.ps1\"",
"timeout": 30
}
]
}
]
}The path must be absolute. Use forward slashes or escaped backslashes in the JSON.
Keeping the hook in sync
The running copy at ~/.claude/hooks/check-context.ps1 and the source copy at scripts/hooks/check-context.ps1 in this repo should stay identical. When updating the hook:
- Edit
scripts/hooks/check-context.ps1in this repo - Copy it to
~/.claude/hooks/check-context.ps1 - Test by submitting a prompt in a session with a large transcript
- Commit the update:
fix(hooks): <description>
Adjusting the threshold
The threshold is defined at the top of the script:
$THRESHOLD = 9000090,000 tokens was chosen because:
- The Anthropic prompt cache has a 5-minute TTL at approximately 100k tokens
- Triggering at 90k gives enough headroom to write the memory dump without hitting the hard context limit
- It fires early enough to save meaningful state without firing too aggressively on short sessions
Raise the threshold if you find it fires too often on normal work. Lower it if you want earlier warnings.
Hook output format
Hooks communicate back to Claude Code via stdout JSON:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "The text to inject as a system reminder..."
}
}Anything written to stderr is logged but does not reach Claude Code. Exit code 0 always — any non-zero exit would block the prompt.
Repo-level hooks (workstream pipeline set)
These hooks ship with every repo that uses the workstream pipeline pattern. Templates are in templates/claude/hooks/. The settings.json wiring is in templates/claude/settings.json.template.
block-secrets.ps1
Event: PreToolUse — matcher: Write|EditAction: Blocks the write and exits 1 if the content matches any secret pattern.
Patterns checked (case-insensitive):
password\s*[:=](api[_-]?key|apikey)\s*[:=](access[_-]?token|auth[_-]?token)\s*[:=](client[_-]?secret)\s*[:=]-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----AccountKey=(Azure Storage connection string)
The hook reads tool_input.content (Write) or tool_input.new_string (Edit) from stdin. If a match is found it writes a human-readable error to stderr and exits 1, blocking the tool call.
validate-path.ps1
Event: PreToolUse — matcher: Write|EditAction: Blocks writes to protected path prefixes and exits 1.
Protected prefixes:
.git/or\.git\node_modules/dist/.env(exact filename or prefix)
Exits 0 (allow) for all other paths. The path check is case-insensitive.
log-tokens.ps1
Event: PostToolUse — matcher: Write|EditAction: Appends a JSONL entry to .claude/logs/tokens.jsonl.
Entry format:
{"ts":"2026-01-01T00:00:00Z","tool":"Write","file":"src/App.tsx","input_tokens":1234,"output_tokens":56}If logs/tokens.jsonl doesn't exist, the hook creates it. Fails silently — never blocks.
format-on-write.ps1
Event: PostToolUse — matcher: Write|EditAction: Runs a formatter on the written file. Ships as a no-op stub — all formatter invocations are commented out.
To enable prettier on TypeScript/JavaScript files, uncomment this block:
{ $_ -in @('ts', 'tsx', 'js', 'jsx') } {
if (Get-Command prettier -ErrorAction SilentlyContinue) {
prettier --write $filePath 2>$null
}
}Never exits non-zero — formatter errors are swallowed.
summarize-session.ps1
Event: StopAction: Appends a JSONL entry to .claude/logs/sessions.jsonl when a session ends.
Entry format:
{"ts":"2026-01-01T00:00:00Z","event":"stop","session_id":"abc123"}Fails silently — never blocks the session stop.
check-context.ps1
Event: UserPromptSubmitAction: Same as the user-level hook but scoped to this repo. Kept for reference and per-project threshold customization.
Important: This hook is wired at user-level (~/.claude/settings.json) so it fires for every session. Do NOT also wire it in the repo-level settings.json — it would fire twice per prompt. The file is present in .claude/hooks/ for documentation and threshold overrides.
See the check-context.ps1 section above for full documentation.
Adding new hooks
Additional hooks can be added to settings.json. Supported events:
UserPromptSubmit— fires before Claude Code processes a user messagePreToolUse— fires before a tool call (can block it)PostToolUse— fires after a tool call completesStop— fires when Claude Code finishes a turn
For each new hook, add a script under scripts/hooks/ in this repo and copy it to ~/.claude/hooks/. Document what it does and why.