mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-09-14 16:18:45 +08:00
4743df93b7
Drop the 24h TTL: HTTP validators are the whole freshness contract. Key cache on URL alone; prompt-aware keying with normalization gave false safety (semantic differences slipped through). Prompt is kept as metadata and surfaced in the hit message so the next agent can judge whether the earlier reading applies. Reframe docs around "HTTP resource cache, not prompt cache". While here, fix two latent bugs: - Replace the unquoted heredoc in the pre-hook with printf. The heredoc expanded $vars and backticks inside cached content, so a compromised doc page could trigger command substitution on cache hit. - Strip CR before awk paragraph-mode parsing of curl -I -L output so blank separators between response blocks on a redirect chain are recognised (was silently picking intermediate headers). Remove dead -v IGNORECASE=1 (gawk-only; tolower() already handles it).
107 lines
4.2 KiB
Bash
Executable File
107 lines
4.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# sdd-cache-pre.sh — PreToolUse hook for WebFetch.
|
|
#
|
|
# HTTP resource cache keyed by URL. Freshness is delegated to the origin via
|
|
# HTTP validators; 304 Not Modified is the only signal to serve from cache.
|
|
# On hit, exits 2 and writes the cached body to stderr so Claude Code can
|
|
# deliver it to the agent in place of the WebFetch result. Otherwise exits 0.
|
|
#
|
|
# No TTL: if validators don't catch a change, nothing will. Entries without
|
|
# ETag or Last-Modified are never cached (can't revalidate).
|
|
#
|
|
# Cached bodies are prompt-shaped (WebFetch post-processes through a model),
|
|
# so the key is URL-only and the original prompt is surfaced in the hit
|
|
# message so the next agent can tell if the earlier reading still applies.
|
|
#
|
|
# Dependencies: jq, curl, shasum (or sha256sum).
|
|
|
|
set -euo pipefail
|
|
|
|
# Graceful degradation: if any dependency is missing, let the fetch through.
|
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
command -v curl >/dev/null 2>&1 || exit 0
|
|
command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || exit 0
|
|
|
|
if [ -t 0 ]; then INPUT="{}"; else INPUT=$(cat); fi
|
|
|
|
# Debug logging: active when SDD_CACHE_DEBUG=1 is set, or when a sentinel
|
|
# file exists at .claude/sdd-cache/.debug. Toggle with `touch` / `rm`.
|
|
dbg() {
|
|
local dir="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache"
|
|
[ "${SDD_CACHE_DEBUG:-0}" = "1" ] || [ -f "$dir/.debug" ] || return 0
|
|
mkdir -p "$dir"
|
|
printf '%s [pre] %s\n' "$(date -u +%FT%TZ)" "$*" >> "$dir/.debug.log"
|
|
}
|
|
dbg "fired"
|
|
|
|
URL=$(printf '%s' "$INPUT" | jq -r '.tool_input.url // empty' 2>/dev/null || true)
|
|
if [ -z "$URL" ]; then dbg "no url in tool_input, exit"; exit 0; fi
|
|
dbg "url=$URL"
|
|
|
|
# Cache key is sha256(URL), truncated to 128 bits.
|
|
hash_key() {
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
printf '%s' "$1" | shasum -a 256 | cut -c1-32
|
|
else
|
|
printf '%s' "$1" | sha256sum | cut -c1-32
|
|
fi
|
|
}
|
|
|
|
CACHE_DIR="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache"
|
|
CACHE_FILE="$CACHE_DIR/$(hash_key "$URL").json"
|
|
|
|
if [ ! -f "$CACHE_FILE" ]; then dbg "no cache file at $CACHE_FILE, exit"; exit 0; fi
|
|
dbg "cache file exists: $CACHE_FILE"
|
|
|
|
FETCHED_AT=$(jq -r '.fetched_at // 0' "$CACHE_FILE" 2>/dev/null || echo 0)
|
|
ORIGINAL_PROMPT=$(jq -r '.prompt // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
ETAG=$(jq -r '.etag // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
LAST_MOD=$(jq -r '.last_modified // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
|
|
# No validator means we cannot verify freshness — never serve from cache.
|
|
if [ -z "$ETAG" ] && [ -z "$LAST_MOD" ]; then
|
|
dbg "cached entry has no etag/last-modified, cannot revalidate, bypass"
|
|
exit 0
|
|
fi
|
|
|
|
HEADERS=()
|
|
[ -n "$ETAG" ] && HEADERS+=(-H "If-None-Match: $ETAG")
|
|
[ -n "$LAST_MOD" ] && HEADERS+=(-H "If-Modified-Since: $LAST_MOD")
|
|
|
|
STATUS=$(curl -sI -o /dev/null -w "%{http_code}" \
|
|
--max-time 5 -L \
|
|
"${HEADERS[@]}" \
|
|
"$URL" 2>/dev/null || echo "000")
|
|
dbg "revalidation HEAD status=$STATUS"
|
|
|
|
if [ "$STATUS" != "304" ]; then
|
|
dbg "not 304, letting WebFetch proceed"
|
|
exit 0
|
|
fi
|
|
|
|
# Server confirmed content unchanged. Serve cached copy to the agent.
|
|
CONTENT=$(jq -r '.content // empty' "$CACHE_FILE" 2>/dev/null || true)
|
|
if [ -z "$CONTENT" ]; then dbg "cache file has empty content field, bypass"; exit 0; fi
|
|
dbg "cache HIT, blocking WebFetch with ${#CONTENT} bytes of cached content"
|
|
|
|
VERIFIED_AT_ISO=$(date -u -r "$FETCHED_AT" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
|
|| date -u -d "@$FETCHED_AT" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|
|
|| echo "unknown")
|
|
|
|
# Emit the payload with printf so $CONTENT is never interpreted by the shell
|
|
# (docs contain backticks, $vars, and backslashes in code examples; an
|
|
# unquoted heredoc would treat them as command substitution).
|
|
{
|
|
printf '[sdd-cache] Cache hit for %s\n\n' "$URL"
|
|
printf 'Revalidated via HTTP 304; unchanged since %s. Use the cached\n' "$VERIFIED_AT_ISO"
|
|
printf 'content below as if WebFetch had just returned it.\n\n'
|
|
if [ -n "$ORIGINAL_PROMPT" ]; then
|
|
printf 'Original WebFetch prompt: "%s". If your angle differs, judge\n' "$ORIGINAL_PROMPT"
|
|
printf 'whether this reading still covers it.\n\n'
|
|
fi
|
|
printf -- '----- BEGIN CACHED CONTENT -----\n'
|
|
printf '%s\n' "$CONTENT"
|
|
printf -- '----- END CACHED CONTENT -----\n'
|
|
} >&2
|
|
exit 2
|