essayUpdated Jul 24, 202612 min read$18

The Reverse Engineer Was a Fleet of Claude Agents. The Editor Was Me.

Claude agents built an entire working subsystem for the wrong explanation. The real fix was two bytes. How I decide which AI-generated evidence deserves to survive.

cps2reverse-engineeringai-directedclaude-codemulti-agent
CPS-2 Reverse EngineeringPart 14 of 43
Browse all writing
On this page

The other posts in this series end with a note that the reverse engineering was AI-directed. Readers keep asking what that actually means, and I do not want to answer with a disclaimer. I want to show the machine -- especially the times it failed.

I did not write the disassembly or hand-trace the 68000. Claude Code agents, across several Opus and Sonnet generations, did the searching, patching, and much of the tool-building. My work was the system around them: frame the questions, fan out competing investigations, force hostile review, decide what entered the build, and refuse to publish a result that had not run.

That last rule came from a wrong turn. The workflow once built and live-verified an entire satellite-object subsystem for Shuma-Gorath's Mystic Smash. The subsystem worked; its premise did not. The authentic attack was already in the ROM, and the real fix was a two-byte nop. That is the stakes of this post: agents can produce impressive, functioning evidence for the wrong story, so authorship here means deciding what counts as true.

Evidence Strip

Goal: Explain how the entire CPS-2 body of work on this blog -- Dark Sakura, Shuma-Gorath, the stages, the crash fixes -- was actually produced: by directing fleets of Claude agents through an encrypted arcade ROM, with me as the editor who decides what counts as true.

Constraints: No source code, encrypted 68000 program ROM, and agents that will confidently report success on things that do not work. The last constraint is the one that shaped everything.

Approach: Fan out parallel hypothesis agents; send independent adversarial agents to refute what they found; accept nothing until it runs live in the emulator and the model reads the screenshot itself. Keep a memory index that carries retractions as prominently as facts.

Result: A cumulative live-gated build chain, two secret characters fighting natively, and -- the part this post is about -- four documented cases where the AI was confidently wrong and the workflow caught it, including one where an entire working subsystem was retracted in favor of a two-byte nop.

Proof: Every claim below traces to a shipped build, a screenshot gate, or a retraction stamped into the project memory. The war stories are told with their real numbers.

Artifacts: The Dark Sakura and Shuma-Gorath posts are the "what." This is the "how."

The instrument was Claude Code -- Anthropic's agentic CLI -- across several model generations and months of sessions. The agents were capable of reverse-engineering an encrypted arcade ROM, and equally capable of building on a wrong premise and reporting success. The interesting engineering is not the prompt. It is the refutation pipeline.

The Machine Has Three Stages, and the Middle One Is Hostile

Every hard problem in this project ran through the same shape:

Fan out. Multiple hypothesis agents attack the same question in parallel, isolated from each other so they can't converge on a shared mistake. One recorded workflow ran five agents and three independently converged on the same answer -- that convergence is signal. Another ran eight with zero errors; another ran four mappers into a synthesis and then two verifiers.

Refute. Then independent adversarial agents get the findings with one instruction: refute this. Not review, not confirm -- refute. A finding only counts after surviving that pass. The refuters earn their keep: one shipped design died because its premise that $01326A was the sole reader of a variable was false -- an immediate scan found 9 hits. Another correctly predicted that static signature-matching would never find the object allocator and that only a live trace would (it was right; the allocator at $3FAE fell out of tracing a live Ryu Hadouken).

Live gate. Whatever survives refutation still has to run. Agents drive FBNeo -- the custom 64MB core -- through Lua harnesses that auto-navigate the menus, commit the secret characters, execute inputs, and screenshot. The screenshots come out as gd files, get converted by a small gd2png tool the agents wrote, and then the model reads the images itself. Not the logs. The pixels.

How a hard question gets answered
Drawing the diagram…
Parallel hypotheses, then independent agents sent to refute them.

The One Rule Was Born From Being Burned

Before any of this existed, an earlier project taught me the failure mode the hard way: I was repeatedly told a fix was in, and each time live play was still broken -- garbled audio, a bot walking one direction forever. It destroyed trust and wasted sessions. Out of that came the rule that governs everything here, written verbatim into the project handoff:

A fix is not fixed until you (1) run it in the emulator, (2) read the screenshot yourself -- read the .png, do not infer from logs -- and (3) confirm the match survived: $FF4000 == 0x0800.

On top of that sit standing regression gates that every build must pass, forever: Mystic Stare's ring still fires. Ryu's Hadouken still fires (proving the rerouted object dispatch is stock-safe). Wolverine's superjump is unchanged, frame-for-frame. New capability never gets to cost old truth.

The builds themselves follow a six-step protocol, and every step is an assertion that can fail the build:

What a fix has to survive before it counts as a fix
Drawing the diagram…

The final spikes fix asserts that exactly 2 bytes changed in exactly one ROM member.

Now the war stories -- the four times this machinery earned its existence.

War Story 1: The Sprites Were Never Compressed. Our Own Tool Had Scrambled Them.

Early analysis of Shuma-Gorath's sprite data measured entropy at 7.95 bits per byte -- essentially random, the signature of compression. The plan absorbed it as a 30-50 hour codec-archaeology line item, the highest-risk item on the board.

It was false. The analysis artifact had run CPS-2 decryption over the entire first megabyte of the ROM wholesale. That recovers opcodes -- code is what the encryption protects -- but it scrambles data that was never encrypted in the first place. Same address, two truths, depending on which view you read.

One ROM address, two views
Drawing the diagram…
The reading that cost weeks, and the reading that was correct.

The 30-50 hour line item dropped to roughly zero. That is the mirage failure: the AI didn't misread the ROM, it misread its own artifact, and it would have confidently spent weeks on the consequence.

War Story 2: We Built a Whole Satellite Subsystem. The Real Fix Was Two Bytes.

Mystic Smash -- Shuma's rolling-ball attack -- was missing its spikes. An agent investigation concluded the spikes were six orbiting satellite objects that a previous patch had skipped spawning. On that model, a full satellite subsystem was built: six objects spawned, seated in formation, tracked the ball, despawned cleanly on recovery with no pool leak. The mechanism was live-verified. The objects really did all of that.

And the model was still wrong.

The spiky ball was an animation already sitting in the ROM -- group 22, sub 5, at relocated address $3F84A8 -- that the move's selector at $3FB744 never picked, because an earlier remap had left its test reading the opponent's state, making it permanently false. Worse, the smooth variant it picked instead carries attack-box-set $56, which resolves to atk $0000: no hitbox at all, and that's authentic MSHvSF data. The spiky variant carries box-set $57, a real box, atk $0045.

The detector that was testing the wrong frame
Drawing the diagram…

What made the truth findable was a positive control: before believing "this move has no hitbox," an agent proved the box pipeline healthy on a move that has one -- Shuma's Strong Punch shows atk $0014 with reach 71 for 5 frames. So "no box" was a data fact, not broken plumbing.

The final fix: two bytes. $3FB75A: 6602 becomes 4e71 -- a branch nopped out. Live-verified the only way that counts: the atk box live on 37 ball frames where there had been zero, the "FIRST ATTACK" banner on screen, score 0 to 10200, and the ring regression still passing.

The satellite build is recorded in the lineage as Discredited -- do not ship, do not extend, a labeled dead branch. But retraction is not deletion: three independently-measured facts from the dead branch were explicitly carried forward (the owner latch is $b0, not $a0; a handler ending in rts never draws -- it must end in the draw tail; copying the animation pointer does not install sprite state). An entire subsystem, retracted in favor of a nop -- and the salvage itemized.

War Story 3: I Asked Why Native Felt Worse. The Comfortable Answer Failed the Test.

Native Dark Sakura played worse than the old donor-chassis build, and the comfortable explanation was "the native port lost move completeness." Instead of accepting it, we ran an identical 29-move battery -- one emulator harness, scripted inputs, both builds.

The comfortable answer was refuted. Moves the donor has that the native lost: zero confirmed. (28 of 29 passed on both sides; the single native failure was a contaminated test window, and one move -- back-jump -- actually works only natively.) The real regressions were elsewhere: specials degrading into normals -- landing in state $28 instead of $2A/$10, a Dragon Punch with vertical displacement 0 versus the donor's 24 -- and movement speed roughly doubled on walk: walk forward 152 versus 74, walk back 111 versus 55, superjump rise 362 versus 281.

The caveat stays attached wherever this is cited: it is not a pure A/B. The donor build commits character id $12 -- Ryu's engine -- so the donor side measures Sakura's sprites on Ryu's physics.

This is the workflow pointed at my own beliefs. The question was mine; the machinery treated it exactly like an agent's hypothesis.

War Story 4: The Premise Everyone Repeated for Weeks Was Wrong, and the Truth Was Stronger

For weeks, every writeup -- mine included -- said Capcom left two unfinished secret character slots in the ROM. One skeptical question sent an agent back to the tables, and the ROM answered plainly: every per-character table has exactly 23 entries, ids $00 through $2C. There is no 24th row anywhere. Id $2E borrows Spider-Man's $0E entry through sheer indexing arithmetic; $30 was never provisioned at all. The crashes I'd been fixing exist because nothing was there.

The published posts were corrected the same day, propagated across six of them in one commit. And the corrected claim is the stronger one: these are not unfinished characters finished. They are ids the engine was never built to hold, taught to hold them.

The Memory Carries Retractions, or the Dead Models Come Back

Sessions end. Context evaporates. What persists is a curated memory index of verified facts -- and, just as deliberately, retractions. The satellite entry is not deleted; it is stamped in place: the whole model below is wrong, do not build on it. The retraction sits exactly where the next session will trip over it. Without that, a discredited model is just a well-written document waiting to convince a fresh agent.

The agents also built their own tooling mid-project, which I want on the record because it is the part that still surprises me: the mvsc-text MCP server that renders text in fonts ripped from the game ROMs (it made the YouTube thumbnails); the gd2png converter; dozens of Lua capture and probe harnesses; the PC ring buffer and wild-jump logger compiled into the emulator core itself. The verification machine was partially self-manufactured.

What Still Doesn't Work -- Including the Workflow Itself

Here is the part I'd want a reviewer to read first.

The project bounds: everything runs in an emulator on a custom 64MB FBNeo core, not stock CPS-2 hardware, and that core breaks Fightcade netplay -- offline only. The native characters still carry residual Ryu physics clamps (their walk, jump, and gravity rows are seated from MSH data now, but the engine still shows a mini "RYU" tag under their lifebars). Supers are researched, not built. Shuma has his own voice table now; Dark Sakura's wrong sound effects are still silenced, not replaced. And the honest current number: the moveset identity gate reads pass=11 FAIL=6 on the current build -- the supers, Devitalization, the stare/smash strength variants, and two command normals still collapse into other moves.

The workflow bounds: it catches wrong models, but only after they're built -- the satellite subsystem cost real effort before the refutation landed. It is also not immune to its own operational errors: a cwd-relative ROM path once meant a full test cycle ran against a stale zip; a careless grep filter once ate a build error and a stale build got tested as new. The gates caught both, which is the point -- but the machine's most reliable output is not correctness. It is caught mistakes.

What separated the useful output from the confident nonsense was never the model. It was refusing to count anything that hadn't run.

Written by Daniel Plas Rivera · 2,568 words · $18

ShareXLinkedIn