Claude Code buddy 宠物系统逆向分析 —— 如何重置并刷到你想要的宠物
- 内容介绍
- 文章标签
- 相关推荐
Claude Code 最近更新了一个 /buddy 功能,可以领养一只专属小宠物。
领了之后给我一直仙人掌… 什么鬼
想换一只但是官方没给重置入口。那就只能自己动手了。
省流版
根据你 Claude Code 的安装方式选择执行命令, npm 安装用 node, native 安装用 bun
node buddy-reroll.js [options]
bun buddy-reroll.js [options]
buddy-reroll.js
#!/usr/bin/env node
// buddy-reroll.js
// Buddy reroll script — supports both Node.js and Bun
// Bun.hash results match actual Claude Code; Node.js (FNV-1a) results do NOT.
//
// Usage:
// node buddy-reroll.js [options] # runs with FNV-1a (Node mode)
// bun buddy-reroll.js [options] # runs with Bun.hash (Bun mode)
//
// Options:
// --species <name> Target species (duck, cat, dragon, ...)
// --rarity <name> Minimum rarity (common, uncommon, rare, epic, legendary)
// --eye <char> Target eye style (· ✦ × ◉ @ °)
// --hat <name> Target hat (none, crown, tophat, propeller, halo, wizard, beanie, tinyduck)
// --shiny Require shiny
// --min-stats [value] Require ALL stats >= value (default: 90)
// --max <number> Max iterations (default: 50000000)
// --count <number> Number of results to find (default: 3)
// --check <uid> Check what buddy a specific userID produces
//
// Examples:
// bun buddy-reroll.js --species duck --rarity legendary --shiny
// bun buddy-reroll.js --species dragon --min-stats 80
// bun buddy-reroll.js --check f17c2742a00b2345c22fddc830959a6847ceb561fa06adb26b74b1a91ac657bc
const crypto = require('crypto')
// --- Constants (must match Claude Code source) ---
const SALT = 'friend-2026-401'
const SPECIES = ['duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin', 'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus', 'robot', 'rabbit', 'mushroom', 'chonk']
const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary']
const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }
const RARITY_RANK = { common: 0, uncommon: 1, rare: 2, epic: 3, legendary: 4 }
const EYES = ['·', '✦', '×', '◉', '@', '°']
const HATS = ['none', 'crown', 'tophat', 'propeller', 'halo', 'wizard', 'beanie', 'tinyduck']
const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK']
const RARITY_FLOOR = { common: 5, uncommon: 15, rare: 25, epic: 35, legendary: 50 }
// --- Hash functions ---
function hashFNV1a(s) {
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function hashBun(s) {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}
// --- PRNG (Mulberry32 — same as Claude Code) ---
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
}
}
function pick(rng, arr) {
return arr[Math.floor(rng() * arr.length)]
}
function rollRarity(rng) {
let roll = rng() * 100
for (const r of RARITIES) {
roll -= RARITY_WEIGHTS[r]
if (roll < 0) return r
}
return 'common'
}
function rollStats(rng, rarity) {
const floor = RARITY_FLOOR[rarity]
const peak = pick(rng, STAT_NAMES)
let dump = pick(rng, STAT_NAMES)
while (dump === peak) dump = pick(rng, STAT_NAMES)
const stats = {}
for (const name of STAT_NAMES) {
if (name === peak) stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30))
else if (name === dump) stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15))
else stats[name] = floor + Math.floor(rng() * 40)
}
return stats
}
function createRoller(hashFn) {
return function rollFull(uid) {
const rng = mulberry32(hashFn(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
const stats = rollStats(rng, rarity)
return { rarity, species, eye, hat, shiny, stats }
}
}
// --- Parse CLI args ---
function parseArgs() {
const args = process.argv.slice(2)
const opts = { max: 50_000_000, count: 3 }
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--species': opts.species = args[++i]; break
case '--rarity': opts.rarity = args[++i]; break
case '--eye': opts.eye = args[++i]; break
case '--hat': opts.hat = args[++i]; break
case '--shiny': opts.shiny = true; break
case '--min-stats': {
const next = args[i + 1]
opts.minStatsVal = (next && !next.startsWith('--')) ? parseInt(args[++i]) : 90
break
}
case '--max': opts.max = parseInt(args[++i]); break
case '--count': opts.count = parseInt(args[++i]); break
case '--check': opts.check = args[++i]; break
case '--help': case '-h':
console.log(`Usage: node/bun buddy-reroll.js [options]
Options:
--species <name> ${SPECIES.join(', ')}
--rarity <name> ${RARITIES.join(', ')}
--eye <char> ${EYES.join(' ')}
--hat <name> ${HATS.join(', ')}
--shiny Require shiny
--min-stats [value] Require ALL stats >= value (default: 90)
--max <number> Max iterations (default: 50000000)
--count <number> Results to find (default: 3)
--check <uid> Check what buddy a specific userID produces`)
process.exit(0)
}
}
// Validate
if (opts.species && !SPECIES.includes(opts.species)) {
console.error(`Unknown species: ${opts.species}\nAvailable: ${SPECIES.join(', ')}`)
process.exit(1)
}
if (opts.rarity && !RARITIES.includes(opts.rarity)) {
console.error(`Unknown rarity: ${opts.rarity}\nAvailable: ${RARITIES.join(', ')}`)
process.exit(1)
}
if (opts.eye && !EYES.includes(opts.eye)) {
console.error(`Unknown eye: ${opts.eye}\nAvailable: ${EYES.join(' ')}`)
process.exit(1)
}
if (opts.hat && !HATS.includes(opts.hat)) {
console.error(`Unknown hat: ${opts.hat}\nAvailable: ${HATS.join(', ')}`)
process.exit(1)
}
return opts
}
// --- Main ---
const opts = parseArgs()
const isBun = typeof Bun !== 'undefined'
const hashFn = isBun ? hashBun : hashFNV1a
const rollFull = createRoller(hashFn)
const runtimeLabel = isBun ? 'bun (Bun.hash)' : 'node (FNV-1a)'
const RARITY_STARS = { common: '★', uncommon: '★★', rare: '★★★', epic: '★★★★', legendary: '★★★★★' }
// --- Check mode ---
if (opts.check) {
console.log(`Runtime: ${runtimeLabel}`)
console.log(`Checking userID: ${opts.check}\n`)
const r = rollFull(opts.check)
console.log(` Species : ${r.species}`)
console.log(` Rarity : ${r.rarity} ${RARITY_STARS[r.rarity]}`)
console.log(` Eye : ${r.eye}`)
console.log(` Hat : ${r.hat}`)
console.log(` Shiny : ${r.shiny}`)
console.log(` Stats :`)
for (const name of STAT_NAMES) {
const val = r.stats[name]
const bar = '█'.repeat(Math.floor(val / 5)) + '░'.repeat(20 - Math.floor(val / 5))
console.log(` ${name.padEnd(10)} ${bar} ${val}`)
}
process.exit(0)
}
const filters = []
if (opts.species) filters.push(`species=${opts.species}`)
if (opts.rarity) filters.push(`rarity>=${opts.rarity}`)
if (opts.eye) filters.push(`eye=${opts.eye}`)
if (opts.hat) filters.push(`hat=${opts.hat}`)
if (opts.shiny) filters.push('shiny=true')
if (opts.minStatsVal) filters.push(`all stats>=${opts.minStatsVal}`)
console.log(`Runtime: ${runtimeLabel}${isBun ? '' : ' (results will NOT match Claude Code)'}`)
console.log(`Searching: ${filters.join(', ') || 'any'} (max ${opts.max.toLocaleString()}, find ${opts.count})`)
console.log('')
const minRarityRank = opts.rarity ? RARITY_RANK[opts.rarity] : 0
let found = 0
const startTime = Date.now()
for (let i = 0; i < opts.max; i++) {
const uid = crypto.randomBytes(32).toString('hex')
const r = rollFull(uid)
if (opts.rarity && RARITY_RANK[r.rarity] < minRarityRank) continue
if (opts.species && r.species !== opts.species) continue
if (opts.eye && r.eye !== opts.eye) continue
if (opts.hat && r.hat !== opts.hat) continue
if (opts.shiny && !r.shiny) continue
if (opts.minStatsVal && !Object.values(r.stats).every(v => v >= opts.minStatsVal)) continue
found++
const statsStr = STAT_NAMES.map(n => `${n}:${r.stats[n]}`).join(' ')
console.log(`#${found} [${r.rarity}] ${r.species} eye=${r.eye} hat=${r.hat} shiny=${r.shiny}`)
console.log(` stats: ${statsStr}`)
console.log(` uid: ${uid}`)
console.log('')
if (found >= opts.count) break
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
if (found === 0) {
console.log(`No match found in ${opts.max.toLocaleString()} iterations (${elapsed}s)`)
} else {
console.log(`Found ${found} match(es) in ${elapsed}s`)
}
OAuth 用户看这里:
NaynIruR:oauth 用这个办法 Claude Oauth登录刷 /buddy 宠物的方法找到了
分析细节
宠物系统原理
让 AI 分析了昨晚泄露的源码(src/buddy/),整个系统分两层:
Bones(骨架) —— 外观、物种、稀有度、属性,全部由 hash(userID + SALT) 确定性生成,不存储在配置中,每次读取时实时计算。
Soul(灵魂) —— 名字和性格,由模型生成,存储在 ~/.claude.json 的 companion 字段中。
// src/buddy/companion.ts
const SALT = 'friend-2026-401'
export function companionUserId(): string {
const config = getGlobalConfig()
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
}
也就是说:同一个 userID,永远生成同一只宠物。
一共有多少种?
18 个物种:
| 物种 | 物种 | 物种 |
|---|---|---|
| duck | goose | blob |
| cat | dragon | octopus |
| owl | penguin | turtle |
| snail | ghost | axolotl |
| capybara | cactus | robot |
| rabbit | mushroom | chonk |
5 个稀有度:
| 稀有度 | 权重 | 概率 |
|---|---|---|
| ★ common | 60 | 60% |
| ★★ uncommon | 25 | 25% |
| ★★★ rare | 10 | 10% |
| ★★★★ epic | 4 | 4% |
| ★★★★★ legendary | 1 | 1% |
还有 6 种眼睛(· ✦ × ◉ @ °)、8 种帽子、5 项属性(DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK),以及 1% 概率的 shiny 闪光版。
userID 是什么?
没登录 OAuth 账号的情况下,userID 是首次启动时随机生成的 32 字节 hex 字符串:
// src/utils/config.ts
const userID = randomBytes(32).toString('hex')
saveGlobalConfig(current => ({ ...current, userID }))
它只用于:遥测分析(匿名 device_id)、A/B 分桶、buddy 种子。跟对话历史、API key、本地配置完全无关。 换掉不影响任何东西。
如何重置
编辑 ~/.claude.json,删掉两个字段:
// 删掉这两段
"userID": "ab54093b...",
"companion": {
"name": "Pricklebait",
"personality": "...",
"hatchedAt": 1775006380441
}
重启 Claude Code,会自动生成新 userID,再 /buddy 就能领到新宠物。
如何定向刷到指定物种?(重点来了)
踩坑:Node.js ≠ Bun
源码里 hashString 有两条路径:
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 打包的,所以实际用的是 Bun.hash()。用 Node.js 跑出来的 userID 写进配置,得到的宠物完全不对! 我第一次就踩了这个坑——脚本算出来是 legendary duck,实际领到了一只 blob。
正确做法:用 Bun 跑
#!/usr/bin/env bun
// buddy-reroll-bun.js
const crypto = require('crypto')
const SALT = 'friend-2026-401'
const SPECIES = ['duck','goose','blob','cat','dragon','octopus','owl',
'penguin','turtle','snail','ghost','axolotl','capybara','cactus',
'robot','rabbit','mushroom','chonk']
const RARITIES = ['common','uncommon','rare','epic','legendary']
const RARITY_WEIGHTS = { common:60, uncommon:25, rare:10, epic:4, legendary:1 }
const RARITY_RANK = { common:0, uncommon:1, rare:2, epic:3, legendary:4 }
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
}
}
function hashString(s) {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}
function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)] }
function rollRarity(rng) {
let roll = rng() * 100
for (const r of RARITIES) { roll -= RARITY_WEIGHTS[r]; if (roll < 0) return r }
return 'common'
}
const TARGET = process.argv[2] || 'duck'
const MAX = parseInt(process.argv[3]) || 500000
let best = { rarity: 'common', uid: '' }
for (let i = 0; i < MAX; i++) {
const uid = crypto.randomBytes(32).toString('hex')
const rng = mulberry32(hashString(uid + SALT))
const rarity = rollRarity(rng)
const species = pick(rng, SPECIES)
if (species === TARGET && RARITY_RANK[rarity] > RARITY_RANK[best.rarity]) {
best = { rarity, uid }
console.log(`found: ${rarity} ${species} -> ${uid}`)
if (rarity === 'legendary') break
}
}
console.log(`\nBest: ${best.rarity} ${TARGET} -> ${best.uid}`)
运行:
bun buddy-reroll-bun.js duck 500000
输出:
found: uncommon duck -> 160bd890...
found: rare duck -> a1cc774a...
found: epic duck -> 883062be...
found: legendary duck -> 3e75bebd7bfcdf36b2234650415ce51a64d37bcdb8f7db0c5f979cbfe5f3bc66
50 万次迭代基本稳定能出 legendary,几秒就跑完。
写入配置
把刷到的 userID 写进 ~/.claude.json(确保 companion 字段已删除),重启后 /buddy 领取即可。
以上分析基于 Claude Code 2.1.89 Native
其它版本如果改了 SALT 或算法,脚本需要对应调整,交给 Claude修改即可。
看看我的传奇鸭子!
image439×586 8.9 KB
网友解答:image1024×784 66 KB
强啊佬,附一个宠物合集
--【壹】--:
太强了佬
--【贰】--:
666 这个好看!!!
--【叁】--:
66666,这干货,牛逼!
--【肆】--:
[!success]
好耶
image778×981 121 KB
--【伍】--:
刚试了一下没效果呢
--【陆】--:
太牛了佬,再见了我的普通蜗牛,我要跟传说卡皮巴拉当朋友了
--【柒】--:
图片678×650 12.5 KB
传说小鬼
--【捌】--:
厉害了 !
--【玖】--:
太强了大佬
--【拾】--:
哇,还有刷初始号
--【拾壹】--:
image270×479 15.2 KB
自己调
--【拾贰】--:
登录过的账号也可以吗?
--【拾叁】--:
怎么没有牛和马
哦,他在电脑前
--【拾肆】--:
太强了,大佬
--【拾伍】--:
image1024×784 66 KB
强啊佬,附一个宠物合集
--【拾陆】--:
太厉害了佬友
--【拾柒】--:
佬友, 好像不太行呢,填写的bun的userID 然后重新执行不是鸭子呢。版本是 2.1.89 (Claude Code)
--【拾捌】--:
厉害了~
--【拾玖】--:
666 太强了 还能这么搞
Claude Code 最近更新了一个 /buddy 功能,可以领养一只专属小宠物。
领了之后给我一直仙人掌… 什么鬼
想换一只但是官方没给重置入口。那就只能自己动手了。
省流版
根据你 Claude Code 的安装方式选择执行命令, npm 安装用 node, native 安装用 bun
node buddy-reroll.js [options]
bun buddy-reroll.js [options]
buddy-reroll.js
#!/usr/bin/env node
// buddy-reroll.js
// Buddy reroll script — supports both Node.js and Bun
// Bun.hash results match actual Claude Code; Node.js (FNV-1a) results do NOT.
//
// Usage:
// node buddy-reroll.js [options] # runs with FNV-1a (Node mode)
// bun buddy-reroll.js [options] # runs with Bun.hash (Bun mode)
//
// Options:
// --species <name> Target species (duck, cat, dragon, ...)
// --rarity <name> Minimum rarity (common, uncommon, rare, epic, legendary)
// --eye <char> Target eye style (· ✦ × ◉ @ °)
// --hat <name> Target hat (none, crown, tophat, propeller, halo, wizard, beanie, tinyduck)
// --shiny Require shiny
// --min-stats [value] Require ALL stats >= value (default: 90)
// --max <number> Max iterations (default: 50000000)
// --count <number> Number of results to find (default: 3)
// --check <uid> Check what buddy a specific userID produces
//
// Examples:
// bun buddy-reroll.js --species duck --rarity legendary --shiny
// bun buddy-reroll.js --species dragon --min-stats 80
// bun buddy-reroll.js --check f17c2742a00b2345c22fddc830959a6847ceb561fa06adb26b74b1a91ac657bc
const crypto = require('crypto')
// --- Constants (must match Claude Code source) ---
const SALT = 'friend-2026-401'
const SPECIES = ['duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin', 'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus', 'robot', 'rabbit', 'mushroom', 'chonk']
const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary']
const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }
const RARITY_RANK = { common: 0, uncommon: 1, rare: 2, epic: 3, legendary: 4 }
const EYES = ['·', '✦', '×', '◉', '@', '°']
const HATS = ['none', 'crown', 'tophat', 'propeller', 'halo', 'wizard', 'beanie', 'tinyduck']
const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK']
const RARITY_FLOOR = { common: 5, uncommon: 15, rare: 25, epic: 35, legendary: 50 }
// --- Hash functions ---
function hashFNV1a(s) {
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function hashBun(s) {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}
// --- PRNG (Mulberry32 — same as Claude Code) ---
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
}
}
function pick(rng, arr) {
return arr[Math.floor(rng() * arr.length)]
}
function rollRarity(rng) {
let roll = rng() * 100
for (const r of RARITIES) {
roll -= RARITY_WEIGHTS[r]
if (roll < 0) return r
}
return 'common'
}
function rollStats(rng, rarity) {
const floor = RARITY_FLOOR[rarity]
const peak = pick(rng, STAT_NAMES)
let dump = pick(rng, STAT_NAMES)
while (dump === peak) dump = pick(rng, STAT_NAMES)
const stats = {}
for (const name of STAT_NAMES) {
if (name === peak) stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30))
else if (name === dump) stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15))
else stats[name] = floor + Math.floor(rng() * 40)
}
return stats
}
function createRoller(hashFn) {
return function rollFull(uid) {
const rng = mulberry32(hashFn(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
const stats = rollStats(rng, rarity)
return { rarity, species, eye, hat, shiny, stats }
}
}
// --- Parse CLI args ---
function parseArgs() {
const args = process.argv.slice(2)
const opts = { max: 50_000_000, count: 3 }
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--species': opts.species = args[++i]; break
case '--rarity': opts.rarity = args[++i]; break
case '--eye': opts.eye = args[++i]; break
case '--hat': opts.hat = args[++i]; break
case '--shiny': opts.shiny = true; break
case '--min-stats': {
const next = args[i + 1]
opts.minStatsVal = (next && !next.startsWith('--')) ? parseInt(args[++i]) : 90
break
}
case '--max': opts.max = parseInt(args[++i]); break
case '--count': opts.count = parseInt(args[++i]); break
case '--check': opts.check = args[++i]; break
case '--help': case '-h':
console.log(`Usage: node/bun buddy-reroll.js [options]
Options:
--species <name> ${SPECIES.join(', ')}
--rarity <name> ${RARITIES.join(', ')}
--eye <char> ${EYES.join(' ')}
--hat <name> ${HATS.join(', ')}
--shiny Require shiny
--min-stats [value] Require ALL stats >= value (default: 90)
--max <number> Max iterations (default: 50000000)
--count <number> Results to find (default: 3)
--check <uid> Check what buddy a specific userID produces`)
process.exit(0)
}
}
// Validate
if (opts.species && !SPECIES.includes(opts.species)) {
console.error(`Unknown species: ${opts.species}\nAvailable: ${SPECIES.join(', ')}`)
process.exit(1)
}
if (opts.rarity && !RARITIES.includes(opts.rarity)) {
console.error(`Unknown rarity: ${opts.rarity}\nAvailable: ${RARITIES.join(', ')}`)
process.exit(1)
}
if (opts.eye && !EYES.includes(opts.eye)) {
console.error(`Unknown eye: ${opts.eye}\nAvailable: ${EYES.join(' ')}`)
process.exit(1)
}
if (opts.hat && !HATS.includes(opts.hat)) {
console.error(`Unknown hat: ${opts.hat}\nAvailable: ${HATS.join(', ')}`)
process.exit(1)
}
return opts
}
// --- Main ---
const opts = parseArgs()
const isBun = typeof Bun !== 'undefined'
const hashFn = isBun ? hashBun : hashFNV1a
const rollFull = createRoller(hashFn)
const runtimeLabel = isBun ? 'bun (Bun.hash)' : 'node (FNV-1a)'
const RARITY_STARS = { common: '★', uncommon: '★★', rare: '★★★', epic: '★★★★', legendary: '★★★★★' }
// --- Check mode ---
if (opts.check) {
console.log(`Runtime: ${runtimeLabel}`)
console.log(`Checking userID: ${opts.check}\n`)
const r = rollFull(opts.check)
console.log(` Species : ${r.species}`)
console.log(` Rarity : ${r.rarity} ${RARITY_STARS[r.rarity]}`)
console.log(` Eye : ${r.eye}`)
console.log(` Hat : ${r.hat}`)
console.log(` Shiny : ${r.shiny}`)
console.log(` Stats :`)
for (const name of STAT_NAMES) {
const val = r.stats[name]
const bar = '█'.repeat(Math.floor(val / 5)) + '░'.repeat(20 - Math.floor(val / 5))
console.log(` ${name.padEnd(10)} ${bar} ${val}`)
}
process.exit(0)
}
const filters = []
if (opts.species) filters.push(`species=${opts.species}`)
if (opts.rarity) filters.push(`rarity>=${opts.rarity}`)
if (opts.eye) filters.push(`eye=${opts.eye}`)
if (opts.hat) filters.push(`hat=${opts.hat}`)
if (opts.shiny) filters.push('shiny=true')
if (opts.minStatsVal) filters.push(`all stats>=${opts.minStatsVal}`)
console.log(`Runtime: ${runtimeLabel}${isBun ? '' : ' (results will NOT match Claude Code)'}`)
console.log(`Searching: ${filters.join(', ') || 'any'} (max ${opts.max.toLocaleString()}, find ${opts.count})`)
console.log('')
const minRarityRank = opts.rarity ? RARITY_RANK[opts.rarity] : 0
let found = 0
const startTime = Date.now()
for (let i = 0; i < opts.max; i++) {
const uid = crypto.randomBytes(32).toString('hex')
const r = rollFull(uid)
if (opts.rarity && RARITY_RANK[r.rarity] < minRarityRank) continue
if (opts.species && r.species !== opts.species) continue
if (opts.eye && r.eye !== opts.eye) continue
if (opts.hat && r.hat !== opts.hat) continue
if (opts.shiny && !r.shiny) continue
if (opts.minStatsVal && !Object.values(r.stats).every(v => v >= opts.minStatsVal)) continue
found++
const statsStr = STAT_NAMES.map(n => `${n}:${r.stats[n]}`).join(' ')
console.log(`#${found} [${r.rarity}] ${r.species} eye=${r.eye} hat=${r.hat} shiny=${r.shiny}`)
console.log(` stats: ${statsStr}`)
console.log(` uid: ${uid}`)
console.log('')
if (found >= opts.count) break
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
if (found === 0) {
console.log(`No match found in ${opts.max.toLocaleString()} iterations (${elapsed}s)`)
} else {
console.log(`Found ${found} match(es) in ${elapsed}s`)
}
OAuth 用户看这里:
NaynIruR:oauth 用这个办法 Claude Oauth登录刷 /buddy 宠物的方法找到了
分析细节
宠物系统原理
让 AI 分析了昨晚泄露的源码(src/buddy/),整个系统分两层:
Bones(骨架) —— 外观、物种、稀有度、属性,全部由 hash(userID + SALT) 确定性生成,不存储在配置中,每次读取时实时计算。
Soul(灵魂) —— 名字和性格,由模型生成,存储在 ~/.claude.json 的 companion 字段中。
// src/buddy/companion.ts
const SALT = 'friend-2026-401'
export function companionUserId(): string {
const config = getGlobalConfig()
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
}
也就是说:同一个 userID,永远生成同一只宠物。
一共有多少种?
18 个物种:
| 物种 | 物种 | 物种 |
|---|---|---|
| duck | goose | blob |
| cat | dragon | octopus |
| owl | penguin | turtle |
| snail | ghost | axolotl |
| capybara | cactus | robot |
| rabbit | mushroom | chonk |
5 个稀有度:
| 稀有度 | 权重 | 概率 |
|---|---|---|
| ★ common | 60 | 60% |
| ★★ uncommon | 25 | 25% |
| ★★★ rare | 10 | 10% |
| ★★★★ epic | 4 | 4% |
| ★★★★★ legendary | 1 | 1% |
还有 6 种眼睛(· ✦ × ◉ @ °)、8 种帽子、5 项属性(DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK),以及 1% 概率的 shiny 闪光版。
userID 是什么?
没登录 OAuth 账号的情况下,userID 是首次启动时随机生成的 32 字节 hex 字符串:
// src/utils/config.ts
const userID = randomBytes(32).toString('hex')
saveGlobalConfig(current => ({ ...current, userID }))
它只用于:遥测分析(匿名 device_id)、A/B 分桶、buddy 种子。跟对话历史、API key、本地配置完全无关。 换掉不影响任何东西。
如何重置
编辑 ~/.claude.json,删掉两个字段:
// 删掉这两段
"userID": "ab54093b...",
"companion": {
"name": "Pricklebait",
"personality": "...",
"hatchedAt": 1775006380441
}
重启 Claude Code,会自动生成新 userID,再 /buddy 就能领到新宠物。
如何定向刷到指定物种?(重点来了)
踩坑:Node.js ≠ Bun
源码里 hashString 有两条路径:
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 打包的,所以实际用的是 Bun.hash()。用 Node.js 跑出来的 userID 写进配置,得到的宠物完全不对! 我第一次就踩了这个坑——脚本算出来是 legendary duck,实际领到了一只 blob。
正确做法:用 Bun 跑
#!/usr/bin/env bun
// buddy-reroll-bun.js
const crypto = require('crypto')
const SALT = 'friend-2026-401'
const SPECIES = ['duck','goose','blob','cat','dragon','octopus','owl',
'penguin','turtle','snail','ghost','axolotl','capybara','cactus',
'robot','rabbit','mushroom','chonk']
const RARITIES = ['common','uncommon','rare','epic','legendary']
const RARITY_WEIGHTS = { common:60, uncommon:25, rare:10, epic:4, legendary:1 }
const RARITY_RANK = { common:0, uncommon:1, rare:2, epic:3, legendary:4 }
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
}
}
function hashString(s) {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}
function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)] }
function rollRarity(rng) {
let roll = rng() * 100
for (const r of RARITIES) { roll -= RARITY_WEIGHTS[r]; if (roll < 0) return r }
return 'common'
}
const TARGET = process.argv[2] || 'duck'
const MAX = parseInt(process.argv[3]) || 500000
let best = { rarity: 'common', uid: '' }
for (let i = 0; i < MAX; i++) {
const uid = crypto.randomBytes(32).toString('hex')
const rng = mulberry32(hashString(uid + SALT))
const rarity = rollRarity(rng)
const species = pick(rng, SPECIES)
if (species === TARGET && RARITY_RANK[rarity] > RARITY_RANK[best.rarity]) {
best = { rarity, uid }
console.log(`found: ${rarity} ${species} -> ${uid}`)
if (rarity === 'legendary') break
}
}
console.log(`\nBest: ${best.rarity} ${TARGET} -> ${best.uid}`)
运行:
bun buddy-reroll-bun.js duck 500000
输出:
found: uncommon duck -> 160bd890...
found: rare duck -> a1cc774a...
found: epic duck -> 883062be...
found: legendary duck -> 3e75bebd7bfcdf36b2234650415ce51a64d37bcdb8f7db0c5f979cbfe5f3bc66
50 万次迭代基本稳定能出 legendary,几秒就跑完。
写入配置
把刷到的 userID 写进 ~/.claude.json(确保 companion 字段已删除),重启后 /buddy 领取即可。
以上分析基于 Claude Code 2.1.89 Native
其它版本如果改了 SALT 或算法,脚本需要对应调整,交给 Claude修改即可。
看看我的传奇鸭子!
image439×586 8.9 KB
网友解答:image1024×784 66 KB
强啊佬,附一个宠物合集
--【壹】--:
太强了佬
--【贰】--:
666 这个好看!!!
--【叁】--:
66666,这干货,牛逼!
--【肆】--:
[!success]
好耶
image778×981 121 KB
--【伍】--:
刚试了一下没效果呢
--【陆】--:
太牛了佬,再见了我的普通蜗牛,我要跟传说卡皮巴拉当朋友了
--【柒】--:
图片678×650 12.5 KB
传说小鬼
--【捌】--:
厉害了 !
--【玖】--:
太强了大佬
--【拾】--:
哇,还有刷初始号
--【拾壹】--:
image270×479 15.2 KB
自己调
--【拾贰】--:
登录过的账号也可以吗?
--【拾叁】--:
怎么没有牛和马
哦,他在电脑前
--【拾肆】--:
太强了,大佬
--【拾伍】--:
image1024×784 66 KB
强啊佬,附一个宠物合集
--【拾陆】--:
太厉害了佬友
--【拾柒】--:
佬友, 好像不太行呢,填写的bun的userID 然后重新执行不是鸭子呢。版本是 2.1.89 (Claude Code)
--【拾捌】--:
厉害了~
--【拾玖】--:
666 太强了 还能这么搞

