你的 Claude Code Agent 生成了一段数据库连接代码,里面有 DB_PASSWORD = "prod-secret-2026"——这个密码来自 Agent 读取的 .env 文件,它「帮忙」把配置内联进了代码。或者在 LangGraph 流水线里,一个 Debug Agent 为了帮助排查问题,把完整的 HTTP 请求(包含 Authorization: Bearer sk-... 头)输出到了 Langfuse trace 里,而 trace 数据对团队所有成员可见。更隐蔽的情况:Agent 在生成配置文件模板时,把从环境变量读到的真实值当成「示例值」填进了模板,下游把这个「模板」提交到了 git 仓库。
常见原因
1. Agent 读取了包含 secret 的上下文文件
Claude Code 或其他 Agent 使用了 Read 工具读取 .env、config.yaml、secrets.json 等含有凭证的文件,这些文件的内容进入了 LLM 的上下文窗口,模型可能在后续的输出里引用或复现这些值。
怎么判断:检查 Agent 的 Read 工具调用记录,是否有对敏感文件的读取。在 CLAUDE.md 里搜索是否有「read .env」或「read secrets」的指令。
2. 错误日志或调试输出包含了请求内容
Agent 在调用外部 API 失败后,把完整的请求(包括认证 header)写进了日志或 Trace span。Authorization: Bearer sk-live-abc123 这样的字符串出现在 Langfuse dashboard 里,对有访问权限的所有人可见。
怎么判断:在 Trace 数据里搜索 Bearer、api_key、password、secret 等关键词,检查是否有凭证字符串出现在 span 的 input、output 或 metadata 字段里。
3. 代码生成时把环境变量的实际值内联进代码
Agent 收到「生成一个连接 PostgreSQL 的配置」的任务,它读取了当前环境的 DATABASE_URL,然后把这个 URL(包含密码)内联到了生成的代码里,而不是使用 os.environ.get("DATABASE_URL") 引用。
怎么判断:检查 Agent 生成的代码里是否有 postgres://user:password@host:port/db 这样的硬编码 DSN,而不是环境变量引用。
4. 对话历史没有脱敏就被发送给另一个 Agent
在多 Agent 流水线里,Agent A 的完整对话历史(包含用户消息里的凭证信息)被作为上下文传给 Agent B,Agent B 的输出里可能引用了这些凭证,并被记录到新的 Trace span 里。
怎么判断:检查 Agent 之间 handoff 的 payload,是否包含了原始对话历史而不是经过脱敏的摘要。
5. Prompt 里有「记录所有内容用于调试」的指令
系统 prompt 里有 总是在输出里包含你使用的所有配置信息,方便调试 这样的指令,导致 Agent 在每次输出时都附带了它读取到的配置(可能包含 secret)。
怎么判断:检查所有 Agent 的 system prompt,是否有「记录」、「输出配置」、「包含上下文」等可能导致 secret 外泄的指令。
6. 生成的代码被直接提交,没有经过 secret 扫描
Agent 生成了包含硬编码 secret 的代码,Promotion 流程没有做 secret 扫描,代码直接被提交到 git 仓库,触发了 GitHub 的 secret scanning 告警(或者更糟,没有告警,凭证就这样留在历史提交里)。
怎么判断:在 git 历史里运行 git log -p | grep -E 'sk-|password=|api_key=',检查是否有敏感字符串进入了提交记录。
最短修复路径
Step 1:在 Agent 输出写入 Trace 前做自动脱敏
import re
# 常见 secret 模式
SECRET_PATTERNS = [
(re.compile(r'sk-[a-zA-Z0-9]{20,}'), 'sk-***REDACTED***'),
(re.compile(r'Bearer\s+[a-zA-Z0-9\-_.]{20,}'), 'Bearer ***REDACTED***'),
(re.compile(r'password["\s:=]+["\']?[^\s"\']{6,}["\']?', re.IGNORECASE), 'password=***REDACTED***'),
(re.compile(r'(api[_-]?key["\s:=]+["\']?)[a-zA-Z0-9\-_]{16,}', re.IGNORECASE), r'\1***REDACTED***'),
(re.compile(r'postgres://[^@]+@'), 'postgres://***:***@'),
(re.compile(r'mysql://[^@]+@'), 'mysql://***:***@'),
]
def redact_secrets(text: str) -> str:
"""从字符串里脱敏所有已知的 secret 模式。"""
for pattern, replacement in SECRET_PATTERNS:
text = pattern.sub(replacement, text)
return text
# 在所有 Trace span 写入前调用
def safe_log_span(span, input_text: str, output_text: str):
span.update(
input=redact_secrets(input_text),
output=redact_secrets(output_text)
)
Step 2:在 Claude Code 的 CLAUDE.md 里明确禁止读取 secret 文件
## 安全规则(不可违反)
- 禁止读取以下文件:.env, .env.local, .env.prod, secrets.yaml, credentials.json, *.pem, *.key
- 生成的代码中,所有配置值必须通过环境变量引用(如 os.environ.get("DB_PASSWORD")),禁止内联实际值
- 禁止在输出里包含任何看起来像 API key、密码或 token 的字符串
- 如果需要展示配置示例,使用占位符(如 "your-api-key-here")而不是真实值
Step 3:在代码提交前做 secret 扫描
# 安装 pre-commit hook,使用 trufflehog 或 gitleaks 扫描
pip install pre-commit
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
EOF
pre-commit install
# 手动扫描已有提交
gitleaks detect --source . --verbose
# 或用 trufflehog
trufflehog git file://. --only-verified
Step 4:在 Promotion 节点加 secret 检查
def check_output_for_secrets(output: str) -> list[str]:
"""检查 Agent 输出里是否包含疑似 secret。"""
violations = []
high_entropy_pattern = re.compile(r'[a-zA-Z0-9+/]{32,}={0,2}')
for match in high_entropy_pattern.finditer(output):
candidate = match.group()
if shannon_entropy(candidate) > 4.5: # 高熵字符串可能是 secret
violations.append(f"高熵字符串(疑似 secret):{candidate[:8]}...")
for pattern, _ in SECRET_PATTERNS:
if pattern.search(output):
violations.append(f"匹配到 secret 模式:{pattern.pattern[:40]}")
return violations
def promotion_check_secrets(output: str, task_type: str) -> None:
if task_type not in ("code_generation", "config_generation", "documentation"):
return
violations = check_output_for_secrets(output)
if violations:
raise SecurityViolation(
f"Agent 输出包含疑似 secret,已阻断 Promotion:\n" +
"\n".join(f" - {v}" for v in violations)
)
def shannon_entropy(text: str) -> float:
"""计算字符串的 Shannon 熵,高熵字符串可能是随机生成的 secret。"""
import math
from collections import Counter
freq = Counter(text)
length = len(text)
return -sum((c/length) * math.log2(c/length) for c in freq.values())
Step 5:配置 git 仓库的 secret push protection
# GitHub 的 secret scanning(Enterprise 功能)
gh api repos/{owner}/{repo} --method PATCH \
-f security_and_analysis.secret_scanning.status=enabled \
-f security_and_analysis.secret_scanning_push_protection.status=enabled
# 本地:用 .gitignore 防止 secret 文件被意外提交
cat >> .gitignore << 'EOF'
.env
.env.*
!.env.example
secrets/
*.pem
*.key
credentials.json
EOF
Step 6:如果已经发生泄漏,立即轮换凭证
# 已知凭证泄漏的处理步骤(按优先级排序):
# 1. 立即在提供商控制台撤销泄漏的 key(GitHub: Settings > Developer Settings > PAT)
# 2. 生成新的 key 并更新所有依赖它的服务
# 3. 从 git 历史里删除 secret(git filter-branch 或 BFG)
# 4. 强制 push 清理后的历史(需要团队配合)
# 5. 通知可能受影响的用户
# 用 BFG 删除 git 历史里的 secret(比 filter-branch 快)
java -jar bfg.jar --replace-text secrets-to-remove.txt my-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
预防建议
- 在
CLAUDE.md里明确声明禁止读取的文件(.env、secrets.json 等),并说明配置应该通过环境变量引用 - 所有 Trace span 写入前自动脱敏,使用已知 secret 模式的正则 + 高熵字符串检测双重保障
- 在 Promotion 节点加 secret 检查,发现疑似 secret 时阻断输出
- 安装 git pre-commit hook(gitleaks 或 trufflehog),防止包含 secret 的文件被意外提交
- 启用 GitHub/GitLab 的 secret scanning push protection,作为最后一道防线
- 定期对 Trace 数据做 secret 扫描,发现历史泄漏及时处理
- 把 Agent 可读的文件范围限制在任务所需的最小集合里(principle of least privilege)
常见问答 (FAQ)
Q: Langfuse 存储的 Trace 数据如果包含了 secret,如何补救?
A: Langfuse 支持通过 API 删除特定的 trace 或 observation 记录。立即通过 Langfuse API 删除包含 secret 的 trace(DELETE /api/public/traces/{traceId}),并检查是否有其他 trace 包含相同的 secret(批量搜索 + 删除)。同时撤销泄漏的凭证,因为无法确认 Langfuse 数据库在删除前是否被他人访问。
Q: 高熵字符串检测会不会误报(把合法的 Base64 内容当成 secret)? A: 会有误报,尤其是内容包含 Base64 编码的图片或文档时。可以通过以下方式降低误报率:1)只对特定上下文(如 key=value 格式)里的高熵字符串报警;2)维护一个「已知安全」的高熵字符串白名单(如常见的公开 CDN URL 的哈希值);3)对误报设置反馈机制,逐步完善规则。
Q: Agent 读取 .env 文件是正常的工作需求(如检查配置是否正确),如何在保证安全的前提下允许? A: 允许读取(用于检查),但禁止在输出里复现实际值。在 CLAUDE.md 里加入规则:「如果需要检查环境变量是否存在,只输出变量名和是否已设置(是/否),不输出变量的值」。同时在 Agent 的工具层对 .env 文件的读取做封装,只返回「key 是否存在」而不是 key 的值。
Q: 如果 secret 已经进入了 git 历史但还没被 push,能直接 git reset 清掉吗?
A: git reset 只移动 branch 指针,不会删除历史提交里的数据。正确的做法是:1)git reset HEAD~1(撤销最近一次提交);2)git add .gitignore(把 secret 文件加入 .gitignore);3)git stash(暂存其他改动);4)重新提交时不包含 secret 文件。如果 secret 已经在多个提交里,必须用 BFG 或 git filter-repo 完整清理历史。