Claude Code的Undercover Mode:当AI学会隐藏自己
Claude Code有一个从未出现在任何文档中的模式,在这个模式下,它会系统性地抹除一切AI参与的痕迹。不写Co-Authored-By,不写Generated with Claude Code的footer,甚至连system prompt里都不告诉模型它自己是什么型号。这个模式叫Undercover Mode,只存在于Anthropic内部构建版本中,外部用户永远看不到它,因为整个功能在公开构建时会被dead code elimination彻底剔除。
从行为推断,这个机制的存在意味着Anthropic员工日常使用Claude Code向公开仓库提交代码。如果没有某种保护措施,commit message里可能出现未发布模型的代号,PR描述里可能暴露内部项目名称,system prompt里的模型标识可能通过某种方式泄露。Undercover Mode就是为了堵住这些口子。
触发条件
Undercover Mode的激活不是基于组织级别的判断。Anthropic的GitHub组织(anthropics)下既有私有仓库也有公开仓库(比如claude-code本身就是公开的),所以不能简单地按组织名做白名单。从行为推断,系统维护了一份硬编码的私有仓库列表,大约包含二三十个确认为私有的仓库,同时覆盖SSH和HTTPS两种URL格式。只有当前工作目录的git remote URL匹配到这份列表中的某一项时,才认定为internal状态,此时Undercover Mode关闭。
这个设计选择本身就值得玩味。一个更自然的做法是检查整个anthropics组织下的所有仓库,但那样做会把公开仓库也纳入白名单,导致在公开仓库工作时undercover被意外关闭。所以他们选择了更保守的逐仓库枚举。这份列表每增加一个条目都意味着有人确认了该仓库的私有性质,而不是依赖某个可能随时变化的组织级权限设置。
仓库分类有四种状态:
1 | State Undercover Meaning |
注意最后一行。分类检查是异步的,在启动时发起,但在结果返回之前,默认状态是ON。这是一个safe-by-default的设计:在你证明自己在安全环境之前,系统假设你不安全。如果启动后的异步检查确认当前仓库是internal,系统还会清除已缓存的system prompt section,让下一轮对话能拿到Undercover Mode关闭后的新prompt。这个清除动作本身也说明了一个问题:system prompt的缓存和undercover状态的异步判定之间存在竞态,需要显式处理。
还有一个边界情况:用户可能从一个非git目录(比如 /tmp
下的临时复现目录)运行Claude
Code,然后push到一个公开的remote。这时候没有git
remote可以匹配,分类结果是none,undercover自动开启。这不是bug,是feature。
具体改变了什么
Undercover Mode一旦激活,会在多个层面同时生效:
Git commit层面,Co-Authored-By
trailer被完全移除。attribution函数在检测到undercover状态后直接返回空字符串,不管用户怎么配置。正常情况下这个函数会根据当前模型生成类似
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
的trailer,还有一套fallback逻辑:对于无法识别的模型,会fallback到一个硬编码的公开模型名,避免泄露内部代号。但在undercover模式下,这些都不需要了,直接返回空。
PR描述层面,🤖 Generated with Claude Code
的footer同样被清空。包括那个带有贡献比例统计的增强版本(比如
93% 3-shotted by claude-opus-4-6),也一起消失。
System
prompt层面,这是最有意思的部分。模型的名称、ID、family信息全部从环境描述中移除。正常情况下system
prompt会包含类似
You are powered by the model named Claude Opus 4.6
这样的信息,在undercover模式下这整段都被suppress了。不仅如此,关于Claude
Code产品本身的描述和最新模型家族列表也一并移除。
用伪代码表示:
1 | def compute_env_info(model_id): |
此外,在commit和PR的prompt中还会注入一段显式的undercover指令,列出绝对不能出现在commit
message中的内容:内部模型代号(比如Capybara、Tengu之类的动物名)、未发布的版本号、内部repo名称、Slack频道、短链接,以及任何暗示自己是AI的表述。这段指令以
Do not blow your cover
结尾。指令中甚至给出了正反例对比:
1 | GOOD: |
从行为推断,那些BAD示例很可能来自真实的事故——有人在公开仓库的commit里留下了内部模型代号,然后整个团队被迫回顾并建立了这套防护机制。安全策略通常来自事后复盘,不是预防性想象。
还有一个defense-in-depth的细节:即使用户在设置中关闭了git相关的指令,undercover指令依然会被注入。代码注释明确写道,attribution的机械性剥离和模型ID隐藏可以自动工作,但显式的别暴露身份指令是最后一道防线,防止模型主动在commit message里提到内部代号。
不可关闭
代码文件头部的注释直接写明: There is NO force-OFF.
这背后是一个非对称风险分析。如果undercover误开(false positive),后果是少了一些attribution信息,PR描述少了个emoji,轻微不便。如果undercover误关(false negative),后果是未发布模型的代号、内部项目名、仓库路径出现在公开的git历史中,这些信息一旦push就无法撤回。
1 | Scenario Risk |
环境变量CLAUDE_CODE_UNDERCOVER=1只能强制开启,不能关闭。唯一让undercover关闭的方式是你的repo remote URL匹配到那份硬编码的内部仓库列表。没有flag,没有配置文件,没有workaround。这是故意的。当强制开启时,系统甚至会跳过首次使用时的自动提示弹窗,因为既然你手动设了环境变量,说明你知道自己在干什么,不需要被通知。
这种设计在安全工程中很常见但在产品功能中不多见。大多数功能都提供关闭开关,因为用户体验优先。但这里产品团队做了一个明确的判断:信息泄露的不可逆性压过了用户控制权。
身份压制
"不告诉模型它是谁"这件事值得单独讨论。正常运行时,Claude Code的system prompt包含明确的身份信息:模型名称、模型ID、模型家族、产品形态描述。Undercover Mode把这些全部移除。模型在运行时无法从system prompt中得知自己是Claude,无法得知自己运行在哪个版本上。
当然,这不是真正的身份消除。模型的行为模式、语言风格、知识范围都是固定的,有经验的用户一眼就能看出来。但system
prompt级别的身份压制确实阻断了一条具体的信息泄露路径,模型不会在回复中引用自己的型号名称,因为它在当前上下文中根本没有这个信息。更具体地说,它不会在commit
message里写出 1-shotted by claude-opus-4-6
这样的内容,因为它不知道自己是claude-opus-4-6。
这个设计隐含了一个有趣的假设:LLM的自我认知主要来自system prompt中的显式声明,而非从参数权重中涌现。如果模型对自身身份的认知是权重层面的,那么删除system prompt中的信息不会有任何效果。但从实际表现来看,这种prompt级别的干预确实有效,至少在减少模型主动自称I'm Claude方面是有效的。
这里有一个微妙的张力。模型的训练数据中包含大量关于Claude的信息,RLHF过程也强化了特定的身份认知模式。Undercover Mode本质上是在用指令层面的干预去压制权重层面的倾向。这像是给一个记得自己名字的人催眠,告诉他忘掉自己叫什么。效果取决于催眠的深度和记忆的强度之间的较量。从实用角度,只要模型不在commit message里自报家门就够了,不需要它真的忘掉自己是谁。
构建时消除
Undercover Mode的所有代码路径都以
process.env.USER_TYPE === 'ant'
作为门控。这个USER_TYPE是构建时的 --define
常量,不是运行时环境变量。bundler在构建外部版本时会将这个检查constant-fold为false,然后dead
code
elimination会把整个ant-only分支删除。代码注释中特别强调,这个检查必须在每个调用点内联展开,不能提取到公共常量,否则bundler无法做constant
folding,dead code elimination就会失效。
1 | # This is what the bundler sees in the external build: |
这意味着公开发布的Claude Code二进制文件中物理上不存在undercover相关的代码。不是隐藏了入口,是代码本身不在那里。你反编译也找不到。这和那些通过feature flag控制的隐藏功能有本质区别——feature flag只是不显示UI入口,代码还在;build-time elimination是从产物中彻底移除。
从工程角度,这是一个挺优雅的方案。同一份源码服务内外两个构建目标,通过构建时常量而非运行时分支来区分,避免了维护两套代码的负担,同时确保敏感逻辑不会意外出现在公开产物中。
设计启示
整个Undercover Mode的设计可以浓缩为几条原则:safe default(未知状态默认开启),asymmetric risk(只允许向更安全的方向手动干预),defense in depth(机械性剥离+prompt指令+构建时消除多层叠加),以及build-time elimination(敏感功能从公开产物中物理移除)。
这些原则单独看都不新鲜,但组合在一起应用到一个AI coding assistant上,产生了一些值得思考的问题。当一个AI工具需要把隐藏自己是AI这件事做成一个正式的工程特性,有代码、有测试、有注释说明设计意图,这说明AI工具的使用已经进入了一个新阶段。不再是能不能用的问题,而是用了之后痕迹怎么处理的问题。
从另一个角度看,Undercover Mode的存在也暗示了一个事实:Anthropic内部大量使用自家的AI工具进行日常开发,包括向开源社区贡献代码。这本身不奇怪,但需要一个专门的工程系统来管理这些贡献的署名和信息安全,说明这已经不是偶尔为之,而是系统性的工作流程。AI辅助编程从个人尝鲜到组织级部署,中间隔着的就是这类基础设施。
某种意义上,Undercover Mode是AI工具成熟度的一个标志。就像一个间谍机构的成熟度不看它能派出多少间谍,而看它的cover story管理系统有多完善。