作者在使用claude -p生成commit信息时发现AI会读取项目CLAUDE.md中的指令,揭示了AI编程工具的隐式上下文注入风险。
我的 git_commit.py 只有 20 行脚本,读取 git diff --staged 然后调用 claude -p 生成符合 Conventional Commits 规范的提交信息。这是一个再窄不过的 LLM 调用场景:输入一个 diff,输出一行内容。不涉及文件探索、没有工具调用、不需要知道项目上下文。
raw = subprocess.check_output(
["claude", "-p", SYSTEM + "\n\n" + diff],
text=True,
timeout=20,
stderr=subprocess.PIPE,
).strip()
我之所以深究下去,是因为有次我在检查仓库的 CLAUDE.md(原本是去看别的东西),注意到文件开头有一段叫做 "MANDATORY routing rules" 的内容——规定 shell 输出要经过沙箱工具、阻止 curl、读取网页前先建立索引之类的规则。这些跟一个"输入 diff、输出提交信息"的脚本八竿子打不着。所以我以为:无所谓,git_commit.py 调用 claude -p 时带的是完全自包含的 system prompt 和 diff 字符串,凭什么要去加载项目指令文件?
我没有想当然,而是实际验证了一下。从仓库根目录执行和 git_commit.py 同样形式的调用,只是问模型是否看到了那些规则:
$ claude -p "Reply with exactly one line: either 'YES-SAW-CTX-RULES' if your \
context includes instructions mentioning ctx_fetch_and_index or context-mode \
MANDATORY routing rules, or 'NO-CTX-RULES' if it does not. Nothing else."
YES-SAW-CTX-RULES
它加载了。一次裸的 claude -p 调用(没有任何 flag)和交互式会话一样,会自动从当前目录发现并加载 CLAUDE.md,不管你的 prompt 和项目有没有关系。git_commit.py 没有给 subprocess.check_output 传 cwd,所以它继承了调用者的当前目录——而这个脚本本来就是在仓库内部运行的,cwd 恰好就是仓库根目录。
那个 CLAUDE.md 文件是 4404 字节、79 行。大约一千个 token 的路由规则、输出格式约束,还有一个项目记忆系统协议块,这些东西对这个"一次性的 diff → 提交信息"任务来说一点用都没有。倒不是说结果错了——返回的提交信息本身没问题——只是每次调用都要平白多付这笔开销,而且任务根本用不上那些内容。
真正让我担心的不只是 token 成本,而是耦合。git_commit.py 的行为表面上完全由文件顶部硬编码的 SYSTEM 字符串定义:
SYSTEM = (
"You are a git commit message generator. "
"Output ONLY the commit message — one line, no explanation, no markdown, no quotes, "
...
)
但实际上并非如此。无论何时,只要当前工作目录碰巧有 CLAUDE.md,它就会不声不响地跟着一起加载进来,而且同样能轻易改变输出结果。如果将来有人修改了项目的 CLAUDE.md,加上"提交信息一律用 Title Case"或者"主题行偏好句子式表达",git_commit.py 就会开始这样做——代码没改、prompt 没改,但因为从哪个目录运行脚本,结果就不一样了。
于是我去找解决方案。claude --help 文档里恰好记录了这种场景:
--bare Minimal mode: skip hooks, LSP, plugin sync, attribution,
auto-memory, background prefetches, keychain reads, and
CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic
auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via
--settings (OAuth and keychain are never read).
但第二句话是陷阱。这个项目的 key_facts.md 里明确说了:"没有 ANTHROPIC_API_KEY——Claude 调用走的是 claude -p 子进程(OAuth 会话)。" --bare 明确拒绝读取 OAuth 会话,只接受 API key。用 --bare 来解决上下文加载问题,反而会破坏脚本实际依赖的认证方式——文档看起来对,实际上跑起来就挂。
真正我要的那个 flag 是 --safe-mode:
--safe-mode Start with all customizations (CLAUDE.md, skills, plugins,
hooks, MCP servers, custom commands and agents, output
styles, workflows, custom themes, keybindings, and more)
disabled ... Auth, model selection, built-in tools, and
permissions work normally.
关键是"Auth ... works normally"这句——它禁用了 CLAUDE.md 发现机制,但不影响进程的认证方式。用同样的方式验证了一下:
$ claude -p --safe-mode "Reply with exactly one line: either 'YES-SAW-CTX-RULES' ... or 'NO-CTX-RULES' ..."
NO-CTX-RULES
这就是我最终采用的修复方案,在仓库里两处做同样调用的地方都改了——git_commit.py 的 subprocess 调用,以及 server.py 里为 generate_commit_message MCP 工具服务的 _claude() 辅助函数:
raw = subprocess.check_output(
["claude", "-p", "--safe-mode", SYSTEM + "\n\n" + diff],
text=True,
timeout=20,
stderr=subprocess.PIPE,
).strip()
得到的教训不是"到处加 --safe-mode"。而是:headless CLI 调用会像 shell 继承环境变量一样悄无声息地继承所在目录的上下文——默认行为,作用域就是调用时所在的目录。解决这个问题用的 flag 往往不是名字听起来最像答案的那一个。--bare 看起来就是"停止自动加载项目文件"的正确答案,对于用 API key 认证的场景也确实是对的。但对于通过 OAuth 认证的设置,它是列表里唯一一个会悄悄破坏你本来要修的东西的 flag。我能发现这一点,唯一的原因是我两个 flag 都试了、实际运行了并检查了结果,而不是看了 flag 名字就以为没问题。