源码泄漏带来的意外惊喜
大家好,我是卡卡。
相信前两天的Claude Code源码泄露事件大家都有听说过吧?不管是公众号这两天的热点还是掘金文章的热搜,几乎都是在讨论Claude Code源码泄露的问题。源码我大概也看了别的大佬整理后的,确实很震撼。
不过话说回来,大家有没有发现这两天在 Claude Code 终端写代码的时候,底部出现了一个彩虹色命令?

其实这个是Claude Code在4月1号悄悄上线的宠物模式,只需要输入 /buddy 就会孵化出一只专属于你的小宠物,连命令都是彩虹色的。
我看了对应的说明才知道,其实这玩意是有概率的,并不是你想要啥就开出啥,其实就是随机生成的,跟开盲盒一样。
宠物系统详解
我翻了翻泄露的源码,才发现这个电子宠物系统比我想的要复杂不少。它有几个核心要素:物种、稀有度、眼睛、帽子、闪光状态,还有五维属性值。
先说物种,这个电子宠物总共支持18个物种:
duck 鸭子 · goose 鹅 · cat 猫 · dragon 龙 · octopus 章鱼 · owl 猫头鹰 · penguin 企鹅 · turtle 乌龟 · snail 蜗牛 · ghost 幽灵 · axolotl 蝾螈 · capybara 水豚 · robot 机器人 · rabbit 兔子 · mushroom 蘑菇 · blob 团子 · cactus 仙人掌 · chonk 胖猫

每个物种都有自己的ASCII形象,还挺可爱的。
这些ASCII宠物有一些设计特点:
- 每个宠物都是固定尺寸,约5行高、10-12列宽
- 使用纯文本字符绘制,不依赖图片资源
- 眼睛和帽子是可替换的占位符,用
{E}和{H}标记 - 帽子只有在非普通品质时才会显示
然后是稀有度,这个有点意思,分五个等级,概率也不是平均的,传说只有1%:
| 稀有度 | 星级 | 概率 | 属性下限 |
|---|---|---|---|
| 普通 | ★ | 60% | 5 |
| 少见 | ★★ | 25% | 15 |
| 稀有 | ★★★ | 10% | 25 |
| 史诗 | ★★★★ | 4% | 35 |
| 传说 | ★★★★★ | 1% | 50 |
帽子这块有8种款式,也都有对应的ASCII符号:
scss
none 无 tophat 礼帽 propeller 螺旋桨 halo 光环
[___] -(*)- ○
wizard 巫师帽 beanie 毛线帽 crown 皇冠 tinyduck 小鸭子
/^\ ~~~ ♛♛♛♛♛ 🦆
不过要注意哦,普通品质的宠物是没有帽子的,只有少见及以上稀有度才能戴帽子。
眼睛也有6种样式:
· 圆点 ✦ 星星 × 叉号 ◉ 实心 @ 螺旋 ° 空心
再就是闪光机制,这个跟宝可梦里的闪光概念差不多,概率只有1%,闪光宠物会有特殊光效,颜色也会变,算是比较稀有的。
最后说一下五维属性,每个宠物都有五项数值:
| 属性 | 中文名 | 说明 |
|---|---|---|
| DEBUGGING | 调试 | 调试能力 |
| PATIENCE | 耐心 | 耐心程度 |
| CHAOS | 混乱 | 混乱指数 |
| WISDOM | 智慧 | 智慧值 |
| SNARK | 吐槽 | 吐槽能力 |
每个属性范围是1-100,稀有度越高属性下限越高。传说品质的宠物属性下限50,普通品质只有5,差距还是挺大的。
我的开箱结果
我昨天没事也来开了一个,结果最终开出来是这只:

我来简单拆解一下这张图:
- 物种:dragon 龙 ------ 这个一眼就能看出来
- 稀有度:common 普通(★)------ 只有颗星,最基础的等级,概率60%
- 帽子:无 ------ 普通品质没有帽子,只有少见及以上才能戴
- 眼睛:圆点样式 ------ 默认的眼睛类型
- 闪光:否 ------ 普通灰色,没有光效
- 属性:五维数值都比较低,普通品质下限只有5
不过对我来说,开出的这一只灰灰的普通龙,啥装饰都没有,挺寒酸的。
我本来试了下能不能重新生成,结果发现根本不行。开出来啥就是啥,想换个新的?没办法呀。
但人家开出来的金色传说龙戴皇冠,闪光带光效,真的太羡慕了。同样都是龙,差距咋这么大呢?
所以我就去看了下源码,看看这个buddy到底是怎么生成的。
宠物生成逻辑
翻了翻源码后,我发现这套宠物系统设计得还挺有意思的。整个生成流程分散在好几个文件里,我就一个个来看。
首先是 buddy-generator.ts,这个是核心生成器。里面用了一个叫 Mulberry32 的随机算法。这个算法是个PRNG(伪随机数生成器),特点是可以用种子初始化,相同的种子永远产生相同的随机序列。也就是说,一旦种子确定了,后续所有的"随机"选择其实都是确定的。
那种子从哪来?看 user-seed.ts,里面定义了种子生成逻辑:
typescript
const SALT = 'friend-2026-401';
function getUserSeed(userID: string): number {
return hashString(userID + SALT);
}
这里有个固定盐值 "friend-2026-401",硬编码在源码里。用户ID加上这个盐值,算出一个哈希值,就是种子。
有了种子,生成器就能开始"随机"选择了。物种、稀有度、眼睛、帽子、闪光状态、五维属性,全都是从预定义的池子里按顺序抽出来的。因为种子固定,所以抽出来的结果也固定。
源码里还把宠物分成了两层:
骨架层(Bones) ------ BuddySkeleton 类
这一层管的是外观。物种是啥、稀有度多少、戴啥帽子、眼睛啥样、是否闪光,这些都属于骨架。骨架一旦生成就不可变,因为它完全由种子决定。
灵魂层(Soul) ------ BuddySoul 类
这一层管的是性格。宠物的名字、性格描述、说话风格,都属于灵魂。这部分不是由种子决定的,而是第一次孵化时由Claude实时生成的,存在本地配置文件里。所以灵魂是可以重置的,删掉本地记录重新孵化,就会生成新的性格。
本地存储位置在 ~/.claude.json,里面有个 companion 字段存着灵魂数据。骨架数据不存,因为每次都能用种子重新算出来。
简单来说:
- 骨架层 = userID + SALT → Mulberry32 → 外观属性(不可变)
- 灵魂层 = 本地存储 → 性格描述(可重置)
核心公式
我们把上面的逻辑抽象一下,就是下面这个公式:
scss
宠物属性 = Hash(userID + SALT) → Mulberry32 → 随机抽取
其中:
userID:我们的用户标识SALT:固定值"friend-2026-401"(硬编码在源码里)
关键点:相同的 userID 永远生成相同的宠物。
所以理论上:
ini
改 userID = 改宠物
发现漏洞
既然我知道了生成逻辑,接下来就是找突破口。
正常情况下,如果我们用官方登录,Claude Code 会把我们的 accountUuid 写入 ~/.claude.json。这个值绑定我们的真实 Anthropic 账户,是服务器下发的一串唯一标识符,无法伪造。
也就是说,官方登录用户的开箱结果完全由 accountUuid 决定,我们改不了,所以宠物也改不了。
我看了下配置文件,确实有个 accountUuid 字段:
json
{
"accountUuid": "xxxxx-xxxxx-xxxxx-xxxxx",
...
}
那能不能改这个字段呢?不行,改了下次登录会被覆盖重写。而且就算强行改成一个假的UUID,服务器那边验证不了,登录都成问题。
我就在想,有没有别的入口?
看源码里 /buddy 命令的实现,发现其实它会先尝试读取 accountUuid,如果不存在,就会退而求其次去读另一个字段 ------ userID。
关键是,这个 userID 字段跟 accountUuid 不一样,它不是服务器下发的,而是本地生成的。Claude Code 有个逻辑:如果用户用环境变量 CLAUDE_CODE_OAUTH_TOKEN 登录,就不会写入 accountUuid,只写入 userID。
这意味着什么?意味着在没有 accountUuid 的情况下,userID 成了决定宠物的唯一因素。而 userID 这个字段......
我可以随便改!
你敢信???
一个简单的本地配置字段,竟然能决定宠物的所有属性。改一行配置,换个宠物。
但是注意!这里有个前提条件:
如果配置文件里已经有 accountUuid(用官方 OAuth 登录过),宠物是由 accountUuid 决定的,改 userID 没用。
要让 userID 生效,必须:
- 删除
accountUuid字段,或者 - 用环境变量
CLAUDE_CODE_OAUTH_TOKEN登录 (这种登录方式不会写入accountUuid)
所以我们完整的操作步骤是:
bash
# 1. 编辑配置文件
vim ~/.claude.json
# 2. 删除 accountUuid 这一行(如果有的话)
# 3. 删除 companion 这一行(旧的宠物灵魂数据)
# 4. 写入脚本生成的 userID
# 5. 重启 Claude Code,输入 /buddy 领取新宠物
暴力美学
既然能改 userID,那事情这不就简单了嘛。
我的思路是这样的:我随机生成一个 userID,用跟 Claude Code 完全一样的算法算一下这个 ID 对应什么宠物。如果生成的宠物符合我们想要的条件,就把这个 userID 写进配置文件。如果不符合,就换下一个 userID 继续试。
其实本质上就是一个暴力搜索:穷举 userID,直到命中目标宠物。
我写了个简单的实现脚本:
javascript
for (let i = 0; i < 50000000; i++) {
const uid = randomBytes(32).toString('hex');
const pet = rollPet(uid);
if (pet.species === 'capybara' &&
pet.rarity === 'legendary' &&
pet.shiny) {
console.log('找到了!', uid);
break;
}
}
这段代码做的事情很简单:
- 生成一个随机的 64 字符十六进制 userID(32 字节,即 256 位)
- 用跟源码完全一致的算法算出这个 userID 对应的宠物属性
- 检查是不是传说闪光水豚
- 不是就继续下一个,是就停下来输出结果
这里说明一下:源码里的 userID 格式是
randomBytes(32).toString('hex'),即 64 个十六进制字符(32 字节,256 位)。我们用相同的格式生成,确保和源码一致。
循环最多跑 5000 万次。听起来很多,但实际上现代计算机跑这种纯计算任务很快。因为整个过程不需要网络请求,不需要读写文件,就是纯数学运算,每秒能算几十万次。
最终把找到的 userID 写进 ~/.claude.json,重启 Claude Code,输入 /buddy。
传说中的金色闪光龙,就可以到手啦。
关于搜索复杂度
我本地跑了几轮脚本测试,统计了一下不同目标需要的搜索时间:
| 目标 | 预期尝试 | 耗时 |
|---|---|---|
| 普通品质指定物种 | ~18 次 | <1ms |
| 稀有品质指定物种 | ~180 次 | <1ms |
| 传说品质指定物种 | ~1800 次 | <1ms |
| 传说 + 指定帽子 | ~18000 次 | <1ms |
| 传说 + 闪光 | ~180 万次 | ~1s |
那为什么不同目标差距这么大?
其实是因为每个条件都会缩小命中范围。物种有18种,指定一个物种就要试约18次才能命中;稀有度有5档,传说只有1%概率,所以指定传说要再乘100倍;闪光概率更狠,只有1%,又要乘100倍。多个条件叠加,搜索次数就是指数级增长。
不过实际测试下来,脚本执行速度非常快。因为算法本身很简单,纯数学运算没有网络请求和文件读写,Bun引擎跑起来每秒能处理几十万次循环。基本不到一秒就能刷到传说闪光。
如果 5000 万次还没找到(极端情况),可以增大 --max 参数。
完全可以接受的哇。
做了个工具
为了方便测试,我用 Bun 也写了个交互式工具来修改电子宠物。
核心思路很简单,就是把我前面分析的生成逻辑完整复现一遍:
第一步,复现 Mulberry32 随机算法。这是整个生成流程的基础,必须跟源码完全一致:
javascript
function mulberry32(seed) {
let a = seed >>> 0;
return function() {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
第二步,复现种子生成逻辑。用 Bun 内置的哈希函数,加上固定盐值:
javascript
const SALT = 'friend-2026-401';
function hashString(s) {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn);
}
function getUserSeed(uid) {
return hashString(uid + SALT);
}
第三步,按照源码的顺序依次抽取属性。物种、稀有度、眼睛、帽子、闪光、属性,每一步都要用同一个随机序列:
javascript
function rollFull(uid) {
const rng = mulberry32(hashString(uid + SALT));
const rarity = rollRarity(rng); // 先抽稀有度
const species = pick(rng, SPECIES); // 再抽物种
const eye = pick(rng, EYES); // 抽眼睛
const hat = rarity === 'common' ? 'none' : pick(rng, HATS); // 抽帽子
const shiny = rng() < 0.01; // 1%概率闪光
const stats = rollStats(rng, rarity); // 最后抽属性
return { rarity, species, eye, hat, shiny, stats };
}
顺序很重要。源码里是先抽稀有度再抽物种,如果顺序搞反了,生成的结果就跟真实宠物不一致了。
最后一步,暴力搜索。随机生成 userID,计算属性,检查是否符合条件:
javascript
for (let i = 0; i < 50000000; i++) {
const uid = randomBytes(32).toString('hex');
const pet = rollFull(uid);
if (pet.species === targetSpecies &&
pet.rarity === targetRarity &&
pet.shiny === targetShiny) {
// 找到了!写入配置
break;
}
}
整个工具就是把这些逻辑封装起来,加上交互式界面,让用户选择想要的物种、稀有度、帽子、眼睛、是否闪光,然后自动搜索并写入配置文件。
测试一下
写完后我等不及想要自己的金色传说,工具运行结果如下:

金色传说,我踏马来辣!!!

为什么必须用 Bun?
这个问题很关键。
我翻了翻 linux.do 上大佬们的逆向分析,发现源码里 hashString 函数有两条路径:
typescript
function hashString(s: string): number {
if (typeof Bun !== 'undefined') {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn) // Bun 环境
}
// FNV-1a fallback(Node.js)
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
Claude Code 的二进制是用 Bun 打包的,运行时 typeof Bun !== 'undefined' 为 true,所以实际走的是 Bun.hash() 这条路。
如果我们用 Node.js 跑脚本,会走下面的 FNV-1a fallback。问题是,Bun.hash 和 FNV-1a 算出来的哈希值完全不同。同一个字符串 "xxx" + "friend-2026-401",两种算法算出来的数值不一样。
后果就是:脚本算出来是 legendary dragon,写入配置后重启,开出来却是只 common blob。
用 Node.js 生成不了正确的宠物。必须用 Bun。
另外,请确保我们的 Bun 版本不要太旧。Claude Code 目前使用 Bun 1.2.x 打包,不同版本的 Bun.hash() 实现可能有细微差异。如果我们跑出来的宠物还是不对,需要试试升级 Bun:
bash
bun upgrade
那这种方式安全吗?
可能会有同学担心我这样暴力修改会不会导致一系列安全问题,那我来简单解释一下。
会不会损坏本地文件?
其实不会。脚本只改一个文件:~/.claude.json,而且只改一个字段:userID。这个文件是 Claude Code 的配置文件,存的是一些本地设置,比如主题、遥测ID之类的。改了 userID 不会影响我们的对话历史、项目配置、任何代码文件。
就算改坏了,大不了删掉这个文件,Claude Code 下次启动会自动重新生成一个。我们的对话记录、项目文件都在其他地方,完全不受影响。
会不会被封号?
肯定不会的。首先,脚本是完全离线运行的,不发送任何网络请求,Anthropic 那边根本不知道我跑了脚本。其次,userID 只是个本地标识符,用于遥测分析和 buddy 种子,跟我们的账号订阅、API 额度、账单完全无关。它不是 accountUuid,不是 API key,不是任何敏感凭证。
我们改 userID 跟改本地配置里的主题色没区别,Anthropic 不可能因为用户改了本地配置就封号。
会不会有不可逆的影响?
这个也不会。想恢复原来的宠物?其实有两种方式:
- 删掉配置里的
userID字段,用官方 OAuth 重新登录,会自动生成新的accountUuid,宠物绑定回我们的真实账户 - 记下原来的 userID,想换回去随时写回去
整个过程可逆,随时恢复。
那这个Buddy 到底有啥用?
其实说实在的,这玩意儿不是帮我们写代码的,就是陪我写代码的。陪玩?额,不对,是陪伴电子宠物。。。
一开始我也以为就是个装饰,后来看了源码才发现,它其实也是有性格的。就比如我前面介绍的那五个属性:调试、耐心、混乱、智慧、吐槽。不是摆着好看的。每只宠物的性格都不一样,吐槽高的会嫌弃你代码写得烂,智慧高的偶尔蹦两句金句,混乱高的......你永远猜不到它下一句说啥。
而且它不消耗 API 用量,官方说了不计数。想怎么玩都行,不会扣额度。
有哪些命令?
/buddy------ 第一次是孵化宠物,之后输入就是把它叫出来/buddy pet------ 撸它一下,会冒爱心动画/buddy rename <名字>------ 给它改名/buddy off------ 关掉宠物,不想看就藏起来
另外直接喊它的名字,它也会搭理你,偶尔对当前代码发表点看法。
说白了就是愚人节的一个小彩蛋,没啥生产力加成。但一个人在终端里 debug 到凌晨的时候,旁边蹲只会冒爱心的 ASCII 小动物,多少能舒服一点,嘻嘻。
写在最后
其实呢我们这篇文章梳理这个电子宠物实现的核心思路很简单:确定性的随机算法 + 可修改的本地字段 = 可穷举的盲盒。看了源码后才发现其实也不算特别难理解,挺不错的一个数学算法逻辑。
通过工具我也成功刷到了属于我自己的金色传说。大家有空也可以去看看大佬们整理好的 Claude Code 源码,挺有意思的。这大概就是逆向的快乐吧。
另外这篇文章的前提是使用 API 接入方式的 Claude Code。如果你是官方 OAuth 登录,流程会有点不一样,需要额外处理 accountUuid 的问题。这块卡兹克大佬写了很详细的文章,推荐大家去看看哈。传送门: Claude Code宠物系统悄悄上线,我把白板刷成了金色传说。
如果不想自己折腾脚本,市面上也有现成的工具,比如 cc-buddy 和 any-buddy,都是大佬们做的,功能挺完善,有兴趣也可以去试试。
以上就是我的分析,希望能给感兴趣的朋友一点启发。祝大家都能刷到自己想要的电子宠物!