1. 凌晨四点的告警:Claude Trace 日志为什么会写进手机号
凌晨四点被安全组电话叫醒,说生产环境的 Trace 日志里出现了完整的用户手机号,这种体验一次就够了。问题不在 Claude 本身,而在于我们默认把「调试方便」放在了「数据最小化」前面:Trace 会把 Prompt、补全内容、工具调用参数、甚至中间变量原样落盘,而用户画像、订单备注、客服对话里天然带着手机号、身份证号、邮箱这类 PII。GDPR 第 32 条要求的是「适当的技术与组织措施」,日志里明文存 PII 基本等于没措施。
这篇记录的是我实际走过的一遍排查与加固流程:先定位是哪条链路把 PII 写进 Trace,再用可复制的脱敏配置把风险压下去,最后用 Trace 回放验证脱敏真的生效,而不是「配置写了就当生效」。适合正在用 Claude 做生产应用、又需要过 GDPR 或内部合规审计的团队。核心检索词就三个:Claude Trace 日志、GDPR 合规、日志脱敏配置。
先说结论,避免你走弯路:Trace 脱敏不能只靠「采样率」或「少记点」,结构化数据(JSON、XML)在语义分块下往往整块保留,采样对它几乎无效。真正有效的是在写入前做字段级脱敏,并且把脱敏逻辑放在统一入口,而不是散落在各个业务代码里。
2. 前置准备:用 TaoToken 统一 Key 接入,把脱敏放在入口
排查时最痛的一点是:Key 散落在多个服务、多个环境变量里,你根本不知道哪条链路在写日志。所以第一步不是改代码,而是把模型调用收敛到一个统一入口。我用 TaoToken 做统一接入,官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。统一入口的好处是:脱敏、审计、限流都能在一层做,Trace 写盘前必然经过它。
你需要先拿到 Key,去控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。Key 只放服务端环境变量,别写进前端或提交到仓库。
接入文档在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你用的是 Claude Code 这类编码 Agent,长期跑建议用 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,比按次调用更稳。想先验证模型行为,可以直接在模型对话里试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
注意:统一 Key 不等于自动合规。它只是让你有一个「必经之路」可以挂脱敏中间件。脱敏规则本身还得你自己定义。
3. 可复制配置:settings.json 与 config.toml 脱敏骨架
下面这套配置是我实际在用的骨架,分两部分:Claude 客户端侧的 settings.json 控制 Trace 行为,服务侧的 config.toml 控制脱敏规则。你可以直接抄结构,把正则换成你们业务里的 PII 模式。
3.1 settings.json:关掉全量 Trace,开启写入前脱敏钩子
{ "trace": { "enabled": true, "mode": "redacted", "dir": "/var/log/claude/trace", "file_permission": "440", "max_field_length": 512, "drop_raw_prompt": true, "redaction_hook": "/opt/claude/hooks/redact.py", "sampling": { "enabled": false, "note": "结构化数据下采样不可靠,改用字段级脱敏" } }, "api": { "base_url": "https://taotoken.net/api", "key_env": "TAOTOKEN_API_KEY", "timeout_ms": 30000 } }关键点解释:drop_raw_prompt设为 true,意味着原始 Prompt 不落盘,只落脱敏后的版本;redaction_hook指向一个可执行脚本,Trace 在写盘前会调用它;file_permission设成 440,避免日志被其他进程读取。sampling.enabled我直接关了,因为前面说过,采样对结构化 PII 基本无效,留着只会给人虚假的安全感。
3.2 config.toml:字段级脱敏规则
[redaction] mode = "strict" fail_closed = true [[redaction.rules]] name = "cn_mobile" pattern = "(?<!\\d)1[3-9]\\d{9}(?!\\d)" replace = "[PHONE]" [[redaction.rules]] name = "email" pattern = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" replace = "[EMAIL]" [[redaction.rules]] name = "id_card" pattern = "(?<!\\d)[1-9]\\d{5}(19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx](?!\\d)" replace = "[ID]" [[redaction.rules]] name = "json_pii_field" pattern = "\"(phone|mobile|id_card|email|address)\"\\s*:\\s*\"[^\"]*\"" replace = "\"$1\":\"[REDACTED]\"" [redaction.audit] enabled = true log_redaction_events = true store_original = falsefail_closed = true很重要:如果脱敏脚本执行失败,Trace 直接拒绝写盘,而不是「失败就写原文」。这是合规里典型的 fail-safe 设计。store_original = false保证不保留原文副本,否则脱敏等于白做。
3.3 脱敏钩子脚本骨架
#!/usr/bin/env python3 import json, re, sys RULES = [ (re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)"), "[PHONE]"), (re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[EMAIL]"), (re.compile(r"(?<!\d)[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx](?!\d)"), "[ID]"), ] def redact_text(s: str) -> str: for pat, rep in RULES: s = pat.sub(rep, s) return s def walk(obj): if isinstance(obj, dict): return {k: walk(v) for k, v in obj.items()} if isinstance(obj, list): return [walk(v) for v in obj] if isinstance(obj, str): return redact_text(obj) return obj def main(): raw = sys.stdin.read() try: data = json.loads(raw) except json.JSONDecodeError: sys.stdout.write(redact_text(raw)) return sys.stdout.write(json.dumps(walk(data), ensure_ascii=False)) if __name__ == "__main__": main()这个脚本对嵌套 JSON 做递归遍历,字符串字段统一过正则。实测下来,它对「手机号藏在 address 字段里」这种拼图式泄露也能拦住,因为它是按值匹配,不依赖字段名。
4. 验证请求:用 Trace 回放确认脱敏真的生效
配置写完不代表生效,必须用真实请求验证。我用的方法是:构造一条带 PII 的测试请求,走统一入口,然后回放 Trace 日志,检查落盘内容里还有没有明文。
4.1 构造测试请求
export TAOTOKEN_API_KEY="你的Key" curl -s https://taotoken.net/api/v1/messages \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet", "messages": [ {"role": "user", "content": "用户张三,手机号13812345678,邮箱zhangsan@example.com,请生成一段客服回复"} ] }'4.2 回放 Trace 并检查
TRACE_FILE=$(ls -t /var/log/claude/trace/*.jsonl | head -1) echo "检查文件: $TRACE_FILE" grep -E "13812345678|zhangsan@example.com" "$TRACE_FILE" && echo "存在明文PII,脱敏未生效" || echo "未发现明文PII,脱敏生效" grep -o "\[PHONE\]\|\[EMAIL\]\|\[ID\]" "$TRACE_FILE" | sort | uniq -c预期结果是:第一条 grep 无输出,第二条能看到[PHONE]和[EMAIL]的计数。如果第一条有输出,说明脱敏钩子没被调用,或者规则没覆盖到。
4.3 用模型对话做交叉验证
有时候 Trace 脱敏生效了,但模型返回内容里还带着 PII,这属于另一类风险。可以在模型对话里直接测:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,输入同样的测试数据,看返回是否回显了手机号。如果回显了,说明你的 Prompt 里就不该带原始 PII,应该在调用前先脱敏。
5. 本篇常见错排查
5.1 脱敏脚本没执行,日志里还是明文
最常见原因是redaction_hook路径不对,或者脚本没有执行权限。检查:
ls -l /opt/claude/hooks/redact.py chmod +x /opt/claude/hooks/redact.py另外确认 Trace 进程有权限读取该脚本,容器环境下要挂载进去。
5.2 采样开了但 PII 还是泄露
这就是我踩过的坑:以为降低采样率能减少 PII,结果结构化数据整块保留。解决办法是关掉采样,改用字段级脱敏。如果你确实需要采样来控制日志体积,至少要对结构化字段做单独处理。
5.3 正则匹配不到嵌套 JSON 里的手机号
如果手机号是数字类型而不是字符串,正则匹配不到。需要在脱敏脚本里先把数字转成字符串再匹配,或者对字段名做白名单处理。我的做法是:对phone、mobile、id_card这类字段名直接整字段替换,不依赖值匹配。
5.4 脱敏后日志无法调试
这是合规和可调试性的经典矛盾。我的折中方案是:保留字段名和结构,只替换值,并且记录original_length元数据。这样你还能看出「这里原本有个 11 位的东西」,但不暴露具体内容。审计时如果需要,可以走单独的审批流程去查原始数据,而不是让原始数据默认躺在日志里。
5.5 GDPR 合规验证动作清单
排查完配置,建议按这个清单过一遍:确认 Trace 目录权限是 440;确认store_original为 false;确认脱敏钩子 fail_closed;确认日志保留周期有上限;确认访问日志的人有审计记录;确认 DPO 知道这套流程。这几条做完,基本能应对内部审计。
6. 长期编码与 Agent 场景的接入建议
如果你是用 Claude Code 或类似 Agent 做长期编码,Trace 日志量会更大,PII 风险也更高。建议用 Coding Plan 做统一接入:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,把脱敏中间件挂在 Agent 和模型之间。Claude Code 的 Anthropic 兼容接入可以参考:https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
最后说一个我实际用下来的小技巧:把脱敏规则做成可热更新的配置文件,而不是硬编码在脚本里。这样业务新增 PII 类型时,不用重启服务就能生效。另外,每周跑一次 Trace 回放扫描,用 grep 找明文 PII,比等安全组告警要主动得多。凌晨四点的电话,能少接一次是一次。