Skip to content

Instantly share code, notes, and snippets.

@ShinyChang
Created April 1, 2026 15:09
Show Gist options
  • Select an option

  • Save ShinyChang/3592848680afece4c9f3d12dabcddc8e to your computer and use it in GitHub Desktop.

Select an option

Save ShinyChang/3592848680afece4c9f3d12dabcddc8e to your computer and use it in GitHub Desktop.
Claude Code /buddy reroll skill — brute-force the internal salt to customize your companion's species, rarity, shiny, eye, hat, and stats
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.

Buddy Reroll

Customize your Claude Code /buddy companion by brute-forcing the internal salt to match desired traits.

How it works

The buddy system generates traits deterministically from hash(userId + salt). This skill:

  1. Reads the user's account ID from ~/.claude.json
  2. Searches for a 15-character salt that produces the desired traits
  3. Patches the Claude binary (same-length replacement, then re-signs)
  4. Clears the old companion so /buddy re-hatches

Prerequisites

  • Bun runtime — needed for Bun.hash (wyhash) to match the binary's hash function
which bun || npm install -g bun

Step 1: Ask what the user wants

Present 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
  • common forces hat to none

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.

Step 2: Find a matching salt

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.mjs

Reading 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_MAX higher and timeout accordingly

Step 3: Let user pick from results

Present results as a table:

Salt Rarity Species Eye Hat Shiny Primary Stat Stats

Step 4: Patch the binary

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}')
"

Step 5: Clear companion and re-hatch

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.

Step 6: Verify

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.

Troubleshooting

Patch didn't take effect

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:

  1. Verify the patch is in the binary: python3 -c "print(open('$(which claude)','rb').read().count(b'NEW_SALT'))"
  2. Verify no old salt remains: python3 -c "print(open('$(which claude)','rb').read().count(b'OLD_SALT'))"
  3. Re-sign: codesign --force --sign - "$(which claude)"
  4. Ensure companion was cleared from ~/.claude.json
  5. Start a completely new session (not just a new conversation in the same process)

Binary was auto-updated

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.

Restoring original

cp "$(which claude).bak" "$(which claude)"
codesign --force --sign - "$(which claude)"

Stat Ranges & Limits

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).

Stat search options

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;
}

Notes

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment