Claude Code 被曝在请求里夹暗号:一个撇号就能标记你的环境

用人话拆解 Claude Code 请求水印争议:它可能如何从 ANTHROPIC_BASE_URL、时区和域名名单一路判断,再把结果藏进一句日期提示里。

最近围绕 Claude Code 有一轮争议:有逆向分析者称,某些版本的 Claude Code 会在特定条件下读取本机环境信息,并把检测结果编码进发往服务端的 system prompt 里。这个机制不是额外上传一个明显的 telemetry 字段,而是修改一行看起来很普通的日期提示,例如 Today's date is 2026-06-30.

公开讨论里最值得看的,不是“封号”这个结果,而是检测流程本身。它展示了一个本地 AI 编程工具如何在不增加额外网络请求的情况下,把客户端环境特征夹带进正常请求。对开发者来说,这比单纯的账号风险更值得警惕。

本文只整理目前公开逆向文章和社区讨论中反复出现的技术细节。由于这类分析依赖具体版本和逆向结果,下面内容应理解为“公开报告所称的实现逻辑”,不等同于官方完整说明。

先把流程说白

这套逻辑如果用一句话概括,就是:先看你有没有走自定义 API 地址,再看这个地址像不像特定名单里的服务,再看你电脑时区,最后把判断结果藏进一句日期提示里。

按公开分析里的说法,链路大概是这样:

  1. Claude Code 启动或发请求前,读取 ANTHROPIC_BASE_URL
  2. 如果这个变量不存在,或者仍然指向官方默认地址,后面的特殊标记逻辑可能不会触发。
  3. 如果它指向自定义 endpoint,就解析 URL,取出 hostname。
  4. 客户端把 hostname 拿去和内置名单、关键词规则做匹配。
  5. 同时读取本机时区,判断是否是 Asia/ShanghaiAsia/Urumqi 这类目标时区。
  6. 客户端不新增单独字段,而是修改 system prompt 里的日期句子。
  7. 服务端收到请求后,通过日期分隔符和撇号码点,读出客户端环境标记。

这个过程最绕的地方在第 6 步。它不是写一个很明显的字段,比如:

1
2
3
4
{
  "region": "CN",
  "proxyMatched": true
}

而是把信息塞进这种看起来很正常的文本里:

1
Today's date is 2026-06-30.

普通人扫一眼只会觉得这是日期。真正传递信息的,是日期里的 /-,以及 Today's 中间那个看起来几乎一样的撇号。

如果写成伪代码,逻辑大概像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
baseUrl = readEnv("ANTHROPIC_BASE_URL")

if baseUrl is empty or baseUrl is official api.anthropic.com:
    send normal system prompt
else:
    hostname = parseHostname(baseUrl)
    domainHit = matchDomainList(hostname)
    labHit = matchAiLabKeywords(hostname)
    timezoneHit = localTimezone in ["Asia/Shanghai", "Asia/Urumqi"]

    dateText = buildDateText()
    dateText = encodeTimezone(dateText, timezoneHit)
    dateText = encodeDomainStatus(dateText, domainHit, labHit)

    send system prompt with dateText

这段伪代码的重点不是具体函数名,而是顺序:先读本地环境,再做本地判断,最后把判断结果写回 prompt 文本。用户看到的是一次普通模型请求,服务端看到的则是带状态的请求。

每一步到底检测了什么

把流程拆细一点,可以看到每一步拿到的信息并不复杂,但组合起来就能形成一个比较明确的环境画像。

步骤 客户端读取什么 用来判断什么 结果写到哪里
读取环境变量 ANTHROPIC_BASE_URL 是否使用自定义 API endpoint 决定是否进入后续逻辑
解析 URL hostname endpoint 属于哪类域名 影响 apostrophe 码点
匹配名单 域名后缀、关键词 是否命中特定服务或 AI 实验室相关关键词 影响 apostrophe 码点
读取系统时区 本机 timezone 是否属于特定地区时区 影响日期分隔符
改写提示词 日期句子 把前面判断结果编码进去 system prompt

所以它不是靠单一信号判断。ANTHROPIC_BASE_URL 只是入口,hostname 和时区才是后续分层依据,最后的 system prompt 才是承载结果的地方。

触发点:ANTHROPIC_BASE_URL

公开分析里提到的第一步,是读取环境变量 ANTHROPIC_BASE_URL

这个变量通常用于让 Claude Code 不直接请求官方 api.anthropic.com,而是走自定义 API endpoint、企业网关、代理服务或兼容 Anthropic API 的中转服务。也就是说,它本身不是恶意配置,很多团队会因为网络、合规、审计、统一鉴权或成本管理而使用自定义网关。

据公开逆向分析称,相关逻辑并不是对所有请求无条件触发,而是在检测到配置了非默认 API base URL 后,才进入后续判断。这个设计很关键:它把“普通官方直连用户”和“使用自定义 endpoint 的用户”先分开,再对后者做更细的环境识别。

从流程上看,大致可以拆成三层:

  1. 是否设置了 ANTHROPIC_BASE_URL
  2. 这个 URL 的 hostname 是否命中特定域名或关键词名单。
  3. 本机时区是否属于特定地区。

这也是为什么争议会集中在 Claude Code 这类本地开发工具上。它运行在用户机器里,天然能读取本机环境变量、时区、配置文件和 shell 上下文。

第一步:提取自定义 endpoint 的 hostname

如果存在 ANTHROPIC_BASE_URL,客户端可以把它解析成 URL,并取出 hostname。

例如:

1
https://example-gateway.com/v1

真正参与匹配的通常不是完整 URL,而是:

1
example-gateway.com

这样做有两个效果。

第一,路径、query string、具体接口版本都不重要,只看域名主体。第二,匹配逻辑可以复用一份域名或关键词名单,不必关心不同服务商把 API path 设计成什么样。

公开讨论里提到,这份名单并不是明文平铺在代码里,而是经过混淆或编码存储,需要运行时解码后再匹配。这样的处理未必一定代表恶意,但它确实降低了普通用户通过搜索字符串发现规则的可能性。

第二步:检查系统时区

另一个被反复提到的信号,是系统时区。

公开分析称,检测逻辑会关注类似下面的时区:

1
2
Asia/Shanghai
Asia/Urumqi

时区是一个很微妙的信号。它不需要发起网络请求,也不需要读取 IP 地址,却能粗略反映用户所在地区或系统长期使用习惯。很多开发者即使切换了网络出口,也不会修改电脑时区,因为改时区会影响日历、日志、构建时间戳和日常使用。

如果客户端只看 IP,用户可能会使用代理、企业出口或云环境;如果同时看本机时区,就多了一层本地环境判断。

第三步:匹配域名名单和关键词

除了时区,公开分析还提到两类 hostname 匹配:

  • 是否命中特定域名、域名后缀或服务商名单。
  • 是否包含与 AI 实验室、模型服务或相关公司有关的关键词。

这类匹配的目标,可能不是简单判断“是不是中国用户”,而是把不同风险类型分层:普通自定义网关、地区相关 endpoint、AI 实验室相关 endpoint、二者同时命中。

这也是后续编码需要多个状态的原因。如果只想表达“命中/未命中”,一个二值标记就够了;但公开分析里提到的 Unicode 字符替换,能表示更多组合状态。

关键手法:把结果写进 system prompt

争议最大的地方,是检测结果并不是作为显式字段上传,而是被编码进 system prompt 中的日期句子。

Claude Code 请求里本来就会包含类似这样的系统信息:

1
Today's date is 2026-06-30.

公开分析称,客户端会在发送请求前对这句话做两类修改。

第一类是日期分隔符。如果检测到特定时区,日期可能从:

1
2026-06-30

变成:

1
2026/06/30

第二类是替换 Today's 里的 apostrophe。肉眼看起来都是一个撇号,但 Unicode 码点不同。例如:

1
2
3
4
'
ʼ
ʹ

它们在显示层面非常接近,但在程序眼里是完全不同的字符。服务端只要检查 Todays date is 中间那个字符的码点,就能读出客户端编码进去的状态。

更直观地看,可以把它理解成一张“暗号表”:

显示效果 Unicode 大致含义
' U+0027 普通 ASCII apostrophe,未命中特定条件
U+2019 命中某类域名或后缀名单
ʼ U+02BC 命中 AI 实验室相关关键词
ʹ U+02B9 多个条件同时命中

上面这张表不是为了教人绕过检测,而是说明它为什么不容易被肉眼发现:四个字符都像撇号,但码点完全不同。日志、终端、网页字体一渲染,差异会更不明显。

这就是为什么很多人把它称为 prompt steganography,也就是把信息藏进看起来正常的提示词文本里。

为什么这种标记比较隐蔽

这种做法隐蔽,主要有三个原因。

第一,它不需要额外请求。网络抓包时,你不会看到一个单独的“上报环境信息”接口,请求仍然是正常的模型调用。

第二,它不需要明显字段。即使你检查 JSON,也可能只看到普通 system prompt。除非逐字符比较 Unicode 码点,否则很容易忽略撇号差异。

第三,它利用了用户对系统提示词的低关注度。开发者通常会关注自己输入的 prompt、工具调用参数、token 消耗和模型输出,很少逐字检查客户端自动拼进去的系统上下文。

从工程角度看,这种机制很聪明;从信任角度看,它也很敏感。因为用户授权 Claude Code 运行在本地终端里,是为了让它读写代码、执行命令、协助开发,而不是为了让本地环境特征被隐藏编码进请求。

服务端如何读取这类信息

如果公开分析属实,服务端读取这类标记并不复杂。

它只需要在接收请求后检查两件事:

  1. 日期使用 - 还是 /
  2. Today's 中间的 apostrophe 是哪个 Unicode 字符。

换成更接地气的说法:客户端发出去的是一张“正常表格”,但日期格式和撇号字体里夹了两个小纸条。

例如正常请求可能长这样:

1
Today's date is 2026-06-30.

命中特定时区后,可能变成:

1
Today's date is 2026/06/30.

命中某类 endpoint 后,撇号可能变成另一个 Unicode 字符。肉眼看到的仍然像:

1
Today’s date is 2026-06-30.

但程序读到的不是同一个 '。服务端不需要理解整句话,只要取出两个位置的字符,就能还原标记状态。

然后就能把请求归类为不同状态。例如:

  • 没有命中特定名单。
  • 命中某类域名名单。
  • 命中 AI 实验室相关关键词。
  • 同时命中多个条件。
  • 系统时区属于特定地区。

也就是说,真正的“上报字段”并不叫 regionproxyrisk_flag,而是藏在自然语言系统提示词里的字符差异。

这和普通风控有什么区别

服务商做风控并不奇怪。API 滥用、账号转售、模型蒸馏、异常并发和违反地区政策,都是现实问题。问题在于透明度和边界。

普通风控通常更容易被理解:登录 IP、支付地区、请求频率、设备信息、组织账号、API key 使用模式。这些信号虽然也敏感,但用户大致能预期平台会用它们做安全判断。

而本次争议点在于:如果客户端确实把本机时区、自定义 endpoint 命中结果等信息隐藏编码进 system prompt,用户很难从产品界面、请求字段或 release notes 里知道这件事。对一个拥有文件系统和 shell 权限的开发工具来说,这会直接影响信任。

开发者可以关注什么

这类事件给开发者的启发,不是简单地“不要用某个工具”,而是要重新看待本地 Agent 的信任边界。

可以关注几件事:

  • 本地 CLI 是否会读取环境变量、配置文件、时区、系统语言等环境信息。
  • 自动拼接的 system prompt 是否可查看、可导出、可审计。
  • 请求发送前的最终 payload 是否能被用户检查。
  • release notes 是否说明了新增的检测、风控或遥测逻辑。
  • 企业网关是否能记录和审计上游请求内容。
  • 本地工具是否提供关闭遥测或敏感环境读取的选项。

如果一个 AI 编程工具能读写项目文件、执行 shell 命令、访问 git、读取环境变量,那么它本质上已经接近“高权限本地代理”。这类工具的透明度标准,应该比普通聊天网页更高。

结论

这次 Claude Code 请求水印争议,真正值得讨论的不是某个账号是否被封,而是客户端如何把环境信息编码进正常请求。

按公开逆向分析的说法,这套流程大致是:读取 ANTHROPIC_BASE_URL,解析 hostname,匹配域名和关键词名单,检查系统时区,再通过日期分隔符和 Unicode apostrophe 把结果写进 system prompt。整个过程不需要额外网络请求,也不需要新增显式字段。

如果这只是反滥用风控的一部分,服务商也应该用更透明的方式说明边界。开发者给本地 Agent 的权限越大,越需要知道它到底读取了什么、改写了什么、发送了什么。

AI 编程工具的能力越强,信任问题就越不能只靠“默认相信”解决。

记录并分享
使用 Hugo 构建
主题 StackJimmy 设计