Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added CLAUDE.md
Empty file.
102 changes: 102 additions & 0 deletions crack.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Real Claude Code companion cracker — uses the exact algorithm from companion.ts
// Key differences from hatch.py:
// 1. PRNG is Mulberry32, not SplitMix32
// 2. Uses Bun.hash (wyhash) natively
// 3. Cracks accountUuid (the field actually used), not userID

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 EYES = ['·','✦','×','◉','@','°']
const HATS = ['none','crown','tophat','propeller','halo','wizard','beanie','tinyduck']
const RARITIES = ['common','uncommon','rare','epic','legendary']
const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }
const STAT_NAMES = ['DEBUGGING','PATIENCE','CHAOS','WISDOM','SNARK']

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) {
const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0)
let roll = rng() * total
for (const rarity of RARITIES) {
roll -= RARITY_WEIGHTS[rarity]
if (roll < 0) return rarity
}
return 'common'
}

function rollFrom(rng) {
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
// consume stat RNG calls too (to keep state consistent, though we don't need stats for matching)
// rollStats: pick peak, pick dump (with retry), then 5 stat rolls
return { rarity, species, eye, hat, shiny }
}

// Target parsing
const target = (process.argv[2] || '').toLowerCase()
if (!target) {
console.error('Usage: bun crack.mjs "legendary shiny rabbit"')
process.exit(1)
}

let wantRarity = null, wantSpecies = null, wantShiny = target.includes('shiny')
for (const r of RARITIES) { if (target.includes(r)) { wantRarity = r; break } }
for (const s of SPECIES) { if (target.includes(s)) { wantSpecies = s; break } }
Comment on lines +62 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parse target traits as whole tokens.

target.includes(r) makes "uncommon" match "common" first, so an uncommon request is cracked as common. Tokenize the input or use word boundaries before matching rarities/species.

Proposed fix
-let wantRarity = null, wantSpecies = null, wantShiny = target.includes('shiny')
-for (const r of RARITIES) { if (target.includes(r)) { wantRarity = r; break } }
-for (const s of SPECIES) { if (target.includes(s)) { wantSpecies = s; break } }
+const tokens = new Set(target.split(/\s+/).filter(Boolean))
+let wantRarity = null, wantSpecies = null, wantShiny = tokens.has('shiny')
+for (const r of RARITIES) { if (tokens.has(r)) { wantRarity = r; break } }
+for (const s of SPECIES) { if (tokens.has(s)) { wantSpecies = s; break } }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let wantRarity = null, wantSpecies = null, wantShiny = target.includes('shiny')
for (const r of RARITIES) { if (target.includes(r)) { wantRarity = r; break } }
for (const s of SPECIES) { if (target.includes(s)) { wantSpecies = s; break } }
const tokens = new Set(target.split(/\s+/).filter(Boolean))
let wantRarity = null, wantSpecies = null, wantShiny = tokens.has('shiny')
for (const r of RARITIES) { if (tokens.has(r)) { wantRarity = r; break } }
for (const s of SPECIES) { if (tokens.has(s)) { wantSpecies = s; break } }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crack.mjs` around lines 62 - 64, The current substring matching using
target.includes(...) causes "uncommon" to match "common"; change matching in the
wantRarity/wantSpecies detection to test whole tokens instead: split/tokenize
the target string (e.g., on whitespace/punctuation) into normalized words and
check if any token equals an entry from RARITIES or SPECIES, or alternatively
use word-boundary regex (\b) when testing each r in RARITIES and s in SPECIES;
update the blocks that set wantRarity and wantSpecies to use this exact-token
check against target instead of includes to avoid partial matches.


if (!wantRarity && !wantSpecies && !wantShiny) {
console.error('Specify at least one of: ' + [...RARITIES, ...SPECIES, 'shiny'].join(', '))
process.exit(1)
}

console.error(`Cracking: ${[wantRarity, wantShiny ? 'shiny' : null, wantSpecies].filter(Boolean).join(' ')}`)

const HEX = '0123456789abcdef'
const start = performance.now()
let attempts = 0

// Generate random 36-char UUID-like strings (matching accountUuid format)
function randomUuid() {
const s = new Array(36)
for (let i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) { s[i] = '-'; continue }
s[i] = HEX[(Math.random() * 16) | 0]
}
return s.join('')
}

while (true) {
attempts++
const candidate = randomUuid()
const key = candidate + SALT
const hash = hashString(key)
const rng = mulberry32(hash)
const result = rollFrom(rng)

if (wantRarity && result.rarity !== wantRarity) continue
if (wantSpecies && result.species !== wantSpecies) continue
if (wantShiny && !result.shiny) continue

const elapsed = ((performance.now() - start) / 1000).toFixed(2)
console.log(JSON.stringify({ userid: candidate, ...result, attempts, elapsed_sec: elapsed }))
Comment on lines +99 to +100
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t publish the cracked UUID as userid.

This tool is explicitly cracking UUID/accountUuid values, but the success payload still uses the old userid key. That makes it easy for downstream consumers to keep wiring the wrong field.

Proposed fix
-  console.log(JSON.stringify({ userid: candidate, ...result, attempts, elapsed_sec: elapsed }))
+  console.log(JSON.stringify({ accountUuid: candidate, ...result, attempts, elapsed_sec: elapsed }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const elapsed = ((performance.now() - start) / 1000).toFixed(2)
console.log(JSON.stringify({ userid: candidate, ...result, attempts, elapsed_sec: elapsed }))
const elapsed = ((performance.now() - start) / 1000).toFixed(2)
console.log(JSON.stringify({ accountUuid: candidate, ...result, attempts, elapsed_sec: elapsed }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crack.mjs` around lines 99 - 100, The log currently emits the cracked value
under the wrong key `userid`; change the output to use the correct `accountUuid`
(or `uuid`) key instead by replacing the `userid: candidate` entry in the JSON
payload with `accountUuid: candidate` (keeping the rest of the object spread of
`result`, `attempts`, and `elapsed_sec` intact), so the
console.log(JSON.stringify(...)) call reflects the correct field name for
downstream consumers.

break
}
Loading