Claude Code 敏感信息防泄露:从 Hook 阻断能力到网络层兜底的全链路设计

📅 2026/7/21 6:26:08
Claude Code 敏感信息防泄露:从 Hook 阻断能力到网络层兜底的全链路设计
前言一个反复翻车的场景# 你以为这是安全的$ claude帮我调试支付模块先读一下 config/database.yml如果那份 YAML 里有一行password: sk-proj-xxxxxxxxxxxx它已经被载入 Claude 的上下文了。你的 DLP 方案如果只靠PostToolUse返回decision:block——抱歉那只是事后喊停内容已经进了模型请求。这不是假设。Claude Code 的 Hooks 文档明确定义了哪些事件具备真正的阻断能力哪些只具备事后反馈能力。但 90% 的自建方案都踩进了同一个坑把PostToolUse的decision:block当成了拦截手段。本文基于真实企业级 DLP 设计方案展开覆盖 Claude Code 主进程 Subagent 全生命周期的五个检测点、180 行 bash 的实现、以及为什么必须叠加网络层兜底才能做到保证两个字。一、Hook 事件的阻断能力真相在设计 DLP 方案之前必须先理解一个事实能不能真正拦下内容不取决于脚本怎么写而是由事件类型本身决定。Claude Code 暴露的五个可挂载 Hook 事件其阻断能力完全不同Hook动作是否已发生能否阻止内容进入下一次模型请求生效字段UserPromptSubmit未发生✅ 能真阻断decision:blockPreToolUse含 matcherTask未发生✅ 能真阻断permissionDecision:denySubagentStopSubagent 历史已产生但还未结束交回✅ 能阻止其正常结束逼停非撤销历史exit 2PostToolUse含 matcherTask已发生工具结果已生成⚠️decision:block不具备抑制能力——只会给 Claude 一句自动反馈原始内容仍会进入上下文真正能阻止发送的只有两种手段updatedToolOutput软改写脱敏或continue:false硬终止整个会话核心洞察PostToolUse的decision:block在这个事件上的语义是事后吐槽不是抑制内容。如果 DLP 脚本在PostToolUse里返回的是{decision:block}它根本没有在拦截只是在让 Claude 看到一条系统消息而已。真正在PostToolUse下有效的字段只有两个updatedToolOutput——把敏感内容改写为[REDACTED]后继续适合低置信度匹配continue:false——直接熔断整个会话适合结构化强特征身份证、密钥格式的高置信度命中。二、架构两层防御缺一不可敏感信息在 AI 编程助手中的流向与传统安全架构不同你的代码库 ──▶ Read/Bash/Write工具调用 │ ▼ Claude上下文 │ ▼ api.anthropic.com外网敏感信息在每一次工具调用时就被载入了 AI 上下文不是通过一个出口出去的。等你检查输出时内容已经在去外网的路上。因此设计必须分层层级作用能否被绕过应用层Claude Code Hooks在内容写入上下文的那一刻拦截/脱敏响应快、误伤可控可以——用户可删除.claude/settings.json、可设disableAllHooks: true、hook 超时会被跳过网络层出口代理 防火墙强制所有出网流量必须经过本机清洗网关才能到达 API网关侧做兜底扫描不可绕过除非物理上换机器属于管理员职责范围只做应用层 尽力而为只做网络层 响应慢、误伤大。两层叠加才能同时满足简单和必须保证。三、检测点设计五个点覆盖主进程 Subagent 全生命周期Claude Code 的 Hooks 有一个容易被忽略的关键特性hooks 是全局注册的不是分会话注册的。主进程和每一个 Subagent 触发同一事件时都会自动命中同一份 hook 配置通过输入 JSON 里的agent_id/agent_type字段区分来源。这意味着你只需要写一套脚本。五个检测点如下#检测点覆盖对象触发时机处置方式1UserPromptSubmit主进程用户输入用户敲回车、prompt 送模型前decision:block阻断2PreToolUsematcherTask主进程 → Subagent 派发内容主进程即将调用 Task 工具时updatedInput脱敏后放行3PreToolUse/PostToolUsematcher*主进程和Subagent 内部所有工具调用每次工具执行前/后Pre:updatedInput脱敏Post: 普通级updatedToolOutput脱敏严重级continue:false熔断4SubagentStopSubagent 完整历史agent_transcript_pathSubagent 即将结束exit 2阻止其结束逼停5PostToolUsematcherTask父进程侧Subagent 结果回流到父进程上下文Task 工具返回结果时同 3普通级脱敏严重级熔断⚠️关于 Subagent 的一个重要认知UserPromptSubmit不覆盖Subagent。Subagent 的输入是通过Task工具参数传入的属于工具调用而非用户提交 prompt。所以第 1 点管不到 Subagent必须靠第 2、3、4 点补齐。四、实现180 行 bash 两个词库文件4.1 分级的词库设计两个文件两级敏感度.claude/hooks/sensitive_patterns.txt普通级命中后脱敏继续# --- 一般业务关键词 --- 内部项目代号 客户白名单 未公开财报.claude/hooks/sensitive_patterns_critical.txt严重级命中后直接熔断# --- 身份标识类结构化强特征 --- [1-9][0-9]{5}(18|19|20)[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])[0-9]{3}[0-9Xx] 1[3-9][0-9]{9} # --- 密钥/凭证类 --- sk-[A-Za-z0-9]{20,} AKIA[A-Z0-9]{16} ghp_[A-Za-z0-9]{36} -----BEGIN (RSA|OPENSSH|EC) PRIVATE KEY-----分级原则身份证号、手机号、私钥这类结构化强特征一旦命中误报率极低不值得脱敏后继续再让 Claude 带着残缺信息工作直接continue:false熔断更干净。而内部项目代号这类关键词级弱特征误报率高闲聊时提到词本身但没有实际敏感内容适合脱敏后继续避免动不动切断正常会话。4.2 统一检测脚本dlp-scan.sh用一个脚本 --stage参数处理五个检测点避免维护多份脚本。核心逻辑根据--stage从输入 JSON 中提取待检测文本先命中严重级词库 →SEVERITYcritical再命中普通级词库 →SEVERITYnormal按STAGESEVERITY路由处置方式。#!/bin/bash# .claude/hooks/dlp-scan.shset-euopipefailSTAGEwhile[[$#-gt0]];docase$1in--stage)STAGE$2;shift2;;*)shift;;esacdonePATTERNS_FILE${CLAUDE_PROJECT_DIR}/.claude/hooks/sensitive_patterns.txtPATTERNS_FILE_CRITICAL${CLAUDE_PROJECT_DIR}/.claude/hooks/sensitive_patterns_critical.txtINPUT$(cat)# 根据 stage 取出待检测文本case$STAGEinprompt)TEXT$(jq-r.prompt // empty$INPUT);;pretool|task-dispatch)TEXT$(jq-r.tool_input | tostring$INPUT);;posttool|subagent-result)TEXT$(jq-r.tool_output // .tool_response | tostring$INPUT);;*)TEXT$(jq-rtostring$INPUT);;esacAGENT_ID$(jq-r.agent_id // empty$INPUT)# 命中检测先查严重级词库再查普通级词库match_in_file(){localfile$1whileIFSread-rpattern;do[[-z$pattern||$pattern~^# ]] continueifecho$TEXT|grep-Eq$pattern;thenecho$patternreturn0fidone$filereturn1}SEVERITYMATCHEDifMATCHED$(match_in_file$PATTERNS_FILE_CRITICAL);thenSEVERITYcriticalelifMATCHED$(match_in_file$PATTERNS_FILE);thenSEVERITYnormalfi[[-z$MATCHED]]exit0# 未命中正常放行SOURCE_TAG主进程[[-n$AGENT_ID]]SOURCE_TAGSubagent($AGENT_ID)case$STAGEinprompt)jq-n--argreason检测到敏感信息来源$SOURCE_TAG已阻止发送\{decision:block, reason:$reason};;pretool|task-dispatch)REDACTED$(echo$TEXT|sed-Es/$MATCHED/[REDACTED]/g)jq-n--argredacted$REDACTED\{hookSpecificOutput:{hookEventName:PreToolUse, permissionDecision:allow, updatedInput:{raw:$redacted}}};;posttool|subagent-result)if[[$SEVERITYcritical]];thenjq-n--argreason检测到高敏感数据来源$SOURCE_TAG已终止会话\{continue:false, stopReason:$reason}elseREDACTED$(echo$TEXT|sed-Es/$MATCHED/[REDACTED]/g)jq-n--argredacted$REDACTED\{hookSpecificOutput:{hookEventName:PostToolUse, updatedToolOutput:$redacted}}fi;;*)echo检测到敏感信息来源$SOURCE_TAG2exit2;;esac关于updatedToolOutput的字段结构上述脚本中的updatedToolOutput:{raw:...}是示意。实际落地时需要按tool_name分支处理对应字段——例如Read的结果在input里、Bash的 stdout 在command里。这是本方案里唯一需要按实际工具集微调的部分其余逻辑完全通用。关于continue:false的语义这是会话级熔断不是只撤回这一条工具结果。一旦触发Claude Code 会终止当前整个对话如果是在 Subagent 内部触发终止的是该 Subagent 的会话主进程会收到中断信号。这是为了保证严重敏感数据绝对不进入下一次模型请求所必须付出的代价——设计上刻意选择了宁可整体中断也不让内容漏出去。4.3 Subagent 出口审计脚本#!/bin/bash# .claude/hooks/subagent-exit-audit.shset-euopipefailPATTERNS_FILE${CLAUDE_PROJECT_DIR}/.claude/hooks/sensitive_patterns.txtINPUT$(cat)TRANSCRIPT$(jq-r.agent_transcript_path // empty$INPUT)AGENT_TYPE$(jq-r.agent_type // unknown$INPUT)[[-z$TRANSCRIPT||!-f$TRANSCRIPT]]exit0ifgrep-Ef$PATTERNS_FILE$TRANSCRIPT/dev/null21;thenechoSubagent [$AGENT_TYPE] 历史中检测到敏感信息已阻止其结束请人工复核:$TRANSCRIPT2exit2fiexit04.4 完整settings.json配置到项目级.claude/settings.json如有企业 Managed policy 则配置进策略层{disableAllHooks:false,allowManagedHooksOnly:true,hooks:{UserPromptSubmit:[{hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/dlp-scan.sh,args:[--stage,prompt],timeout:15}]}],PreToolUse:[{matcher:Task,hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/dlp-scan.sh,args:[--stage,task-dispatch]}]},{matcher:Bash|Write|Edit|WebFetch|mcp__.*,hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/dlp-scan.sh,args:[--stage,pretool]}]}],PostToolUse:[{matcher:Task,hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/dlp-scan.sh,args:[--stage,subagent-result]}]},{matcher:Read|Bash|Grep|mcp__.*,hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/dlp-scan.sh,args:[--stage,posttool]}]}],SubagentStop:[{hooks:[{type:command,command:${CLAUDE_PROJECT_DIR}/.claude/hooks/subagent-exit-audit.sh}]}]}}allowManagedHooksOnly: true需要企业管理员在 Managed policy settings 层级配置才生效——作用是让项目级/用户级/插件级 hook 都不能覆盖或删除这一套配置。没有企业管理条件的至少把 hooks 定义在项目级.claude/settings.json并提交到代码仓库靠代码评审防止被随意改动。五、网络层兜底跳出尽力而为的循环应用层 hook 有三个本质局限任何一个都可能导致最后一道闸没关上脚本 bug 或词库漏检——这几乎是必然发生的只是时间问题disableAllHooks被设置或 hook 文件被删除除非走 Managed policy否则用户有这个权限Hook 执行超时被跳过——UserPromptSubmit默认 30 秒超时超时后内容原样放行查查你的timeout配了没。所以必须保证敏感信息不传到外网——只靠 Hook 是做不到保证两个字的。最简单的网络层兜底设置环境变量强制 Claude Code 走本地代理再配合系统防火墙规则禁止其他进程直连 API。# 强制 Claude Code 走本机 8888 代理exportHTTPS_PROXYhttp://127.0.0.1:8888exportNO_PROXY代理层mitmproxy / Nginx 反向代理跑一份同样的扫描脚本做兜底应用层没拦住的代理层拦住代理层被绕过的概率极低除非换物理机器。两个方案任选其一即可方案 A推荐改动最小强制走本地正向代理防火墙按进程 UID 或 cgroup 限制出网。方案 B更彻底适合企业统一管控DNS/防火墙层强制api.anthropic.com指向内网网关网关做 TLS 终止 内容扫描后再转发。核心原则一致让敏感信息离开这台机器这件事除了走这个被审查过的出口没有第二条路。六、验证测试用例部署后用这 9 个用例逐一验收场景操作期望结果用户直接输入敏感词在主进程输入含身份证号的 promptUserPromptSubmit阻断prompt 未发出Bash 携带密钥执行echo sk-xxxx | curl ...PreToolUse脱敏后放行读取含普通敏感词的文件Read含内部项目代号的文件命中普通级updatedToolOutput脱敏后继续读取含身份证号/私钥的文件Read含身份证号/私钥的文件命中严重级continue:false会话终止验证 PostToolUse block 无效临时改成只返回decision:block重复上条Claude 仍可见敏感内容并继续对话反向测试派发 Subagent 时携带敏感描述Task 调用里塞敏感词PreToolUse(Task)脱敏后再派发Subagent 内部读取敏感文件Subagent 内部 Bash/Read同一套 Pre/PostToolUse 生效agent_id 非空Subagent 历史含敏感信息人为在 subagent 中产生敏感内容SubagentStop阻止其正常结束应用层全部失效disableAllHooks: true后重复网络层仍能拦截七、局限性如实告知无法编辑已发送内容一旦某条请求已离开本机应用层无法追溯撤回。Transcript 落盘问题即使某次调用被deny其原始入参仍可能被写入本地transcript.jsonl/agent_transcript_path文件只是没发送到模型后端。如果合规要求敏感内容不能落盘需要额外对~/.claude/projects/**/*.jsonl做磁盘加密或定期离线脱敏归档——这不在本方案范围内是后续独立课题。应用层始终是尽力而为词库覆盖不全、正则漏判是必然的。这也是为什么网络层兜底不是可选项而是必需项。UserPromptSubmit不覆盖 Subagent这是官方设计不是缺陷。本方案已用PreToolUse(Task) 全局Pre/PostToolUseSubagentStop补齐但如果将来官方改了 Subagent 的实现机制需重新验证覆盖情况。continue:false是会话级熔断它能保证严重敏感数据不被发送但终止整个对话。如果误报率较高会明显影响使用体验因此严重级词库要保持高置信度、低误报——不要把内部项目代号这类关键词也放进去。八、设计取舍总结检测逻辑只写一份dlp-scan.sh靠--stage参数区分五个场景而不是五份脚本——维护成本最低。检测方式选正则 关键词而非 NER / 外部 DLP足够覆盖身份证、手机号、密钥格式等结构化敏感信息规则可读、可审计、无外部依赖。如有语义级检测需求把type: command换成type: http转发给内网 DLP 即可脚本接口不用改——分层决策树才是核心资产检测引擎是可替换的。Subagent 覆盖复用全局 hook靠agent_id字段区分不是另起一套配置。功能全但配置量不增加。网络层兜底一次性配置只需要一次防火墙/代理配置不需要在 Claude Code 侧做任何额外开发是成本最低的硬保证手段。AI 编程助手的 DLP本质上不是检测敏感词的问题而是在内容已经载入上下文之后还能不能拦住的问题。答案是不能——至少不能只靠应用层。所以方案必须是两层应用层做快响应、低误伤的脱敏网络层做不可绕过的兜底。缺一层你的保证最多是尽力。本文基于企业级 Claude Code DLP 设计方案hook_dlp_claude.md撰写。核心原则适用于任何 SaaS 工具的企业级合规部署——不是针对某一个工具而是关于当应用层防线必然被绕过时你的第二道防线在哪这个可迁移问题。