Claude Code buddy 宠物系统逆向分析 —— 如何重置并刷到你想要的宠物

2026-04-11 12:331阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

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.jsoncompanion 字段中。

// 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

dongdongqiang:

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.jsoncompanion 字段中。

// 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

dongdongqiang:

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 太强了 还能这么搞

标签:人工智能