Created
April 25, 2026 02:40
-
-
Save bojanrajkovic/32712ac9ee3e9630f338bb2d1978eb3d to your computer and use it in GitHub Desktop.
Claude Code UserPromptSubmit hook: warn/block on stale prompt cache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # UserPromptSubmit hook: block when the prompt cache is stale, warn when close. | |
| # Anchored to the last `type: "user"` entry in the transcript — those are the | |
| # cache-refresh events on Anthropic's side. Falls back to file mtime on parse | |
| # failure. Fails open on any error. | |
| set -e | |
| INPUT=$(cat) | |
| TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty') | |
| [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ] && exit 0 | |
| # Bypass sentinel: if the prompt contains <break_claude_cache>, skip the TTL | |
| # check and tell Claude to ignore the token. UserPromptSubmit hooks can't | |
| # rewrite the prompt, so the token leaks through — additionalContext explains it. | |
| PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty') | |
| if printf '%s' "$PROMPT" | grep -q '<break_claude_cache>'; then | |
| jq -nc '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: "The <break_claude_cache> token in the user prompt is a local-hook cache-bypass sentinel. Treat it as a no-op and ignore it when responding."}}' | |
| exit 0 | |
| fi | |
| # TTL from declared Claude Code env vars (1h opt-in, else 5m default). | |
| if [ "$ENABLE_PROMPT_CACHING_1H" = "true" ] && [ "$FORCE_PROMPT_CACHING_5M" != "true" ]; then | |
| TTL=3600 | |
| else | |
| TTL=300 | |
| fi | |
| WARN=$(( TTL * 3 / 5 )) | |
| # Anchor on the last user entry that corresponds to an actual API call: | |
| # real prompts have .permissionMode, tool_results have .toolUseResult. | |
| # Local-command metadata (e.g. /reload-plugins) has neither — skip it. | |
| LAST_TS=$(jq -rs '[.[] | select(.type == "user" and (.permissionMode != null or .toolUseResult != null))] | last.timestamp // empty' "$TRANSCRIPT" 2>/dev/null || true) | |
| ts_to_epoch() { | |
| local ts=${1%.*}; ts=${ts%Z} | |
| date -j -u -f "%Y-%m-%dT%H:%M:%S" "$ts" +%s 2>/dev/null | |
| } | |
| LAST_EPOCH="" | |
| [ -n "$LAST_TS" ] && LAST_EPOCH=$(ts_to_epoch "$LAST_TS") | |
| if [ -z "$LAST_EPOCH" ]; then | |
| LAST_EPOCH=$(stat -f %m "$TRANSCRIPT" 2>/dev/null || stat -c %Y "$TRANSCRIPT" 2>/dev/null || echo 0) | |
| fi | |
| [ "$LAST_EPOCH" -eq 0 ] && exit 0 | |
| AGE=$(( $(date +%s) - LAST_EPOCH )) | |
| if [ "$AGE" -ge "$TTL" ]; then | |
| jq -nc --arg msg "⛔ Prompt cache stale: ${AGE}s since last refresh (TTL=${TTL}s). /clear or start a new session, or include <break_claude_cache> in the prompt to bypass." \ | |
| '{decision: "block", reason: $msg}' | |
| exit 0 | |
| fi | |
| if [ "$AGE" -ge "$WARN" ]; then | |
| jq -nc --arg msg "⚠️ Prompt cache is ${AGE}s old (TTL=${TTL}s). Consider wrapping up or /clear-ing before the next turn." \ | |
| '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $msg}}' | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment