<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Hazel Granados — Blog</title>
    <link>https://www.hazeliscoding.dev/blog</link>
    <description>Notes on building software, by Hazel Granados.</description>
    <language>en-us</language>
    <lastBuildDate>Sun, 25 Oct 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.hazeliscoding.dev/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Finding the processes Claude Code leaves running on Windows</title>
      <link>https://www.hazeliscoding.dev/blog/finding-the-processes-claude-code-leaves-running-on-windows</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/finding-the-processes-claude-code-leaves-running-on-windows</guid>
      <pubDate>Sun, 25 Oct 2026 00:00:00 GMT</pubDate>
      <description>Windows never cleans up after a dead parent. How Sweepling ties each orphan back to the Claude Code session that started it, and why it asks before closing anything.</description>
      <category>claude-code</category>
      <category>windows</category>
      <category>csharp</category>
      <category>avalonia</category>
      <content:encoded><![CDATA[<p>A scan of my machine, 38 hours after boot, found a <code>next dev</code> server that had held port 3001 for 35 hours with nothing connected to it. An <code>ng serve</code> had sat idle for 11 hours. Around them were 20 bare <code>tail.exe</code>, <code>grep.exe</code> and <code>sh.exe</code> rows in Task Manager, with nothing to say which project they belonged to.</p>
<p>All of them came from my Claude Code sessions: dev servers, <code>tail -f</code> watchers on task output files, small scripts. The sessions had ended and the processes kept running. There&#39;s an upstream issue about it, <a href="https://github.com/anthropics/claude-code/issues/62659">#62659</a>, and a bot closed it as not planned.</p>
<p>So I wrote <a href="https://github.com/hazeliscoding/sweepling">Sweepling</a>, a Windows tray app that notices these leftovers and closes them once you confirm.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/finding-the-processes-claude-code-leaves-running-on-windows/review.png" alt="Sweepling's review window: three ticked leftovers, each with its folder, age, port and memory, and an Angular dev server under Session still running left unticked"></p>
<h2>Why this is a Windows problem</h2>
<p>On Linux an orphan is adopted by init or a subreaper, and a cleanup tool can end a whole process group in one call. <a href="https://github.com/theQuert/cc-reaper">cc-reaper</a> does this well on macOS and Linux.</p>
<p>Windows does neither. When a parent exits, its children keep running and keep the dead parent&#39;s PID in their own process record. Windows also reuses PIDs, so a few minutes later that number can belong to something unrelated. Task Manager&#39;s tree only nests a process under a parent that&#39;s still alive, so an orphan shows up as one more top-level <code>node.exe</code>.</p>
<p>That gives the first rule: a process&#39;s parent is gone when its PID is missing, or when that PID now belongs to a process that started after the child.</p>
<pre><code class="language-csharp">Proc? ParentOf(Proc p) =&gt;
    p.ParentPid != p.Pid
    &amp;&amp; byPid.TryGetValue(p.ParentPid, out var parent)
    &amp;&amp; parent.Started &lt;= p.Started ? parent : null;
</code></pre>
<p>You can run the same check in PowerShell without installing anything:</p>
<pre><code class="language-powershell">$all = Get-CimInstance Win32_Process
$byId = @{}; $all | ForEach-Object { $byId[$_.ProcessId] = $_ }
$all | Where-Object { $p = $byId[$_.ParentProcessId]; -not $p -or $p.CreationDate -gt $_.CreationDate } |
  Where-Object Name -in &#39;node.exe&#39;, &#39;tail.exe&#39;, &#39;grep.exe&#39;, &#39;bash.exe&#39;, &#39;sh.exe&#39; |
  Select-Object ProcessId, Name, CreationDate
</code></pre>
<p>It will also list orphans that have nothing to do with Claude Code. Telling those apart is the hard part.</p>
<h2>Tying an orphan to a session</h2>
<p>Plenty of programs start a helper and exit on purpose, so an orphan on its own isn&#39;t a leftover. Sweepling only offers a process it can tie to a Claude Code session, and it has three ways to do that, strongest first.</p>
<ol>
<li><strong>The family tree.</strong> Every few seconds Sweepling takes a snapshot of your processes and records which session each one descends from. Ancestry is only visible while the parents are alive, so it learns early and keeps what it learned after a parent exits.</li>
<li><strong><code>CLAUDE_PID</code>.</strong> Claude Code puts its own PID in its children&#39;s environment. Sweepling reads another process&#39;s environment from its PEB. That memory belongs to the other process, so every size and pointer in it is treated as untrusted and every read is bounded.</li>
<li><strong>Command-line rules.</strong> Programs run from Git Bash are the awkward case: an MSYS2 process hands Windows only a handful of environment variables, and <code>CLAUDE_PID</code> isn&#39;t one of them. For those, a small rules file matches things like a <code>grep --line-buffered</code> from Git Bash&#39;s folder, or a command line that points into Claude Code&#39;s temp folder.</li>
</ol>
<p>When Sweepling can&#39;t tell whether a session has ended, it treats the session as running. A wrong &quot;running&quot; only delays an offer. A wrong &quot;ended&quot; offers someone&#39;s live work.</p>
<p>Strays that share one dead parent and started within ten seconds of each other become a group, together with everything under them, and you close a group as one.</p>
<h2>Is it still in use?</h2>
<p>Belonging to an ended session still isn&#39;t enough. A group waits until its parent has been gone for a minute, and it&#39;s never offered when:</p>
<ul>
<li>anything in it has an established TCP connection, like a dev server a browser tab is still talking to;</li>
<li>anything in it is a windowed app. Sweepling reads the subsystem field in the exe&#39;s PE header, and an exe it can&#39;t read counts as windowed;</li>
<li>everything in it is on the protect list, which covers things like MSBuild nodes and the .NET build servers.</li>
</ul>
<p>Sweepling never looks at CPU or memory use. A busy process is often a build someone is waiting for, and &quot;kill whatever is using a lot of CPU&quot; has ended background builds in other cleanup tools.</p>
<h2>Asking first</h2>
<p>Sweepling never closes anything you haven&#39;t confirmed, and there&#39;s no auto mode, setting or flag to change that. Leftovers of ended sessions start ticked. Strays of a session that&#39;s still running get their own section and are never ticked by default.</p>
<p>Right before closing a group it checks again. Nothing in the group may have gained a connection since you looked, and each PID must still belong to the process you were shown, compared by start time, because the PID you saw thirty seconds ago could be a new process by now. It closes parents first, so a supervisor can&#39;t restart a child it just lost. Each close goes into a local log with command lines masked, since command lines carry tokens and private paths.</p>
<h2>Keeping it cheap</h2>
<p>An app that cleans up leftovers shouldn&#39;t cost more than they do. Sweepling is .NET 10 and Avalonia 12, published with NativeAOT as a single process. Avalonia&#39;s GPU renderer held about 60 MB from launch whether a window was open or not, so Sweepling renders in software. In the tray it idles at about 17 MB of private bytes, and about 23 MB once its window has been opened. Its only network call is the update check against GitHub Releases, and there&#39;s no telemetry, because it can see every command line on the machine.</p>
<h2>What a week of using it found</h2>
<p>I&#39;ve run it every day since it worked. On its busiest day it found 22 groups on my machine: 9 task output watchers, 9 of their <code>grep</code> filters, a Next.js dev server, two Angular dev servers and a shell script. I closed all of them. One was an Angular dev server I had completely forgotten about. The number I watch most is how many it offered that were still in use, and so far that&#39;s zero.</p>
<h2>Try it</h2>
<p>You need Windows 10 or 11 on x64. Download <code>sweepling-setup-win-x64.exe</code> from the <a href="https://github.com/hazeliscoding/sweepling/releases/latest">latest release</a>. It installs for your user only, needs no admin rights, is signed, and keeps itself up to date. It sits in the tray as a straw brush. When the brush shows a count, click it, untick anything you still want, and press Close. To see what it would offer without closing anything, run <code>Sweepling.exe --scan &gt; scan.json</code>.</p>
<p>For now it covers the Claude Code CLI only. On macOS or Linux, use cc-reaper, which does much more. If you already know which port is stuck, <code>npx kill-port 3001</code> is faster than any tray app.</p>
<p>The code is Apache-2.0 <a href="https://github.com/hazeliscoding/sweepling">on GitHub</a>. The most useful issue you could open describes a leftover Sweepling missed, or one it offered while you were still using it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>One repo became three: keeping a preservation port honest</title>
      <link>https://www.hazeliscoding.dev/blog/one-repo-became-three</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/one-repo-became-three</guid>
      <pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate>
      <description>Puzzle Boy&apos;s 30 unplayed stages, the switch key they needed, and why that key ended up in a different repo from the port. Part 2 of 3.</description>
      <category>puzzle-boy</category>
      <category>claude-code</category>
      <category>csharp</category>
      <category>monogame</category>
      <category>architecture</category>
      <content:encoded><![CDATA[<p>Puzzle Boy ships 50 stage files and plays 20 of them. The 30 it never plays include all seven stages with more than one Jack-o&#39;-Lantern, and six of those can&#39;t be finished at all.</p>
<p>This is part 2 of 3. <a href="https://www.hazeliscoding.dev/blog/a-ps2-minigame-in-a-unity-remaster">Part 1</a> covered reading the rules out of <em>Shin Megami Tensei III Nocturne HD Remaster</em> and rebuilding the minigame as <a href="https://github.com/hazeliscoding/puzzle-boy">puzzle-boy</a>. This one covers what I did with the unplayed stages, how that broke the port&#39;s one promise, and how splitting it into three repos fixed it.</p>
<h2>The stages nobody played</h2>
<p>Part 1 said 21 of the 50 files are played. The code does list a 21st, file 00, for &quot;levels 2 and 3&quot;, but every video and guide ends after stage 20, so I count 00 as unused.</p>
<p>None of the 30 is junk. Every start and goal is on floor, no goal is walled off, and a few are tutorial-sized, at 3 or 4 pushes. They include all seven multi-character stages and the six files damaged by the line-ending conversion.</p>
<p>The multi-character ones are where it gets strange. In the original, control passes to the next Jack only when one reaches the goal. Nothing else switches, so the first Jack has to get home alone, using only its own pushes. A search ran out of positions on six of the seven. File 05 shows why: one push can only be made from a dead-end alcove that the block itself seals off, so somebody has to wait in that alcove while another character brings the block over.</p>
<p>They look like stages designed for a switch the shipped code doesn&#39;t have. That&#39;s my guess, not something the code says.</p>
<h2>A switch key and a search</h2>
<p>So the port got a switch key, C. It passes control to the next visible character, in the same order a goal arrival does. Its label on the controls bar, &quot;Switch Character&quot;, is the game&#39;s own guide text. With it, four of the six stuck stages have solutions, and file 47 needs all four Jacks and 10 switches.</p>
<p>Then a search went through all 30 files. It made every move on the port&#39;s real <code>Board</code>, built from the real file, so no solution could lean on a rule the port doesn&#39;t have. Breadth-first search found the fewest pushes, and A* took over when the space got big. A faster second pass had its rules checked against the real <code>Board</code> on 4.7 million single steps before I trusted it, and every solution was replayed input by input on a fresh board before anything shipped.</p>
<table>
<thead>
<tr>
<th>The 30 unused files</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>Solved as shipped</td>
<td>19</td>
</tr>
<tr>
<td>Solved with the switch</td>
<td>4</td>
</tr>
<tr>
<td>Not settled</td>
<td>7</td>
</tr>
<tr>
<td>Proved unsolvable</td>
<td>0</td>
</tr>
</tbody></table>
<p>A stage only counts as unwinnable when the search runs out of positions. The seven unsettled ones hit the search budget, which proves nothing either way. Of the 23 solutions, 19 are proved shortest.</p>
<p>The 23 shipped in <a href="https://github.com/hazeliscoding/puzzle-boy/releases/tag/v0.2.0">v0.2.0</a> as stages 21 to 43, easiest first, with PageUp and PageDown to skip around.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/one-repo-became-three/four-jacks.webp" alt="Stage 34 in the port, a bonus stage with several Jack-o'-Lanterns, cleared by switching between them"></p>
<p>It worked. It also meant the &quot;faithful port&quot; now had a key, a controls-bar label and 23 stages the original never had.</p>
<h2>Three repos, one rule each</h2>
<p>A preservation port shouldn&#39;t contain things the original lacks. But the bonus stages and the switch were good, and I didn&#39;t want to throw them away. So I stopped asking one repo to do both jobs:</p>
<table>
<thead>
<tr>
<th>Repo</th>
<th>Its one rule</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://github.com/hazeliscoding/puzzle-boy-engine">puzzle-boy-engine</a></td>
<td>The rules, stage authoring, and later the code both games share. Not a game.</td>
</tr>
<tr>
<td><a href="https://github.com/hazeliscoding/puzzle-boy">puzzle-boy</a></td>
<td>The original&#39;s scope only: its 20 stages, in its order, from your own files.</td>
</tr>
<tr>
<td><a href="https://github.com/hazeliscoding/puzzle-boy-community">puzzle-boy-community</a></td>
<td>Anything new: stage select, an editor, level packs, the bonus stages and the switch.</td>
</tr>
</tbody></table>
<p>Both games pin the engine as a git submodule at a fixed commit. An engine change lands there first, then each game bumps its pin on purpose. There&#39;s no package feed and no floating <code>main</code>, so an engine change reaches a game only when that game asks for it.</p>
<p><code>Board.Switch</code> stayed in the engine, since Community uses it. The port just never calls it.</p>
<h2>Proving nothing changed</h2>
<p>The split itself was mostly moving files. The work was proving the port was still the port.</p>
<p>Before touching anything, I tagged the last commit <code>pre-split-baseline</code>: a clean build and 164 passing tests. Then:</p>
<ul>
<li><strong>The rules moved with their tests.</strong> 86 of the 164 tests went to the engine with the rules, unchanged but for their namespace.</li>
<li><strong>The stage list is pinned.</strong> A test checks that the port plays exactly the 20 documented files, each once, with none of the 23 bonus files, and that it loads only those 20 from a bundle holding all 50.</li>
<li><strong>The switch can&#39;t come back by accident.</strong> A test scans the game&#39;s compiled metadata for any call to <code>Board.Switch</code>. A scan for an absence proves nothing on its own, so the same scanner must also find <code>Board.Step</code>, which the game does call.</li>
</ul>
<p>Later, Community needed the original&#39;s look too (that&#39;s part 3), so the board renderer, Jack&#39;s model, the camera, the move timing, the sound and the bundle reading moved from the port into the engine. The rule for that move was verbatim. Each moved file was diffed against the last commit before the move: 12 files and 1,889 lines, of which 34 changed. Every change was a namespace, an <code>internal</code> that became <code>public</code> because a game now uses it, or a citation that now names the port&#39;s roadmap. No statement, constant or expression changed, so the port can&#39;t play or look any different. The HUD, menus and text renderer followed the same way: 1,081 lines, 19 changed.</p>
<h2>Community Edition had to play without Nocturne</h2>
<p>If you own Nocturne, you import it once, and Community Edition plays every stage in the original&#39;s look and sound: its board, its Jack-o&#39;-Lantern, its camera, its music. Your copy&#39;s stages come along too, as two more campaigns: the original 20 and the 23 bonus stages.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/one-repo-became-three/classic-stage-select.jpg" alt="Community Edition's stage select with a copy of Nocturne imported, in the original's style, with the Classic: Original 20 and Classic: Bonus campaigns beside First Steps"></p>
<p>How it got that look is part 3. It also had to work for everyone without a copy. The port needs one, and if Community did too, it would just be the port with extras. So its first rule, written into its roadmap, is that an unpacked release starts on a machine with no Nocturne, no Steam, no .NET, no network and no dev checkout, and reaches a playable stage. It never opens on a missing-assets screen.</p>
<p>Without a copy, the Default theme takes over: Pixel Jack, a pumpkin in a witch&#39;s hat, with art and sound drawn and synthesized in code. The builds are self-contained, so there&#39;s nothing to install first. Here are the same two screens each way:</p>
<figure class="pair">

<p><img src="https://www.hazeliscoding.dev/images/blog/one-repo-became-three/classic-title.jpg" alt="The title screen with a copy imported, with the original Jack-o'-Lantern"></p>
<p><img src="https://www.hazeliscoding.dev/images/projects/puzzle-boy/title.png" alt="The title screen without one, with Pixel Jack in his witch's hat"></p>
<figcaption>The title screen: with your copy, then without one</figcaption>
</figure>

<figure class="pair">

<p><img src="https://www.hazeliscoding.dev/images/blog/one-repo-became-three/classic-garden-path.jpg" alt="Garden Path with a copy imported, with the original HUD and key prompts"></p>
<p><img src="https://www.hazeliscoding.dev/images/projects/puzzle-boy/playing.png" alt="Garden Path without one, in the Default theme, after one step"></p>
<figcaption>Garden Path, the first stage: with your copy, then without one</figcaption>
</figure>

<p>It also needed stages of its own. Community ships 20 new ones in a pack called First Steps, made by designer agents with a breadth-first solver and curated into a difficulty curve: a walk, a first push, holes, long blocks, swivels and a switch tutorial. They take from 7 inputs to 161, and every one has a recorded solution that the tests replay to a clear.</p>
<p>The editor builds on the stage-authoring API that came with the engine, which reports problems with coordinates, so a broken stage says where it&#39;s broken:</p>
<p><img src="https://www.hazeliscoding.dev/images/projects/puzzle-boy/editor.png" alt="Community Edition's level editor flagging a block group split at (3, 6)"></p>
<h2>Keeping Nocturne&#39;s layouts out</h2>
<p>An editor and level packs raise an obvious question: what stops someone from converting one of Nocturne&#39;s 50 stages to JSON, mirroring it, and sharing it as their own?</p>
<p>Fingerprints. <code>LayoutFingerprint</code> hashes a stage&#39;s cropped grid in its smallest rotation or mirror, so turning, flipping or shifting a layout doesn&#39;t change it. Each of the 50 Nocturne stages gets two, with and without the goal and starts, and only those 100 SHA-256 hashes are in the repo, generated outside it from a local install. Community&#39;s tests fail if a bundled stage matches one, the pack importer refuses a matching stage, and without the list beside the game nothing is imported or exported at all.</p>
<p>The bonus stages live on in Community the way the port works: imported from your own install, on request. They never ship.</p>
<h2>What the reviews caught</h2>
<p>Every milestone ended with adversarial reviewers who tried to break it. Some favourites:</p>
<ul>
<li>Linux crashed at startup when the GL driver had no multisampling.</li>
<li>Turning on fullscreen caused a stack overflow through a resize handler.</li>
<li>A save file with a duplicate JSON key crashed startup.</li>
<li>A 115-byte fake bundle header made the classic import allocate until it ran out of memory.</li>
</ul>
<h2>Where it ended up</h2>
<p>Both games shipped on October 15. <a href="https://github.com/hazeliscoding/puzzle-boy/releases/tag/v0.3.0">puzzle-boy v0.3.0</a> plays the original&#39;s 20 stages on the shared engine, and nothing else. <a href="https://github.com/hazeliscoding/puzzle-boy-community/releases/tag/v0.1.0">Community Edition v0.1.0</a> runs on Windows, Linux and macOS, Apple Silicon and Intel. Its release job only publishes once every platform&#39;s smoke test passes; on macOS and Linux that means a real window opened, drew the title and stage 1, and took a step.</p>
<p>Then I changed my mind about how Community should look. That&#39;s part 3.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Digging a 2003 PS2 minigame out of a 2021 Unity remaster</title>
      <link>https://www.hazeliscoding.dev/blog/a-ps2-minigame-in-a-unity-remaster</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/a-ps2-minigame-in-a-unity-remaster</guid>
      <pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate>
      <description>Rebuilding Puzzle Boy from Shin Megami Tensei III Nocturne as a standalone game, from the game&apos;s own code but without shipping a byte of it. Part 1 of 3.</description>
      <category>reverse-engineering</category>
      <category>puzzle-boy</category>
      <category>claude-code</category>
      <category>ghidra</category>
      <category>csharp</category>
      <category>monogame</category>
      <content:encoded><![CDATA[<p>The method names in <em>Shin Megami Tensei III Nocturne HD Remaster</em> read like a PS2 source tree from 2003:</p>
<pre><code class="language-text">pbGame.pushCheck      pushing a block
pbGame.roundCheck     turning a swivel block
pbGame.hallCheck      falling into a hole (yes, &quot;hall&quot;)
pb_H                  the old pb.h header, now a C# namespace
pbEdit                the developers&#39; level editor, still in the build
</code></pre>
<p>That&#39;s Puzzle Boy, a minigame deep inside Nocturne. You guide Jack-o&#39;-Lantern through a maze by pushing blocks, turning swivel blocks and filling holes, while the Manikin Kid taunts you. It&#39;s Atlus&#39;s old Game Boy puzzler (Kwirk in the West), redone in 3D. I wanted it as a game of its own that runs anywhere, so I rebuilt it: <a href="https://github.com/hazeliscoding/puzzle-boy">puzzle-boy</a>.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/tour.webp" alt="Jack-o'-Lantern turns the view, switches to the overhead camera and undoes a push from the menu, in the port"></p>
<p>This is part 1 of 3. It covers the first two days: reading the rules out of the game, the line I drew around its copy protection, and how the rules became tests before they became code. <a href="https://www.hazeliscoding.dev/blog/one-repo-became-three">Part 2</a> is about the stages the original never plays, and how one repo became three.</p>
<h2>The rules I started with</h2>
<p>Before writing any code, I wrote down what the project would never do, in the repo&#39;s <code>AGENTS.md</code>, so every agent session starts with it:</p>
<ul>
<li><strong>No Atlus data in the repo.</strong> No stages, text, audio, textures, models, metadata dumps or decompiler output. The game reads all of it from your own install at startup.</li>
<li><strong>No decompiled code in the source.</strong> Every rule found in the game becomes a test on a handmade stage first, and the code is written from the test.</li>
<li><strong>No circumvention.</strong> More on that below.</li>
</ul>
<p>I built it with Claude Code doing the reverse engineering through <a href="https://rea.tools">REA</a> and Ghidra, and me deciding.</p>
<h2>The metadata talks first</h2>
<p>The HD Remaster is a Unity 2019 game compiled with IL2CPP: C# turned into a 383 MB native <code>GameAssembly.dll</code>. IL2CPP keeps every class, method and field name in a separate file, <code>global-metadata.dat</code>, and Nocturne&#39;s isn&#39;t encrypted.</p>
<p>So before decompiling anything, a small parser read 5,378 type definitions. The Puzzle Boy classes were all there, with the names from the list above. <code>pbGame</code> alone has 74 fields and 60 methods. Nearby were <code>sdfChipFreeDelayed</code>, <code>dds3PrimFrameRect</code>, and <code>mcSave</code> for the PS2 memory card. The remaster is the 2003 C code, converted to C# and kept nearly as it was. The help text still documents the developers&#39; edit mode, translated into eight languages, and the debug menu has a &quot;PUZZLE BOY GAME&quot; entry next to &quot;MODEL VIEWER&quot;.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/manikin-kid.jpg" alt="The Manikin Kid explains the goal before stage 1: &quot;Do you see that spinning blue cube? That's the goal.&quot;"></p>
<h2>Six files broken by a line ending</h2>
<p>The stages are 50 files in a Unity bundle called <code>pazzleboy</code> (Atlus&#39;s typo). Each is 828 bytes: a character count, four start positions, a goal, then a 20×20 grid of 16-bit cells. <code>0x2000</code> is a wall, <code>0x0800</code> a hole, <code>0x60nn</code> part of block group <code>nn</code>, and swivels have their own pair of types.</p>
<p>Six files were a few bytes longer. In one of them, the character starts at y = 2573 on a 20-row grid.</p>
<p>Each of the six had exactly as many <code>0D 0A</code> byte pairs as extra bytes. Somewhere in Atlus&#39;s pipeline, a line-ending conversion treated these binary files as text and turned every <code>0A</code> byte into the Windows <code>0D 0A</code>. Undo it, and every file is 828 bytes again and makes sense. The broken one starts its character at (17, 10).</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/first-render-stage-44.jpg" alt="The port's first render: stage file 44, one of the six damaged files, drawn as plain boxes after the repair"></p>
<p>The code later explained why nobody ever noticed: the original never loads those six files. The port repairs them anyway, and its <code>.gitattributes</code> marks stage files as binary, so the same thing can&#39;t happen to me.</p>
<h2>The line around Denuvo</h2>
<p><code>GameAssembly.dll</code> is protected by Denuvo Anti-Tamper. I wasn&#39;t going to dump the running game&#39;s memory, de-obfuscate anything or patch it. What made the project possible at all was one check: all 225 Puzzle Boy methods live in the unprotected <code>.text</code> section, along with 13,002 of the game&#39;s 13,028 methods. The protected code is a separate 316 MB section.</p>
<p>So the rule is short: read only unprotected code, statically. If a question leads into protected code, answer it by playing the real game instead.</p>
<p>The usual tools didn&#39;t make it easy. REA couldn&#39;t open Unity bundles and had no IL2CPP support. Il2CppDumper couldn&#39;t find IL2CPP&#39;s registration tables, because the protector had renamed the sections that hold them. Found by hand, the tables turned out to use the v24.3 layout while the metadata claimed v24.2, and the dumper crashed on that too. So a script of about 100 lines followed each method&#39;s token to its address and wrote 13,028 names into a seed file for Ghidra.</p>
<h2>Ghidra versus a 32 GB laptop</h2>
<p>Then Ghidra had to analyze 383 MB of native code. The first run died about 12 minutes in, when the agent&#39;s session ended and took its background job with it. The second was launched outside Claude Code&#39;s process tree. After 52 minutes, Java held 11.4 GB and the machine had 0.1 GB of RAM free. I stopped it.</p>
<p>The overnight run spent 6.5 hours looping through the protected section without finishing. Denuvo&#39;s code is virtualized, so there&#39;s nothing readable there anyway; Ghidra was just wading through it.</p>
<p>So I made a local analysis copy of the DLL with the protected sections emptied. <code>.text</code> stayed byte-identical, and every address still lined up with the seed file. Nothing was defeated and nothing ran: it just removed the protected code from what Ghidra looked at. The analysis finished in <strong>41 minutes</strong>.</p>
<p>One beat I like: the agent then tried to commit that trimming script to the public repo, and Claude Code&#39;s own safety check refused. A public script that strips sections from a Denuvo-protected binary reads as anti-DRM tooling, whatever it&#39;s for. I agreed. The script and the copy stayed on my machine, and the explanation went here instead.</p>
<h2>Reading the rules, with an adversary</h2>
<p>The research ran as one batch: seven agents each read one area of the decompiled code (pushing, swivels, the turn loop, stages, loading, undo, presentation), and for each one, a second agent re-checked every claim against the pseudocode and tried to prove it wrong. Then a critic looked for what was missing.</p>
<table>
<thead>
<tr>
<th>The research batch</th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>Agents</td>
<td>15</td>
</tr>
<tr>
<td>Tool calls</td>
<td>1,055</td>
</tr>
<tr>
<td>Claims made</td>
<td>111</td>
</tr>
<tr>
<td>Confirmed</td>
<td>97</td>
</tr>
<tr>
<td>Refuted or corrected</td>
<td>10</td>
</tr>
<tr>
<td>Left uncertain</td>
<td>4</td>
</tr>
</tbody></table>
<p>The findings went into the public <code>ROADMAP.md</code> as plain-language rules, each with a short evidence ID, and never as pseudocode. A few surprises:</p>
<ul>
<li><strong>Only 21 of the 50 stage files are ever played.</strong> Level 1 is 20 stages in a fixed order. The six damaged files are among the unused ones.</li>
<li><strong>There&#39;s no move counter.</strong> The only number on screen is the stage counter.</li>
<li><strong>Undo keeps 30 steps and only records pushes and turns.</strong> Undoing throws away any walking since the last push.</li>
<li><strong>The original has a real bug:</strong> its character-collision check swaps x and y. It never matters, because every played stage has exactly one character.</li>
</ul>
<h2>Rules as tests</h2>
<p>Every rule then became a test on a stage drawn as text. Columns are x and rows are y, and a helper turns the drawing into real 828-byte stage bytes, so every test also goes through the parser:</p>
<pre><code class="language-text">#2##
1a..
#o##
#G##
</code></pre>
<p><code>#</code> is a wall, <code>.</code> floor, <code>o</code> a hole, <code>G</code> the goal, digits are characters and lowercase letters are block groups. The expected result is just another drawing.</p>
<p>To check the tests had teeth, I broke the engine on purpose nine ways: reverse the swivel&#39;s turning sense, skip the fall into a hole, drop the sweep check, set the undo limit to 31, and so on. Every mutation failed at least one test. Reversing the swivel sense failed 22.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/menu-undo.jpg" alt="The original's □ menu in the port, with UNDO taking back a push"></p>
<h2>2,756 inputs, and the board was upside down</h2>
<p>Tests prove the code matches what I read. They don&#39;t prove I read it right. For that I needed the real game&#39;s solutions.</p>
<p>AppMedia publishes the full input list for every stage of the HD version. Parsed into left, right, up and down and replayed in the port, they cleared all 20 stages: <strong>2,756 inputs, each stage clearing exactly on its last one, with none refused.</strong></p>
<p>Except at first, they didn&#39;t. Stage 1&#39;s solution only worked under one of the eight possible ways to map screen directions onto the grid, and it wasn&#39;t the port&#39;s. Gameplay video showed why: the original draws +y going <em>up</em> the screen. The port drew it down, so every stage was a mirror image and swivels seemed to turn the wrong way. The rules were right; only the picture was flipped. After a one-line fix, the same mapping cleared all 20.</p>
<h2>Measuring the clock from video</h2>
<p>The original runs one tick per rendered frame, so its speed depends on your frame rate. A 144 Hz monitor plays it faster. The code can&#39;t tell you what &quot;normal&quot; is.</p>
<p>Gameplay video can. In 60 fps footage, every move lasts a multiple of 8 frames, and stage 1&#39;s five-cell walk lasts exactly 40. That&#39;s 4 ticks a step at 30 ticks a second, which is what the port runs. Even the extra tick after a swivel turn shows up as a 2-frame pause.</p>
<h2>Making it look like the real thing</h2>
<p>The rest of the first two days went into the original&#39;s look, all read from the player&#39;s install at startup: the board textures (DXT1, decoded on the CPU), the backdrop, Jack-o&#39;-Lantern&#39;s skinned model and idle animation, the sound effects and music (FMOD banks with Vorbis inside), the HUD, the □ menu and the Manikin Kid&#39;s lines.</p>
<p>The game ships no font file. Its text exists only as TextMeshPro distance-field atlases, so the port runs TextMeshPro&#39;s shader math on the CPU to draw it.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/complete.jpg" alt="COMPLETE! in the port, after the last push of a stage"></p>
<p>Before polishing, a review compared the port&#39;s timing against the research and the footage and found 12 real differences. The overhead view turns in 12 ticks, not 9. The camera orbits a point a quarter cell below Jack. The next stage loads 35 ticks after the winning input. And the original reads no input at all while a move plays, so a key pressed then is lost, not queued.</p>
<h2>Keeping it honest</h2>
<p>&quot;No Atlus data in the repo&quot; is a rule an agent can break by accident, so CI checks it. A script inspects every tracked file by its first bytes for Unity bundles, FMOD sound banks, IL2CPP metadata and Atlus flow scripts. It also rejects stage files outside the tests and Ghidra&#39;s generated names in the source.</p>
<p>A check for an absence proves nothing until you see it fail, so it has a positive control that plants each kind in a scratch repo and expects a failure. I also opened a throwaway pull request with a 4-byte stage file in it. CI failed exactly where it should, and the PR was closed unmerged.</p>
<p>Two mistakes worth admitting. To take screenshots of other stages, the agent once sent keystrokes to the game, while Windows was keeping focus on whatever window I had open. And the first frame-capture tool played the game&#39;s music on my speakers. Both tools now run the game with no real input and no sound.</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/a-ps2-minigame-in-a-unity-remaster/stage-20.jpg" alt="Stage 20, the last stage, in the port"></p>
<h2>Where it ended up</h2>
<p><a href="https://github.com/hazeliscoding/puzzle-boy/releases/tag/v0.1.0">v0.1.0</a> shipped about 24 hours after the first question. It has Windows, Linux and macOS builds, all 20 stages, and the original&#39;s look, timing, sound, HUD, dialogue and menu, read from your own copy. You need <em>Shin Megami Tensei III Nocturne HD Remaster</em> to play it, and the repository holds none of it.</p>
<p>Then I looked at the 30 stage files the original never plays, and found that six of them can&#39;t be won as shipped. That&#39;s <a href="https://www.hazeliscoding.dev/blog/one-repo-became-three">part 2</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Every trap is proven: building an agent skill you can trust</title>
      <link>https://www.hazeliscoding.dev/blog/every-trap-is-proven</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/every-trap-is-proven</guid>
      <pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate>
      <description>What a coding agent gets wrong porting SDL2 to SDL3, how I measured it, and the agent that searched the whole disk for headers.</description>
      <category>claude-code</category>
      <category>agent-skills</category>
      <category>sdl3</category>
      <category>evals</category>
      <category>c</category>
      <content:encoded><![CDATA[<p>This compiles against SDL3 without a single warning:</p>
<pre><code class="language-c">if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {  // SDL3 returns true on success
    SDL_Log(&quot;SDL_Init failed: %s&quot;, SDL_GetError());
    return 1;                                          // so this runs on every launch
}
</code></pre>
<p>In SDL2, <code>SDL_Init</code> returned 0 on success. In SDL3 it returns <code>true</code>. The line looks the same, the compiler is happy, and the game quits every time it starts.</p>
<p>SDL3 is full of changes like that. An audio stream now opens paused, so the game is silent. Textures filter linearly by default, so pixel art comes out blurry. A texture with an alpha channel now blends, so an emulator&#39;s screen can go invisible. SDL&#39;s rename scripts handle most of a port and the compiler catches most of the rest, but neither one catches code that still compiles and now means something else. Coding agents make exactly these mistakes, because most of the SDL code they learned from is SDL2.</p>
<p>So I built <a href="https://github.com/hazeliscoding/sdl3-porter">sdl3-porter</a>, a Claude Code plugin that ports C and C++ from SDL2 to SDL3 and catches those changes. This post is less about the plugin than about the question I kept asking while building it: how do I know it works?</p>
<p><img src="https://www.hazeliscoding.dev/images/blog/every-trap-is-proven/port.gif" alt="Claude Code loads sdl3-porter, ports a small SDL2 game, builds and runs it, and reports each trap it fixed by file, line and trap id"></p>
<h2>What counts as a trap</h2>
<p>I gave myself a strict definition. A trap is a change that compiles against SDL3 with warnings as errors (<code>-Wall -Wextra -Werror</code> on GCC and Clang, <code>/W4 /WX</code> on MSVC) and then breaks at runtime. If any compiler warns about it, the compiler already catches it, and it goes in a reference file instead.</p>
<p>A trap also doesn&#39;t count until it&#39;s proven. Each one has three small C programs:</p>
<ul>
<li>the <strong>SDL2 original</strong>, which must pass against SDL 2.32;</li>
<li>the <strong>naive port</strong>, the code an agent would write, which must compile cleanly and then <strong>fail</strong>;</li>
<li>the <strong>fixed port</strong>, which must pass.</li>
</ul>
<p>CI builds all three and runs them headless on Windows, Linux and macOS, against SDL 3.4.18 and the oldest supported 3.2.0. Headless means no display, no sound card and no gamepad: the offscreen video driver, dummy audio, SDL&#39;s virtual joystick and the software renderer.</p>
<p>The naive port is the important one. It&#39;s the positive control. If it ever stops failing, the trap is no longer proven, and CI goes red. It also has to fail for the right reason: it must print one line that starts with the trap&#39;s id, like <code>bool-returns: SDL_Init reported failure</code>, so a setup error can&#39;t pass for the trap.</p>
<p>That rule threw out a lot of good ideas. Nintendo face buttons, rumble, exclusive fullscreen and high DPI all change in SDL3, but none of them can be proven without real hardware. They became 12 sections of guidance that the agent checks by reading the code. The 17 traps are only the changes I could make fail on demand.</p>
<p>Proving things headless had its own traps. The vsync fixture can&#39;t time frames, because SDL2&#39;s software renderer reports vsync as on and never actually waits. It checks the renderer&#39;s state instead.</p>
<h2>Measuring it</h2>
<p>A trap card tells the agent what to look for. That doesn&#39;t mean the agent finds it. For that I used evals: run Claude Code on an SDL2 program, let it port the code, then check the result.</p>
<p>Two rules made the scores worth believing.</p>
<p><strong>Don&#39;t port the fixture.</strong> The fixture&#39;s SDL2 original checks the exact behavior the trap is about, which points straight at the answer. Each eval ports a separate sample instead: a plausible little SDL2 program with a neutral name and no self-checks. One more case ports a whole game, where the agent has to find the traps itself.</p>
<p><strong>Every grader has to be able to fail.</strong> Most graders are a regular expression over the ported files that checks one trap fixed or one porting step done. A test runs every trap grader against a port made only with SDL&#39;s rename scripts, and fails if any of them passes it. A grader that can&#39;t fail measures nothing.</p>
<p>Each case runs five times with the skill and five times without it, on Sonnet 5 and Haiku 4.5. I also wrote down the rules for messy results before I had any. An infrastructure error gets one traced rerun, and a second error counts as measured. Hitting the turn limit is a real result. No threshold ever gets lowered. When a new Claude Code release shipped in the middle of a batch, I pinned the version and reran the cases that had mixed versions, so one table never mixes two versions.</p>
<h2>What the numbers say</h2>
<p><img src="https://www.hazeliscoding.dev/images/blog/every-trap-is-proven/scores.svg" alt="Eval scores with and without sdl3-porter for each case, on Sonnet 5 and Haiku 4.5"></p>
<p>Without the skill, Sonnet 5 already gets 10 of the 17 traps right every time. On the other 7 it scores between 0.00 and 0.47. With the skill it scores 1.00 on all of them.</p>
<p>The skill doesn&#39;t help much with what the model already knows. It helps with silent default changes, where the SDL2 code looks perfectly fine: textures that now blend, textures that now filter, audio that SDL2 started by itself and SDL3 doesn&#39;t. Sonnet scores 0.00 on all three of those without the skill.</p>
<p>Finding is harder than fixing. In an early run, <code>text-input-off</code> scored 1.00 without the skill as a small standalone sample. In the whole game, the same model missed it in 3 runs out of 3. One trap in a small file is easy to spot. One trap in a game is easy to miss.</p>
<p>The smaller model gains more. On the whole game, Haiku 4.5 went from 0.39 without the skill to 0.92 with it. It isn&#39;t perfect, though. It scores 0.40 on <code>window-mode-null</code>, where it keeps the old call behind a NULL check and calls that a fix, and 0.80 on <code>logical-scale-separate</code>. I shipped 1.0 with both listed as known gaps instead of running the evals until they looked better.</p>
<p>Without the skill, scores also move between runs. <code>linear-by-default</code> scored 0.33 in one run and 0.00 in the next. Five runs per case is a small sample, so I read the table as &quot;reliably fixed&quot; versus &quot;sometimes missed&quot;, not as exact percentages.</p>
<h2>The agent that searched the whole disk</h2>
<p>The first smoke run took 785 seconds with the skill and 43 without. The skill was making the agent much slower.</p>
<p>A traced rerun showed why. The agent spent 87 seconds on one command: <code>find /</code>, looking for <code>SDL_video.h</code> so it could check its port against the SDL3 headers. I added a line to the skill telling it not to search for headers. In the first full eval run, one run hit the 1,200-second timeout anyway. A traced rerun showed the agent running <code>find / -xdev -iname SDL3.h</code> for 120 seconds, even though the skill had told it not to.</p>
<p>The fix was to stop asking for the behavior and remove the reason for it. The skill now ships with the 66 SDL3 headers it needs, 2.3 MB of them. The slowest run dropped from 1,200 seconds to 342.</p>
<p>Then it happened again. After dogfooding, I added a step telling the agent to build the port where it could. Three runs took 746, 808 and 1,130 seconds. This time the agent was searching the disk for an <em>installed</em> SDL3, its library or its CMake files, to decide whether it could build. My rule only covered headers, so this search wasn&#39;t against it.</p>
<p>The rule that finally held was about the action, not the target: don&#39;t run <code>find</code> or <code>locate</code> outside the project, and ask the build instead, with <code>pkg-config --exists sdl3</code> or by configuring the project. In the same nine runs, the agent searched outside the project zero times, and the slowest run took 202 seconds.</p>
<p>A rule against one target invites a search for the next one. A rule about the action covers the next target too.</p>
<h2>Facts in the output beat rules in the docs</h2>
<p>For 1.0, I added a script, <code>find_traps.py</code>, that runs every trap&#39;s search over the project in one command. Writing it found a bug of its own: seven searches never matched pointer access such as <code>ev-&gt;gbutton.button</code>, so they had silently skipped some real code.</p>
<p>The script also prints facts next to its hits. When it sees an old SDL2 hint name, it prints &quot;removed in SDL3&quot; beside it. Before that, Haiku left the dead hint in the whole game in 5 runs out of 5. In a check after the change, it left it in 1.</p>
<p>Rules in the docs did worse. One trap card says in plain words that a NULL check is not a fix. Haiku read that card and then called a NULL check &quot;no change needed&quot;. The traces also showed why small models miss things: Haiku reads files with a line limit, 80 to 150 lines at a time, so a trap card near the bottom of a long reference file sometimes doesn&#39;t get read at all.</p>
<h2>Against the maintainers</h2>
<p>Evals use programs I wrote. To test against real code, I ported open-source projects starting from the commit just before their maintainers did their own SDL3 port, then compared the two ports.</p>
<p>An early comparison found a trap I didn&#39;t have. A Game Boy emulator&#39;s maintainer had fixed a texture that SDL3 now blends by default, which made the whole screen invisible. The agent&#39;s port had missed it. It became <code>blend-by-default</code>, the trap Sonnet scores 0.00 on without the skill.</p>
<p>The larger ports:</p>
<table>
<thead>
<tr>
<th>Program</th>
<th>Size</th>
<th>Cost</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Woof!, a Doom source port</td>
<td>about 165,000 lines</td>
<td>$15.34, 91 min</td>
<td>builds clean, plays a demo headless</td>
</tr>
<tr>
<td>scrcpy</td>
<td>about 31,000 lines</td>
<td>$12.44, 22 min</td>
<td>builds clean, unit tests pass</td>
</tr>
</tbody></table>
<p>Both had misses. In scrcpy, the agent&#39;s own search found the text input code, and then it never turned text input on. Both agents ran every search at once and skimmed past hits. Now the skill asks for a verdict on every section that has hits. Woof!&#39;s menu turned up a mouse-mapping bug in letterboxed windows, which became the 17th trap, <code>logical-scale-separate</code>. The maintainers&#39; own port has the same bug.</p>
<p>The agent also beat the maintainers twice. scrcpy&#39;s own port sent a loop index in gamepad events where SDL3 expects a joystick ID, and the agent&#39;s port sent the ID. Woof!&#39;s port kept the <code>__MACOSX__</code> macro, which SDL3 no longer defines, so some macOS-only code silently stopped building. The agent had renamed it to <code>SDL_PLATFORM_MACOS</code>. I reported that one as <a href="https://github.com/fabiangreffrath/woof/issues/2952">woof#2952</a>. A contributor opened a fix within hours, and I checked the fix&#39;s macOS build without a Mac, by comparing strings in the binaries.</p>
<p>Every dogfood report also overcounted. One listed 6 traps where there was 1. Later, the port I recorded for the README flagged <code>blend-by-default</code> on a sprite whose pixels were all opaque, because the card&#39;s wording allowed it. The agent follows what the card says, not what I meant, so the cards got stricter.</p>
<h2>How it was built</h2>
<p>I built sdl3-porter with Claude Code itself. The repository&#39;s <code>AGENTS.md</code> holds the rules from this post: no trap without a naive port that fails, cite SDL&#39;s migration guide for every trap, never lower a threshold, and test every check that looks for an absence against a case where it should fail. Most of what&#39;s above is what happened when those rules met the evidence.</p>
<h2>Try it</h2>
<p>In Claude Code:</p>
<pre><code class="language-text">/plugin marketplace add hazeliscoding/sdl3-porter
/plugin install sdl3-porter@sdl3-porter
</code></pre>
<p>Then open an SDL2 project and ask Claude Code to port it to SDL3. Commit first, because the port is meant to be reviewed as a diff. The <a href="https://github.com/hazeliscoding/sdl3-porter#readme">README</a> has the full eval table and the known gaps, and <a href="https://github.com/hazeliscoding/sdl3-porter/blob/main/ROADMAP.md">ROADMAP.md</a> records every decision, with its date.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hello, World!</title>
      <link>https://www.hazeliscoding.dev/blog/hello-world</link>
      <guid isPermaLink="true">https://www.hazeliscoding.dev/blog/hello-world</guid>
      <pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate>
      <description>My first post on this portfolio site.</description>
      <category>angular</category>
      <category>portfolio</category>
      <category>personal</category>
      <content:encoded><![CDATA[<p>I’ve wanted a place on my own domain to write short notes about what I’m learning, what I’m building, and what I’m thinking about in tech.</p>
<!-- ![Blog placeholder image](/images/blog/placeholder.svg) -->

<p>This blog lives directly inside my Angular app, using Markdown files that get turned into routes at build time.</p>
<h2>What I’m optimizing for</h2>
<ul>
<li>Owning my content</li>
<li>Keeping it simple</li>
<li>Great performance and SEO</li>
</ul>
<p>Thanks for reading — more posts coming soon.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
