A cartwright builds carts.
A Claude Code skill for building the SD card of a modern handheld retro console, with an agent doing the work.
A TrimUI Brick Pro is a Game Boy that also runs a Mega Drive, a PlayStation, a PSP and an arcade cabinet, off one memory card. Getting one into that state is two or three days of archaeology. Which BIOS filename does this core actually want. Why do all 375 arcade games refuse to start. Where does box art go, and what do you do about the games the scraper cannot find. How do you prove that a 240 GB copy arrived intact rather than finding out in a month.
That archaeology is done. This skill is the result, written for an agent to execute rather than for you to read.
Something is broken. Say what you see and let Claude work backwards.
my arcade games all say "missing files for THIS VERSION of FBNeo"
half my Saturn games have no box art
every PlayStation game shows up twice
The skill opens on a symptom table that routes each of these to a first move, usually a diagnostic rather than a fix. There is a reason for that: in this hobby the obvious repair is normally wrong, and the device already wrote the real reason into a log.
You have an empty card and a drive full of ROMs. Then it is the whole pipeline, twelve phases, in order:
build an SD card for my Brick Pro from this drive
Claude finds the platform tag, interrogates each emulator core, pulls the BIOS files those cores name (a few hundred KB out of multi-GB packs), sorts the ROMs, rebuilds the arcade sets against the core's own CRCs, formats the card, copies, then verifies every file against a manifest before saying it is done. The per-system work is independent, so scraping, rebuilding and verifying fan out across subagents cleanly if you want the whole thing at once.
Done end to end once, on real hardware, that produced:
Systems 19
Games 2,718
Box art 2,716 of 2,716
Collections 71 (10 by genre, 61 by franchise), 2,149 entries, 0 broken
BIOS files 391 (149 MB)
Arcade 375 romsets, none running -> 541 working
Corrupted 0
Every path, command and number in this repo came off that hardware rather than out of a wiki.
git clone https://github.com/chiotas/cartwright ~/.claude/skills/cartwrightClaude loads it when it recognises the domain. Nothing else to configure.
Never trust a database, a filename, a wiki, or this skill. Interrogate the binary.
Half the work in the original build went into discovering that documentation, DAT files and ROM sets all disagreed with what the emulator on the device wanted. Ask the core directly and the problem dissolves. Assume, and you lose an hour.
corespec.py ext picodrive_libretro.so
# bin|gen|smd|md|32x|cue|iso|chd|sms|gg|sg|sc|m3u|68k|sgd|pcoThe flagship case, in full. Every command here was run against a real FBNeo core and a real DAT.
B=https://raw.githubusercontent.com/libretro/FBNeo/master/dats
curl -sLo arcade.dat "$B/FinalBurn%20Neo%20(ClrMame%20Pro%20XML,%20Arcade%20only).dat"
# the core that will actually run, straight off the device
ssh root@$IP 'cat /mnt/SDCARD/.system/tg5040/cores/fbneo_libretro.so' > fbneo.so
# does this dat describe that core? do not skip this
fbneo.py validate --dat arcade.dat --core fbneo.so
# 300/300 sampled rom names present in fbneo.so (100%)
# MATCH — this dat describes this core
fbneo.py rebuild --dat arcade.dat --roms "Roms/Arcade (FBN)" # dry run
fbneo.py rebuild --dat arcade.dat --roms "Roms/Arcade (FBN)" --apply
cat "Roms/.rebuilt/merge.sh" # it deletes files, so read it
sh "Roms/.rebuilt/merge.sh"
fbneo.py maptxt --dat arcade.dat --roms "Roms/Arcade (FBN)"Twelve phases, each with a reference file the agent reads on demand:
- Recon: platform tag, folder tags, core extensions, core BIOS names
- Installing the OS into staging
- BIOS, validated against the cores rather than dumped from a pack
- Sorting ROMs, unzipping, multi-disc
.m3uhandling - The arcade problem: rebuilding MAME sets into FBNeo sets by CRC
- Formatting, copying, and proving the copy is intact
- SSH, paks, reading the device's own logs
- Box art, including the games the scraper misses
- Cross-system collections by genre and franchise
- Theming, and rebuilding what a theme leaves out
- Backups over rclone to a NAS
- Round trip and re-verification
gotchas.md collects the rest. It is short and boring and it will save you an
afternoon.
Every script runs standalone, needs nothing but Python 3, and has a
--selftest.
ziprange.py reads a remote zip over HTTP range requests without
downloading it. It creates a sparse local file, writes only the tail into it,
and hands that to zipfile, which walks the full central directory and never
touches the hole. 391 BIOS files out of multi-GB packs. 496 arcade romsets out
of a 9.63 GB archive, for 3.5 GB of traffic. Split .zip.001 archives work
too.
corespec.py interrogates a libretro core: accepted extensions (taking the
longest pipe-separated string, which is the trap), expected BIOS filenames,
whether the core embeds its ROMs, and whether an FBNeo core uses pre- or
post-rename romset names.
fbneo.py is the reason this repo exists. It parses an FBNeo DAT properly,
skipping merge= entries and CRC-less nodump entries, both of which break
naive parsers. validate then proves the DAT describes the core you are about
to run, by sampling ROM filenames and probing the binary for them, so you never
have to reason about version tags. After that it indexes your zips by CRC,
works out which romset each one contains, and rebuilds it under the filenames
the core wants, renaming the zip when the content turns out to be a different
set. 1942 was really 1942a. gng was really gngb. It writes to a
separate folder, never touches your originals, and emits a merge.sh you read
before running, because the rebuilt folder holds only what changed and swapping
it in wholesale would delete every set that already worked. It also generates
map.txt with real titles and the arcade BIOS hidden from the menu.
manifest.py fingerprints a source tree by size and CRC32, then proves the
copy is intact. A 240 GB card that drops one file without saying so is the
worst outcome in this hobby: you find out weeks later, when a game will not
boot. It also detects sparse files, which report full size in ls and pass
unzip -l while containing nothing.
libretro.py pulls box art from libretro-thumbnails and genre and
franchise metadata from libretro-database. Both are public, no account, no rate
limit worth worrying about. It matches by region, so a USA cart does not end up
wearing a Japanese box.
Verified on a TrimUI Brick Pro (tg5040) running NextUI.
The folder conventions, map.txt, the .media artwork layout, the
Collections/ format and the pak system are shared across the MinUI family:
TrimUI Brick, Smart Pro, and MinUI on the RG35XX line. Expect those to hold and
the platform tag to differ.
On anything else (muOS, Knulli, ArkOS, Batocera, ES-DE, desktop RetroArch) the
method still holds and all five tools still run, because CRCs, libretro cores,
exFAT and HTTP do not care which frontend you use. Only the destination paths
change. references/porting.md lists which parts transfer, and for each
device-specific convention it gives you a way to derive it from the hardware in
front of you. Read it before adapting anything, or you will invent paths that
fail silently.
Say what you want and Claude goes looking: it picks the right convention first (FBNeo for arcade, No-Intro for cartridges, Redump for discs), queries the archive.org API, checks each candidate for access restrictions and actual contents, and comes back with options and sizes for you to choose from. Then it pulls only the entries you need and verifies them before building on them.
What it will not do is carry a list of ROM sites. Those rot within months, and a live search beats a stale list every time. Your own dumps and your own drives work the same way.
One SD card, built from scratch over a couple of days and documented while it happened. The original field guide is still worth reading if you want the narrative rather than the tooling. This repo is that guide turned into something an agent can execute.
The Dreamcast section exists because a wrong answer cost someone 38 GB of deleted ROMs.
MIT.