有人在Reddit上发帖说,Claude Code发给Anthropic API的请求里藏了个奇怪的东西。

不是普通的system prompt,而是混在system数组最前面的一个特殊字段。长得像这样:

{
  "type": "text",
  "text": "x-anthropic-billing-header: cc_version=2.1.37.fbe; cc_entrypoint=cli; cch=a112b;"
}

三个字段:cc_version带版本号和一个神秘后缀,cc_entrypoint固定是clicch是5位十六进制字符,每次请求都会变。

这玩意儿是什么?怎么生成的?为什么搞这么复杂?

有人决定把它搞清楚。


从黑盒开始:Claude Code逆向工程三板斧

Claude Code发布的时候是个Bun二进制文件。JavaScript打包成单个可执行文件,没有node_modules,没有源码,只有编译后的字节码。

研究者用了三招:

第一招是MITM拦截。用mitmproxy搭个代理,看实际发出的请求长什么样。

第二招是二进制提取。Bun会把JavaScript源码嵌入可执行文件。理论上可以提取出来,但得到的是个2MB的minify过的blob,变量名全是单个字母。

第三招是运行时调试。用LLDB挂在进程上,在内存里设监视点,等着看哈希值被写进去的瞬间。

实际操作的时候,三种方法是一起上的。


第一个发现:JavaScript里的cch=00000占位符

在minify过的源码里搜索cch,确实找到了billing header的构造逻辑。但代码长这样:

function u7R(T) {
  let R = `${VERSION}.${T}`;
  let A = process.env.CLAUDE_CODE_ENTRYPOINT ?? "unknown";
  return `x-anthropic-billing-header: cc_version=${R}; cc_entrypoint=${A}; cch=00000;`;
}

cch=00000,永远都是这个。占位符,JavaScript层面根本不负责填它。

真正的问题来了:谁在负责填?


秘密藏在Zig代码里

Claude Code用的Bun是个定制版:1.3.9-canary.51+d5628db23。这个commit在公开的oven-sh/bun仓库里根本不存在。

Anthropic收购了Oven(Bun背后的公司)。所以他们有能力在运行时层面动手脚。

定制版的Bun有个原生fetch实现,用Zig编写。Claude Code发请求时走的不是标准JavaScript的fetch,而是这个自定义的原生版本。

这个原生fetch做了三件事:

  • 检查URL路径是否包含/v1/messages
  • 检查是否有anthropic-version
  • 检查请求体里是否还留着cch=00000这个占位符

三个条件都满足,它才会动手。

它先把整个请求体(带着那个00000占位符)做一次xxHash64哈希,然后把结果的低20位转成5个十六进制字符,直接原地覆盖那五个零。

整个过程发生在C语言级别的内存里。JavaScript根本看不见。


xxHash64:跑得飞快的哈希算法

找到算法用了点技巧。JavaScript代码里没有线索,研究者就去看内存。

用LLDB在cch=00000那块内存区域设监视点,然后触发一次请求。监视点命中时拿到了调用栈,一路追到___lldb_unnamed_symbol18111这个地址。

符号表?没有。调试信息?没有。只有一堆机器码。

反汇编一看,里面有五个质数常量:0x9E3779B185EBCA870xC2B2AE3D27D4EB4F,还有一个标准 avalanche 结尾流程。

这是xxHash64的教科书实现。直接内联在二进制里,没调用任何外部库。

种子保存在二进制数据段里:0x103e900e0。是个64位常量,一组四个存放在一块。

xxHash64算法示意:五个质数常量环绕哈希晶体

研究者把这四个种子轮流试了一遍,用之前抓到的142对输入输出做验证。全部匹配。


cc_version后缀:从对话内容里算出来

cch是原生代码算的,但cc_version的后缀来自JavaScript。

规则是这样的:

取对话里第一条用户消息,从字符索引4、7、20的位置各取一个字符。如果消息太短,取不到就补零。把这些字符、盐值、版本号拼在一起,做一次SHA-256,取结果的前三个十六进制字符。

chars = "".join(msg[i] if i < len(msg) else "0" for i in (4, 7, 20))
suffix = sha256(f"{salt}{chars}{version}".encode()).hexdigest()[:3]

所以同一个版本号,后缀会随着对话内容变化。这也是为什么每次请求cc_version都不一样。


什么被xxHash64哈希保护了

研究团队做了系统性的验证实验。抓一批真实的请求,然后各种改:

修改方式结果
完全不变重放200成功
改system prompt(非billing部分)200成功
换掉session UUID400拒绝
删掉一个tool400拒绝
改一个tool描述里的一个字400拒绝
加一个MCP tool400拒绝
tools数组留空400拒绝

结论很清楚:哈希覆盖了整个请求体的序列化结果。messages、tools、metadata、model配置、thinking配置,全部在内。

唯一能改的是system prompt里非billing的部分,因为那部分是在哈希算完、占位符塞进去之后,才被注入到system数组里的。


不是DRM,是计费

cch这个名字已经暗示了它的用途。这是"billing header",是给Anthropic服务器做归属和计量用的。

选择xxHash64(非加密哈希)也说明问题。它快,不安全。设计逻辑是混淆,不是加密。

用xxHash64意味着这不是访问控制。如果真想防滥用,应该用HMAC之类的对称或非对称签名。研究者的测试显示:

  • 不是TLS指纹检测
  • 没有二进制认证
  • 没有握手流程
  • 不检测重放(同一请求可以发多次)
  • 不检查UUID格式

只要哈希值对上了,服务器就认。

这本质上是防止未授权客户端白嫖特定功能(比如快速模式)的护城河,而不是什么安全边界。


为什么这个Claude Code签名机制发现重要

有了这些信息,任何第三方工具都可以自己实现这套签名逻辑。不需要用Bun二进制,不需要逆向。

大约30行代码就能搞定:

import hashlib, json, uuid, xxhash

# 从Keychain拿OAuth token
creds = json.loads(subprocess.check_output([
    "security", "find-generic-password",
    "-a", os.environ["USER"], "-s", "Claude Code-credentials", "-w"
], text=True).strip())
token = creds["claudeAiOauth"]["accessToken"]

# 算版本后缀
chars = "".join(prompt[i] if i < len(prompt) else "0" for i in (4, 7, 20))
suffix = hashlib.sha256(f"{salt}{chars}{version}".encode()).hexdigest()[:3]

# 构造请求体,放占位符
body = json.dumps({...}, separators=(",", ":"))

# 算cch
cch = format(xxhash.xxh64(body.encode(), seed=SEED).intdigest() & 0xFFFFF, "05x")
body = body.replace("cch=00000", f"cch={cch}")

# 发请求

请求签名流程:JS构造 → Zig验证 → 服务器校验

最复杂的部分是想清楚JSON key的顺序。占位符在system字段里,但如果你的JSON库把messages排在system前面——而对话内容里恰好有cch=00000这个字符串——替换就会打错位置。

解决方案:确保JSON序列化时system在messages前面。而且哈希要补足5位,4位的651f不会匹配5位的0651f


一个小问题:Bun原生fetch的字符串Mutation

这里有个有意思的细节。Bun的原生fetch会原地修改JavaScript字符串。fetch()返回之后,你传进去的body变量里的字节已经不一样了。

JavaScript的字符串按规范应该是不可变的。这是规格违反。

更糟糕的是,JavaScriptCore(JSC,Bun用的引擎)会在多个字符串引用之间共享内部存储。如果你写了const alias = body,两个变量指向同一块内存。fetch()返回后,两个都会被改。

Map和Set的key、interned字符串、rope substring,全都有这个问题。任何代码如果在某个运行时的字符串里恰好藏着cch=00000,然后又往/v1/messages发了个fetch——即使是打给完全不同的服务器——那个字符串也会被悄悄改掉。


能学到什么:从Anthropic签名机制看代码隐蔽性设计

这个发现给我们的最大启示,不是Anthropic做了什么,而是他们怎么做。

把哈希塞进原生运行时的设计决策很有意思。JavaScript层面完全没有痕迹。你没法通过trace JS调用栈找到这个函数,没法通过hook JS函数拦截这个行为。占位符进去,a112b出来,在JavaScript眼里就像魔法。

这个设计的思路挺有意思:不是在门口装锁,而是把门藏起来让人找不到。

对于想和Anthropic API深度集成的开发者来说,知道这些细节有助于理解边界在哪里、什么可以改、什么不能碰。对于其他人,这也是个不错的案例,看看商业软件里还有什么藏在视线之外的东西。


常见问题

Q: 这个签名机制是Anthropic的官方API吗?

不是。这是Claude Code客户端的实现细节,不是Anthropic对外公开的API规范。官方API文档里没有提到x-anthropic-billing-header这个字段。

Q: 有了这个发现,我能用Claude Code的功能而不付钱吗?

不能。cch哈希是计量和归属机制,不是访问控制。服务器用它来识别合法的Claude Code客户端流量并计量,不影响API使用本身的费用。

Q: 这个机制能被封禁或修改吗?

可以。Anthropic随时可以改算法或种子,而且每次Claude Code更新都会换一组常量。从JavaScript提取种子需要二进制分析,自动化难度大,但不是不可能。