| description | Reroll your Claude Code /buddy companion to get a specific species, rarity, shiny, eye, hat, and stats. Triggers on "reroll buddy", "change buddy", "customize buddy", "shiny buddy", "legendary buddy", "buddy species", "buddy ghost/dragon/cat/etc", or when user wants a specific companion build. |
|---|
Customize your Claude Code /buddy companion by brute-forcing the internal salt to match desired traits.
The buddy system generates traits deterministically from hash(userId + salt). This skill:
- Reads the user's account ID from
~/.claude.json - Searches for a 15-character salt that produces the desired traits
- Patches the Claude binary (same-length replacement, then re-signs)
- Clears the old companion so
/buddyre-hatches
- Bun runtime — needed for
Bun.hash(wyhash) to match the binary's hash function
which bun || npm install -g bunPresent the options:
Species (pick one):
duck goose blob cat dragon octopus owl penguin turtle snail ghost axolotl capybara cactus robot rabbit mushroom chonk
Rarity: common uncommon rare epic legendary
- Higher rarity = higher base stats
commonforces hat tonone
Eye: · ✦ × ◉ @ °
Hat: none crown tophat propeller halo wizard beanie tinyduck
Shiny: true / false (only 1% of salts produce shiny — searches take longer)
Primary stat (highest, guaranteed 100 at legendary): DEBUGGING PATIENCE CHAOS WISDOM SNARK
The user does NOT need to specify all fields. Unspecified fields are wildcards.
Write a Bun script to /tmp/buddy-search.mjs and run it. The script brute-forces 15-character salts.
cat > /tmp/buddy-search.mjs << 'SCRIPT'
const species = ['duck','goose','blob','cat','dragon','octopus','owl','penguin','turtle','snail','ghost','axolotl','capybara','cactus','robot','rabbit','mushroom','chonk'];
const rarityWeights = {common:60, uncommon:25, rare:10, epic:4, legendary:1};
const rarityOrder = ['common','uncommon','rare','epic','legendary'];
const eyes = ['·','✦','×','◉','@','°'];
const hats = ['none','crown','tophat','propeller','halo','wizard','beanie','tinyduck'];
const statNames = ['DEBUGGING','PATIENCE','CHAOS','WISDOM','SNARK'];
const baseStats = {common:5, uncommon:15, rare:25, epic:35, legendary:50};
function oN4(seed) {
let s = seed >>> 0;
return () => {
s |= 0; s = s + 1831565813 | 0;
let t = Math.imul(s ^ s >>> 15, 1 | s);
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 generate(userId, salt) {
const seed = Number(BigInt(Bun.hash(userId + salt)) & 0xffffffffn);
const rng = oN4(seed);
let total = 100, roll = rng() * total, rarity = 'common';
for (const r of rarityOrder) { roll -= rarityWeights[r]; if (roll < 0) { rarity = r; break; } }
const sp = pick(rng, species);
const eye = pick(rng, eyes);
const hat = rarity === 'common' ? 'none' : pick(rng, hats);
const shiny = rng() < 0.01;
const base = baseStats[rarity];
let primary = pick(rng, statNames), dump = pick(rng, statNames);
while (dump === primary) dump = pick(rng, statNames);
const stats = {};
for (const s of statNames) {
if (s === primary) stats[s] = Math.min(100, base + 50 + Math.floor(rng() * 30));
else if (s === dump) stats[s] = Math.max(1, base - 10 + Math.floor(rng() * 15));
else stats[s] = base + Math.floor(rng() * 40);
}
return { rarity, species: sp, eye, hat, shiny, primary, dump, stats };
}
// --- Config: set by the skill before running ---
const userId = process.env.BUDDY_USER_ID;
const want = JSON.parse(process.env.BUDDY_WANT); // { species?, rarity?, eye?, hat?, shiny?, primary? }
const maxAttempts = parseInt(process.env.BUDDY_MAX || '500000000');
const resultCount = parseInt(process.env.BUDDY_RESULTS || '5');
let found = [];
for (let i = 0; i < maxAttempts && found.length < resultCount; i++) {
const salt = String(i).padStart(15, '0');
const r = generate(userId, salt);
if (want.species && r.species !== want.species) continue;
if (want.rarity && r.rarity !== want.rarity) continue;
if (want.eye && r.eye !== want.eye) continue;
if (want.hat && r.hat !== want.hat) continue;
if (want.shiny !== undefined && r.shiny !== want.shiny) continue;
if (want.primary && r.primary !== want.primary) continue;
found.push({ salt, ...r });
console.error(`[${found.length}/${resultCount}] ${salt} → ${r.rarity} ${r.shiny ? 'SHINY ' : ''}${r.species} ${r.eye} ${r.hat} | ${r.primary}=${r.stats[r.primary]}`);
}
console.log(JSON.stringify(found));
SCRIPT
BUDDY_USER_ID="<userId>" \
BUDDY_WANT='{"species":"ghost","rarity":"legendary","shiny":true,"primary":"DEBUGGING"}' \
bun /tmp/buddy-search.mjsReading the user ID:
// In ~/.claude.json:
// oauthAccount.accountUuid ?? userID ?? "anon"python3 -c "
import json
with open('$HOME/.claude.json') as f:
d = json.load(f)
uid = d.get('oauthAccount',{}).get('accountUuid', d.get('userID','anon'))
print(uid)
"Timeout guidance:
- Without shiny: seconds
- With shiny: seconds to minutes
- With shiny + legendary + specific species + specific primary: minutes (1 in ~900,000 odds)
- Set
BUDDY_MAXhigher andtimeoutaccordingly
Present results as a table:
| Salt | Rarity | Species | Eye | Hat | Shiny | Primary Stat | Stats |
|---|
The original salt in the binary is 15 ASCII characters. Replace all occurrences with the chosen salt (also 15 chars), then re-sign.
# Backup
cp "$(which claude)" "$(which claude).bak"
# Patch
python3 -c "
import sys
salt_old = open('$(which claude)', 'rb').read()
# Find current salt (may be original or previously patched)
# The original is 'friend-2026-401' but could be any 15-char string from a prior reroll
old = b'THE_CURRENT_SALT' # <-- replace with actual current salt
new = b'THE_NEW_SALT_HERE' # <-- replace with chosen salt
assert len(old) == len(new) == 15
data = salt_old.replace(old, new)
open('$(which claude)', 'wb').write(data)
print(f'Patched {salt_old.count(old)} occurrences')
"
# Re-sign (ad-hoc, required on macOS)
codesign --force --sign - "$(which claude)"Finding the current salt (if unknown):
python3 -c "
with open('$(which claude)', 'rb') as f:
data = f.read()
# The salt is at offset ~0x050b41a0 in the bytecode string pool,
# preceded by length prefix 0f000000 (15) and followed by 00 null.
# Search for the pattern: 4 bytes LE length=15, then 15 printable ASCII, then 00
import re
for m in re.finditer(b'\\x0f\\x00\\x00\\x00([\\x20-\\x7e]{15})\\x00', data):
candidate = m.group(1).decode()
# Verify it's near known buddy code by checking nearby strings
ctx = data[m.start()-64:m.end()+64]
if b'companion' in ctx or b'buddy' in ctx or m.start() > 0x05000000:
print(f'Likely salt at offset {m.start():#010x}: {candidate}')
"python3 -c "
import json
with open('$HOME/.claude.json', 'r') as f:
d = json.load(f)
d.pop('companion', None)
with open('$HOME/.claude.json', 'w') as f:
json.dump(d, f, indent=2)
"Tell the user: Start a new Claude Code session and run /buddy to hatch your new companion.
After the user comes back, check the system reminder for the companion name and species. If it doesn't match expectations, the bytecode may have its own copy — see Troubleshooting.
Bun single-file executables embed both source text and compiled bytecode. The salt exists in 3 locations — all 3 must be patched (Python str.replace handles this). If the result still doesn't match:
- Verify the patch is in the binary:
python3 -c "print(open('$(which claude)','rb').read().count(b'NEW_SALT'))" - Verify no old salt remains:
python3 -c "print(open('$(which claude)','rb').read().count(b'OLD_SALT'))" - Re-sign:
codesign --force --sign - "$(which claude)" - Ensure companion was cleared from
~/.claude.json - Start a completely new session (not just a new conversation in the same process)
Claude Code may auto-update, replacing the binary. Re-run this skill to patch again. The backup at $(which claude).bak contains the unpatched version.
cp "$(which claude).bak" "$(which claude)"
codesign --force --sign - "$(which claude)"Each companion has one primary stat (highest) and one dump stat (lowest). You can choose which stat is primary, but exact values are constrained by rarity:
| Rarity | Primary | Normal (x3) | Dump | Formula |
|---|---|---|---|---|
| Common | 55-84 | 5-44 | 1-19 | base=5 |
| Uncommon | 65-94 | 15-54 | 5-29 | base=15 |
| Rare | 75-100 | 25-64 | 15-39 | base=25 |
| Epic | 85-100 | 35-74 | 25-49 | base=35 |
| Legendary | 100 | 50-89 | 40-54 | base=50 |
Primary: min(100, base + 50 + rand(0-29))
Normal: base + rand(0-39)
Dump: max(1, base - 10 + rand(0-14))
Legendary primary is always 100, but you cannot get multiple stats at 100 or all stats maxed — the algorithm enforces exactly 1 primary and 1 dump per companion. The theoretical best for Legendary is: 100 / 89 / 89 / 89 / 54.
If the user asks for "all 100" stats, explain this limit. To bypass it requires editing the source code directly (only possible with the npm-installed JS text version of Claude Code, not the compiled binary).
You can add stat constraints to the search. Practical filters:
- Primary stat: which stat gets the max value (e.g.,
DEBUGGING) - Dump stat: which stat gets the penalty (e.g.,
CHAOS) - Min/max thresholds: e.g., "all normal stats above 80" (narrows search significantly)
Add these as filters in the BUDDY_WANT config:
{"species":"ghost","rarity":"legendary","shiny":true,"primary":"DEBUGGING","dump":"CHAOS","minNormal":80}Then add to the search script's filter logic:
if (want.dump && r.dump !== want.dump) continue;
if (want.minNormal) {
const normals = statNames.filter(s => s !== r.primary && s !== r.dump);
if (normals.some(s => r.stats[s] < want.minNormal)) continue;
}- The name and personality are LLM-generated at hatch time — you can't control those
- Stats are deterministic from the salt but the exact values depend on RNG rolls
- Shiny is rare (1%) — expect longer searches when requiring it
- Legendary is 1% — combined with shiny + specific species + specific primary, expect ~1 in 900,000 odds
- The salt must be exactly 15 characters (same length as the original
friend-2026-401) - After Claude Code updates, the binary resets — re-patch with the same salt to get the same buddy back