agent-skills 的 sdd-cache Hook:用 HTTP 304 再验证为 Source-Driven Development 构建跨会话 WebFetch 缓存
agent-skills 的 sdd-cache Hook用 HTTP 304 再验证为 Source-Driven Development 构建跨会话 WebFetch 缓存【免费下载链接】agent-skillsProduction-grade engineering skills for AI coding agents.项目地址: https://gitcode.com/GitHub_Trending/agentskill/agent-skills本篇技术指南基于 hooks/SDD-CACHE.md 及其配套脚本 hooks/sdd-cache-pre.sh、hooks/sdd-cache-post.sh完整讲解 agent-skills 仓库中 sdd-cache 这一对 Claude Code Hook 的设计动机、配置方法、运行时机制与本地验证手段。读完后你将掌握如何在不修改source-driven-development技能本身的前提下让 Agent 重复抓取同一官方文档页时自动跳过冗余WebFetch请求同时保留每次都向源站确认内容未变的新鲜度保证。为什么需要 sdd-cache缓存与验证最新文档的矛盾agent-skills 中的 source-driven-development 技能要求 Agent 把每一个框架相关的实现决策都锚定在官方文档上——流程是DETECT → FETCH → IMPLEMENT → CITE。这意味着同一个项目跨多次会话开发时同样的文档页会被反复抓取。把抓取内容存成本地记忆看似自然但文档明确指出这与该技能的核心承诺相矛盾——文档会变过期的缓存会掩盖这一点。sdd-cache 的解法是内容确实缓存在磁盘上但每次复用前都向源站发起 HTTP 条件请求If-None-Match/If-Modified-Since重新验证。只有当源站应答304 Not Modified时才命中缓存——这本身就是一次新鲜度验证而不是单纯地读内存。安装与配置Setup启用步骤共三步注册 Hooks。把下面配置加入.claude/settings.json个人使用可放.claude/settings.local.json。matcher 精确锁定WebFetch工具{ hooks: { PreToolUse: [ { matcher: WebFetch, hooks: [ { type: command, command: bash \${CLAUDE_PROJECT_DIR}/hooks/sdd-cache-pre.sh\, timeout: 10 } ] } ], PostToolUse: [ { matcher: WebFetch, hooks: [ { type: command, command: bash \${CLAUDE_PROJECT_DIR}/hooks/sdd-cache-post.sh\, async: true, timeout: 10 } ] } ] } }注意两个实现细节${CLAUDE_PROJECT_DIR}解析为你启动 Claude Code 的目录。上面的写法适用于 Hook 脚本就放在当前项目内如本仓库的hooks/目录若 agent-skills 安装在别处例如~/agent-skills下的共享插件需把${CLAUDE_PROJECT_DIR}/hooks/...替换为脚本的绝对路径。Post 钩子标记了async: true即异步执行——缓存写入不阻塞主流程。忽略缓存目录。确保.claude/sdd-cache/已在.gitignore中——本仓库的 .gitignore 已包含该条目。正常使用技能。按原样使用/source-driven-development或该技能本身技能与 Agent 工作流零改动——缓存是完全透明的。心智模型URL 为键的 HTTP 资源缓存文档给出的心智模型可以概括为一句话这是一个以 URL 为键的 HTTP 资源缓存新鲜度完全委托给源站的ETag/Last-Modified——没有 TTL键里不含 prompt。一个容易被忽略但很关键的点是缓存存的不是原始 HTML。WebFetch会把每个响应交给模型、按调用方的 prompt 做后处理所以缓存下来的其实是某个 Agent 对页面的阅读结果。因此键只保留 URL保证跨会话可复用原始 prompt 作为元数据保存并在命中消息中显式展示让下一个 Agent 判断这次阅读是否仍然适用。工作机制两个 Hook 如何协作每个 URL 对应一条缓存记录以 JSON 存在.claude/sdd-cache/sha.json。两个事件的分工如下事件行为PreToolUse WebFetch若存在缓存条目则带If-None-Match/If-Modified-Since发起HEAD请求。收到304就拦截本次 fetch并把缓存内容经 stderr 回传给 Agent同时附上原始 prompt 作为元数据否则放行 fetch。PostToolUse WebFetch抓取响应内容再发一次HEAD记录当前ETag/Last-Modified存入{url, prompt, etag, last_modified, content, fetched_at}。新鲜度规则Freshness rules只有源站确认304 Not Modified时条目才会被使用没有ETag或Last-Modified头的条目永远不会被缓存——没有验证器就无法事后核实新鲜度缓存它们等于信任记忆缓存键是sha256(url)。同一个 URL 用不同 prompt 询问会命中同一条目缓存正文反映的是首次抓取时的 prompt命中时该 prompt 会一并展示由 Agent 自行决定复用还是手动重新抓取。Agent 侧看到什么命中缓存WebFetch被 Hook 以退出码2拦截。Claude Code 会把 Hook 的 stderr 载荷作为工具错误回传给 Agent——这是命中缓存的约定信号而非真正的失败。载荷以[sdd-cache] Cache hit for url开头缓存正文包在----- BEGIN CACHED CONTENT -----/----- END CACHED CONTENT -----标记之间Agent 可以像WebFetch刚返回一样使用它。未命中或已过期WebFetch正常执行结果被存下来供下次使用。技能本身没有任何改动DETECT → FETCH → IMPLEMENT → CITE流程照旧Hook 只改变了FETCH这一步的内部行为。源码级实现细节结合两个脚本的源码可以确认文档描述的每个行为在实现层面都有对应代码且有多处防御性设计值得学习。pre 脚本拦截路径hooks/sdd-cache-pre.sh优雅降级开头三个依赖检查——jq、curl、shasum或sha256sum任一缺失就直接exit 0放行 fetch第 20-23 行保证缺依赖时功能只是失效、绝不阻断 Agent 工作。缓存键sha256(URL)截断为前 32 个十六进制字符即 128 bit脚本注释写明这与 post 脚本必须保持一致。无验证器不命中读取条目后若etag和last_modified均为空直接放行第 61-65 行与文档never cached规则一一对应。条件 HEAD用curl -sI -o /dev/null -w %{http_code} --max-time 5 -L发起带验证器的 HEAD超时 5 秒网络失败记为000。状态码不是304就静默放行。安全输出命中时用printf而非 heredoc 输出载荷第 91-105 行。源码注释解释了原因文档正文里含反引号、$变量和反斜杠未加引号的 heredoc 会触发命令替换——这是一个很实用的 shell 细节。验证时间展示命中消息里用date -u -rBSD/macOS与date -u -dGNU/Linux双写兜底把fetched_at转成 ISO 时间展示为 unchanged since。post 脚本写入路径hooks/sdd-cache-post.sh响应形态兼容Claude Code 当前的tool_response是含bytes/code/codeText/durationMs/result/url的对象正文在.result脚本用 jq 依次回退.output → .text → .content → .body并对字符串形态做了分支处理以兼容旧版/自定义集成第 37-58 行注释明确了这一演进背景。只取最终响应的验证器HEAD 用curl -sI -L跟随重定向后用 awk 段落模式取最后一个响应块的头——避免误抓重定向链中 301/302 中间跳的 ETagtr -d \r则确保 awk 能正确识别响应块之间的空行分隔。无验证器即清理若源站不再返回任何验证器post 脚本会主动rm -f删除旧条目第 109-113 行防止留下无法再验证的僵尸缓存。原子写入用jq -n生成 JSON 到file.$$.tmp临时文件成功后mv覆盖失败则清理临时文件——避免 pre 脚本读到半截 JSON。本地测试指南文档给出了四层递进的验证方法可直接复制执行。1. 直接冒烟测试脚本模拟 PostToolUse 载荷写入一条缓存再模拟同 URL 的 PreToolUse# Simulate a PostToolUse payload: cache a page echo { tool_input: { url: https://react.dev/reference/react/useActionState, prompt: extract the signature }, tool_response: useActionState(action, initialState) returns [state, formAction, isPending] } | bash hooks/sdd-cache-post.sh # Inspect the stored entry ls .claude/sdd-cache/ cat .claude/sdd-cache/*.json | jq . # Simulate the next PreToolUse on the same URL prompt echo { tool_input: { url: https://react.dev/reference/react/useActionState, prompt: extract the signature } } | bash hooks/sdd-cache-pre.sh echo exit$?预期结果第一条命令在.claude/sdd-cache/下生成一个文件前提是源站返回了ETag或Last-Modified第二条命令在源站应答304时以退出码2把缓存内容写到 stderr否则静默退出0。2. 真实会话端到端验证按上文注册 Hook 到.claude/settings.local.json在本仓库启动一个 Claude Code 会话让 Agent 抓取一个文档页如fetchhttps://react.dev/reference/react/useActionStateand summarize确认.claude/sdd-cache/下出现了文件用相同 prompt 再让它抓同一页确认第二次WebFetch被拦截、返回缓存内容会话记录中可见带[sdd-cache]前缀的工具错误。3. 新鲜度失效验证想确认文档变更时缓存自动失效可人为制造 ETag 失配。注意挑选具体条目——*.json通配在缓存超过一个文件时并不安全# Pick the entry you want to corrupt (swap in the actual filename) ENTRY.claude/sdd-cache/e49c9f378670cfbb1d7d871b6dee16d9.json # Patch its ETag to something the origin will not recognize jq .etag W/\stale-etag-forced\ $ENTRY $ENTRY.tmp mv $ENTRY.tmp $ENTRY # Next PreToolUse should miss (server returns 200, not 304) echo {tool_input:{url:..., prompt:...}} | bash hooks/sdd-cache-pre.sh echo exit$? # expect 0 (fetch allowed through)4. 调试模式两个 Hook 在调试模式下都会把带时间戳的事件追加写入.claude/sdd-cache/.debug.log。开启方式二选一# Option A: env var (per-session) SDD_CACHE_DEBUG1 claude # Option B: sentinel file (persistent) mkdir -p .claude/sdd-cache touch .claude/sdd-cache/.debug # …disable with: rm .claude/sdd-cache/.debug日志会记录 URL、检测到的tool_response形态、HEAD 状态码以及每次调用命中/未命中的原因。当某次未命中看起来反常时尤其有用——最常见的原因是源站停止发出验证器。已知限制Known limitations文档对限制的陈述同样重要逐条继承如下正文是 prompt 定形的。命中返回的是之前那个 Agent 对页面的阅读结果并展示原始 prompt 供当前 Agent 判断是否适用。若不适用删除.claude/sdd-cache/下的对应文件即可强制重新抓取。每次缓存写入都多花一次 HEAD。因为 Claude Code 不暴露WebFetch已收到的响应头post 钩子必须向源站再查一次以捕获ETag/Last-Modified。每次未命中多一个往返——这是纯 Hook、不改核心的代价。没有ETag或Last-Modified的服务器永远不会被缓存。多数官方文档站react.dev、docs.djangoproject.com、developer.mozilla.org都发验证器不发的站点每次都会重新抓取。行为异常的服务器可能返回错误的304。那属于要排查的服务器 bug而不是缓存需要防御的不变量——设计者拒绝用 TTL 来掩盖问题发现陈旧条目就删掉它。缓存是本地、按项目的没有团队级共享缓存。文档说明要加这一层需要带签名的内容寻址存储超出当前范围。运行环境要求Requirementsjqcurlshasum或sha256sum脚本自动探测二者有其一即可Bash 3.2所有依赖缺失时 Hook 会静默放行、功能降级但不报错这是 pre 脚本 开头显式设计的优雅降级路径。小结为什么这套设计值得借鉴sdd-cache 解决的是一个 Agent 工程中的典型张力重复抓取浪费 token 与延迟盲目缓存又破坏以官方文档为唯一事实来源的可信性。它的回答是把新鲜度判断完全交还给 HTTP 协议本身——304 Not Modified既是缓存信号也是验证证据无验证器则不缓存宁可多抓也不猜。配合 URL-only 键 prompt 元数据、退出码 2 stderr 载荷的拦截约定、无 TTL 的透明缓存语义以及依赖缺失即放行的降级策略整对 Hook 在未改动技能定义的前提下为 source-driven-development 的FETCH阶段加上了可验证、可调试、可失效的加速层。【免费下载链接】agent-skillsProduction-grade engineering skills for AI coding agents.项目地址: https://gitcode.com/GitHub_Trending/agentskill/agent-skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考