How the robot works
I showed up to Huntress CTF 2026 ten days late, played it solo, and let a robot do the playing. It finished 4th overall, one of 56 teams to complete every check, and four-tenths of a second off the podium. It took twenty-one days and two generations of robot. This is how the machine worked, what it did on a race morning, and every morning it fell on its face.
The short version. Every morning at 09:00 Eastern a new day of challenges dropped: file forensics, malicious scripts, compiled programs, Android apps, and from Day 19, live websites to break into. My robot raced them. Anything it had seen before, it answered with plain code, no AI anywhere in the path, usually in seconds. Anything new, it wrote up in a short report, and Claude agents on standby wrote it a fix. An automatic judge tested every fix against every answer the robot had ever been graded on, and the running robot picked it up mid-race without a restart.
Race times are from the public scoreboard (final read 2026-11-01); speed-board counts are from the read at 09:10 ET on Oct 31. No flags, token or challenge artifacts appear on this page, and the example questions are invented to show a shape. The diary entries tell the story day by day; this page is the machine.
The game, and the one number that mattered
Huntress ran this year as an "AI Arena": 31 days, a new day at 09:00 ET, and the same five categories of challenge every day, plus a sixth, web, from Day 19. I call each category a lane. The organizers openly asked players to build deterministic solvers and let the robot play, which is about the best invitation I have ever received. The challenges come from the event's API, the Game Master, which hands out the files and grades the answers.
Every challenge comes in three modes, and each one unlocks the next:
- Single Solve: no clock. Download a file, answer its questions.
- Time Trial: a fresh variant of the file against a server clock. That clock was 30 s on Day 1, 6 to 10 s by Days 4 and 5, then 30 to 325 s from Day 15 on.
- Mastery: several variants at once, often in shapes the Time Trial never showed, against one shared clock.
One finished mode is a check, so five lanes times three modes makes 15 checks a day, 21 or 24 once web arrived. Day 30 threw in one extra: SHA1-Hulud, an old-fashioned reverse engineering challenge, Single Solve only and worth 5 points. Remember that one.
Three rules of the game shaped almost everything I built:
- One wrong answer sinks the whole batch. Send 1,500 answers together and a single bad value saves none of them. Send answers one at a time and a wrong one costs nothing.
- Ranked attempts run out. One Time Trial had 255 of them; one Mastery had 2. After that, only untimed practice is left.
- The grader's wording is the answer. Is that program
ET_EXECorEXEC,64orELF64? Only the grader knows. If the thing a question asks about is missing, the answer is the literal wordnone.
On top of that, the server takes about 100 calls a minute from one team, every kind of call on the same counter. That sounds like plenty until a robot gets excited.
Then came the realisation that defined the whole project. There is no dynamic scoring. Every team that solves everything ends on the same points, so the order among them comes down to who banks their last flag first. Day 31 would decide it. From then on every morning was a dress rehearsal, judged on one number: seconds from the release to the last check correct. I call that stretch the race. The leaders were already doing all 15 checks in 45 to 60 seconds.
That is exactly how it ended. Fifty-six teams finished every check on 1,885 points. On Halloween morning the top four banked their last flags at 234.1, 236.4, 255.1 and 255.5 seconds. We were the 255.5.
The idea in one minute
The whole design is one bet. Most of what this event asks, the robot has seen some version of before, and for those it uses ordinary code: read the file, find the answer, submit. No AI model sits in that path. It is fast, it is the same every time, and it never refuses.
The AI comes in at the edges, where something is genuinely new. When the robot meets a question it cannot answer, it writes down exactly what it is stuck on, and AI agents waiting on standby take it from there. Some write a small fix to the code; others just answer that one question directly. A fix written by a model never touches the race until an automatic judge has tested it against every answer the grader has ever accepted. A refusal or a slow model can delay a fix. It can never stop the robot.
After each race the day's fixes become permanent code, each with a test, so tomorrow's first try starts further ahead than today's.
Two principles held across both generations of the robot. The lane is the unit of learning, not the challenge: the challenges are new every day, but the lanes repeat, so every lesson goes into that lane's shared engine. And right first, then fast, which later sharpened into something blunter: a miss only costs time. Once my board showed that a practice completion still earns the check mark, a miss stopped being fatal and became a delay.
A race morning, start to finish
Take Day 29. Release to the last of 21 checks took 72.5 seconds, nobody beat it that morning, and it put us 1st overall. Here is that morning from the robot's side, in the order things happen.
Before the bell
At 08:30 the robot gives itself a checkup before it is allowed to race. Can it reach the Game Master? Is its clock in step with the server's? Is there disk? Does every solver load? Does each lane still get its old answers right when it replays files the grader has already marked? Has the event's API changed overnight? Anything broken shows up here, not at 09:00.
At 08:50 the race starts up, with a watcher alongside it: an AI session that follows the race live and can step in. Every lane gets its own worker, already warmed up, solvers loaded, connections to the server already open, so nothing has to start up after the release. A minute or two before 09:00 the AI side gets ready too. The judge loads each lane's answer key, and the agents start and wait. On a morning with nothing new, they never get a job.
The bell
From 08:59:00 the robot starts asking whether the day is out. It never waits on one slow answer: it asks several ways at once, faster and faster in the last few seconds, and whichever signal comes back first wins. Warmups gets a shortcut. Its challenge name is predictable, so the robot can ask for it by name and start it before the day's list has even appeared.
The known stuff goes in seconds
Each lane downloads its file, unpacks it while it is still arriving, runs its solver on every question and submits. Answers the solver is sure of go in one batch. Answers it has to guess at go one at a time, because a wrong single costs nothing and a wrong value in a batch costs everything. The moment the Game Master says CORRECT, the lane starts the Time Trial, then the Mastery, the same way. On Day 29 every file lane, all fifteen of its checks, was done by 27.0 seconds. No model touched any of it.
The new stuff gets a ticket
When a solver has no answer, or the grader rejects the one it sent, the lane writes a gap ticket within 0.3 seconds: which question, which file, what it tried and what the grader said. The other lanes keep racing, and a timed attempt still inside its deadline simply stays open while help arrives. From Day 15 on the clocks were long enough for that to work.
The agents on that lane grab the ticket. A patch agent writes a small piece of code or data that teaches the solver the new shape, then hands it to the judge. The judge runs it locked in a sandbox against the lane's whole graded history, and lets it through only if it answers the ticket without changing a single answer the grader ever accepted. The running lane picks the patch up, re-solves its open questions and submits. Meanwhile a hand agent can work the question the way a person would and queue an answer for just that attempt. Whichever gets there first closes the question.
On Day 29 the last check was the second web challenge's Mastery. It came in after one hand answer, about 17 seconds of work, and one judged patch, 16 seconds. That was the morning: two small new things, handled while the clock ran, and 72.5 seconds on the board.
After the finish
Every race attempt goes into the graded record: every file and question the robot has ever been served, with the grader's verdicts. That record is the answer key every later change is tested against. Then comes the day close. The day's live patches get folded into proper solver code, the whole day is replayed with no agents at all, and nothing is saved until it passes the commit gate.
The same morning as a timeline. Orange markers are the steps where a model may be involved.
- Checkup: Game Master reachable, clock in step with the server, disk space, every solver loads, a quick replay per lane, and the live API compared with yesterday's.
- The race starts up with a watcher alongside. Each lane's worker loads its solvers and opens its connections.
- The judge loads each lane's answer key. Patch agents and hand agents start and wait for tickets.
- Release watch starts, once a second. Per-lane checks join at 08:59:57, and everything ticks every half second for the last three seconds.
- The first signal back wins. Warmups can start before the day's list appears. Each challenge goes to its lane, and a slower watch keeps looking for any listed late.
- Each lane downloads, unpacks as it streams in, solves and submits. On a known shape nothing waits on a model.
- An open or rejected question writes a gap ticket. Guesses go out one at a time. A timed attempt still inside its deadline stays open.
- An agent writes a patch and runs the judge. On a pass, the patch goes into the running lane, which re-solves its open questions and submits.
- The second web lane's Mastery lands. All 21 checks CORRECT and no attempt lost. Race and learning records go into the graded record; speed-only runs are kept separate.
- Live patches become permanent engine code, the day is replayed with no agents, one process improvement is tested, and the final changes pass the gate.
Day 31 ran this exact sequence and shows the other side of it. Both web lanes saw the release at 0.12 s and had their targets by 3.7 s, and five lanes were done by 76.9 s. The sixth sat waiting on its agents for about 228 s: five agents tried roughly 480 role-header combinations before a raw dump of the server's response showed the header it was actually reading.
The parts, in the order they act
That was the morning. Here are the pieces that made it, in the order they act. The picture splits in two. Along the top is the known-shape path: plain code that spots the release, downloads, unpacks, solves and submits. Along the bottom is what I call the gap loop, where the AI comes in: ticket, agents, judge, patch. The other lanes never wait on the gap loop, but the lane that opened the ticket does.
The race path
Release poller
Watches for the 09:00 drop by asking several ways at once instead of one question at a time: the day's full list, a check per lane, and a direct look at the warmups challenge, whose name is predictable. The fast checks stop the moment the release is seen. A slow one keeps going, so a challenge listed late still reaches a lane without restarting anything.
Lane workers
One warmed-up worker per lane. It takes its challenge and runs Single Solve, Time Trial and Mastery back to back, starting each the moment the last one comes back CORRECT. It keeps its state on disk, so if it crashes it reconnects to its live attempt instead of opening a new one.
Spare lanes
Standby workers for a challenge the robot has no lane for: a new category, an extra web challenge, a second challenge in a known lane. Two are warm before the release, and more start on demand, up to eight. A spare borrows a known lane's solver when it can and otherwise hands the ticket to its agents. Day 30's SHA1-Hulud took a spare. It had no questions at all, just a flag field, so it went to a human: me.
Solvers
Each lane has its own engine: the shared code for that category that reads the file, answers the questions, and gets smarter every day. A solver is handed a question and its files and returns an answer, a ranked list of guesses, or nothing. What each lane's solver actually does is in The six lanes.
Submitter
Sure answers ride in one batch; guesses go one at a time, so a wrong one costs nothing. If a big batch is rejected, it splits into smaller packs by question type. Anything sent after the deadline is recorded as unknown, never as wrong. That rule exists because 2,842 late answers were once mislabelled and poisoned the robot's own training record.
Rate governor
One shared traffic cop keeps the whole robot under the server's limit. In a race there is no self-imposed cap, just a guard against a runaway loop. If the server does say "slow down" (an HTTP 429), everyone pauses, except a call for a live timed attempt, which keeps trying until just before its deadline.
When a lane gets stuck
The gap ticket is the handoff, and keeping it small was one of the best decisions in the project. It holds only what the solver already knew: the question, the file, what it tried, what the grader said, and a one-word reason it got stuck. It ends with a work order: the shape of answer wanted, an earlier example, where to put the fix, and the command that judges it. An agent does not have to go exploring; it starts with everything on one page.
The agents. Every lane has three patch agents and an Opus hand agent waiting on it; web and the spare lanes also get a Sonnet hand agent. Patch agents write fixes. Hand agents answer the question directly for that one attempt, and when an answer is accepted, the method behind it can be handed to a short-lived derivation agent that turns it into a proper fix, so the next file gets solved, not just answered.
The judge is the reason I could let AI patch a race while it was running. It runs every fix in a locked box with no network and checks it against the lane's whole graded history. A fix passes only if it answers the stuck question and leaves every answer the grader ever accepted exactly where it was. That does not prove a new answer is right; only the live grader can say that. What it proves is that the fix broke nothing.
Hand answers are the one deliberate shortcut past the judge. A hand agent can queue a value for one specific attempt, and the robot sends it on its own, never inside a batch. A wrong hand answer can waste time, but it can never sink a batch of good ones.
Carry means reusing a fix or an accepted answer from one mode in the next mode of the same challenge. Carried code starts out untrusted, sends its answers one at a time, and earns trust only when the grader accepts it.
Backup and memory
Two binary racers
Binary starts on two racers at the release, one at home and one in the cloud. They share the same attempt on the server, so whichever finishes first wins without opening a duplicate. Afterwards the two records are reconciled, keeping the one with more accepted answers.
The graded record
Every race and learning attempt is saved with its questions and the grader's verdicts. This is the answer key every change is tested against. Speed-only reruns are saved elsewhere, so they cannot slow the judge down. The grader's verdict is the only truth here; unknown is never treated as correct.
Where the AI sits, and what fences it in
Security challenges look a lot like malware, because they are training versions of it, and AI safety filters do not always know the difference. On Day 5 a script specimen drew a false-positive refusal. On Day 20 all three web patch agents and the first fallback model were stopped while writing exploit steps; a replacement read the challenge prompt and solved it at 276 s. The real answer was structural: make every known solve plain code, so a refusal can delay a patch but can never stop the robot. A refused ticket goes straight to the fallback models.
Nothing from a challenge is ever run on my machine. Files are unpacked carefully, read and decoded, never executed; anything that truly has to run goes in a sandbox.
For the technical reader: size, ticket format, judge rules and trust boundaries
Size. The second-generation robot, v2, is 66,646 lines of Python: 40,608 in the robot, including 22,407 in lane solvers, and 24,439 lines of tests with 1,464 test functions.
Ticket. A JSON ticket written within 0.3 s from facts the solver already recorded: the question, the prompt, the file and its signature, capped facts, what was tried, the grader's verdicts, and a typed reason (no_reader, no_descriptor, decode_failed, field_unknown, value_rejected, ambiguous or other). Its work order names one output shape, an earlier example, where to write the patch and the command that judges it. Web Single Solves open their ticket beside the technique sweep, the robot's probe of the live target, and the agents read the sweep's live log instead of waiting 3 to 17 seconds for it to finish. On Day 30 that put both web tickets up at 6.2 and 10.4 s, where the sweeps ended at 12.1 and 22.8 s.
Judge sandbox. A patch is either data or a small code module that answers or defers. The judge runs it in a child process with no network, a cleared environment, a temporary filesystem, selected read-only inputs and one writable scratch directory, under per-process limits of 30 s wall clock, 120 s CPU and 8 GiB of address space, with a parent-death signal and cleanup of the whole process group. It passes only if:
- the patch is well formed, with no unfilled template slots;
- the ticket's question now gets an answer the grader has not rejected;
- across the lane's whole graded record, no grader-accepted answer moves;
- on the ticket's own challenge, any value it replaces was only ever rejected or unknown, and every such move is reported;
- for warmups, the predicted Mastery cases still pass;
- the question is not already accepted. A late but correct patch is refused as "no gain" and retargeted to the next mode's ticket, or carried forward.
That is a regression and containment guarantee, not proof that a new answer is right. With no recorded accepted value, the judge can show that the patch answers the ticket, avoids known-rejected values and keeps every earlier accepted answer.
Carry in detail. For live web targets, carried code is scoped to the target family, earns trust only after the grader accepts it on a later mode, and is demoted after repeated refusals. For file lanes, a derivation agent turns a saved hand method into a reusable patch. From Day 31 an accepted hand method is also wrapped into a patch mechanically and judged at once; on its first live morning all 8 wrappers were refused because the method did not reproduce the answer on the ticket's file, and the patch agents carried those lanes instead.
Two trust boundaries. The model and the patch are not isolated the same way. The model gets the ticket's facts and the artifact paths, and has repository tools governed by an allowlist and written instructions; it is not inside the patch sandbox. Only running its candidate patch is isolated. So prompt compliance and tool policy are controls outside the judge's operating-system boundary.
The artifact boundary. Archive extraction rejects absolute paths, parent traversal, backslashes, NULs, links and special files, and caps total size, member size, file count and compression ratio. The known limitation: the fallback archive tool runs directly under a 120 s timeout, and its output is only checked after extraction.
Refusal detection is a heuristic: the agent pool scans stop reasons, common refusal wording and a safeguard event, then requeues the ticket to the fallback models.
The team
One human, several models, each with a job. All the models are Anthropic's, on purpose, so the event's files stayed with one vendor while the robot was being built. A coding agent from a second vendor did one refactor on Oct 11, was then barred from the repo, and was let back in on Oct 29 for builder work.
Me: policy and risk
I set the rules, approved anything that spent a scarce ranked attempt or touched the live server outside the race, owned the scoreboard record, and supervised the morning race. Every standing decision went into the repo's instruction file with a date, so that file doubles as a decision log.
Claude Opus: the main loop
Did the engineering and explained it as it went. Owned the commits on a private repo and wrote a technique note for every challenge. I pinned that working style on purpose: the model does the work, and I come out knowing the technique.
Claude Fable: the advisor
A stronger reviewer, consulted before committing to an approach and again before calling work done. Opus made the final call after that review.
Race-time agents: Sonnet and Opus
The patch agents and hand agents from the morning above: three patch agents and an Opus hand agent per lane, plus a Sonnet hand agent on web and the spares, with short-lived derivation agents turning accepted hand methods into patches.
Refusal fallbacks: Mythos, then Opus 4.6
When a safety filter balks, the task goes to a Mythos-based analyst, then to an Opus 4.6 analyst. Both are pinned subagents, checked on their models on Oct 10.
Reviewers: fresh Opus instances
Independent, read-only reviewers for anything that runs while a clock is live, returning CONFIRMED or NOT CONFIRMED with MAJOR and MINOR findings. Early on the reviewers themselves were tested by planting bugs for them to find.
How it got better every day
The robot learned something every morning, but a lesson was only worth keeping if I could prove it made things better without breaking anything that already worked. That is where most of the engineering went. If someone (usually a model) claimed a fix was faster or more correct, there was a tool that measured it like for like. Of everything here, this is the part I would carry to any other project.
Replay against the grader
Every solver re-runs over every attempt ever recorded and is compared with the grader's own verdicts. A crash, a wrong first guess or a value the grader once rejected is a fail. From Day 25 an unanswered question fails too: any shape we have been shown must have a solver.
Answer-diff
Old code against new over every recorded file. The only answers allowed to change are the ones the change names in advance, so a fix cannot quietly move an answer somewhere else.
The commit gate
Nothing gets saved without passing replay, answer-diff, the unit tests and timing checks, run on 16 workers at once. By Day 28 the full gate covered 70 modules and took 521 s, about 8.7 minutes.
A fake Game Master
A local stand-in for the event server, for practice races. It plays back recorded days with their real delays and rate limits, throws scripted faults (server errors, dropped submits, slow downloads), and uses a load model fitted to Day 19's live trace. The rule: the old robot on the fake server had to match the old robot live, or the fake was not a fair benchmark.
Blind runs
A practice race where the robot knows only what it knew the night before that day. Every decoder, reader and vocabulary entry is tagged with the day it was learned, so a blind run can switch off everything learned since. The rule: a drill that leaks hindsight is invalid.
What would it have changed?
Two more drills: run the night-before code against a recorded morning and score each lane's first try, or drop a new fix onto every past night-before and ask whether it would have changed that morning. Day 26's blind before-and-after showed no measurable change (98 against 103 s mean), and it was reported as exactly that. Most claimed speed wins died this way, which was worth knowing.
Reviews scale with risk. Code that runs while a timed attempt is live gets one independent Opus reviewer. Solver changes get none, because replay and answer-diff against the grader are the check. On Oct 12 the review process itself was tested by committing deliberate bugs in a scratch copy and running them through every layer. Round one found three critical holes; later rounds caught 10 of 12, then 11 of 11.
None of this is exhaustive. Replay covers the variants our team received, and an unrecorded answer has no ground truth. The fake server is calibrated against selected live traces and does not reproduce every server delay or live target.
The story in five phases
The backlog sprint
The repo started at 23:49 on Oct 10, ten days into the event. Days 1 to 6 were solved and turned into solvers in one day: 167 commits on Oct 11. The habits that carried the whole month formed here: a solver per challenge, one shared engine per lane, replay against every graded answer, a measured rate limit, a commit gate.
v1 goes racing
The first live morning: 36th in 658.9 s, while the winner took 13.6 s. That stung, and it forced a pivot: lanes in parallel, the reviews and holds dropped, no model in the path. Day 16 was 8th, Day 17 1st (a slow field), Day 18 4th at 91.9 s against 45.5 s.
The rebuild
After Day 18 I froze v1 as the fallback and asked for a lean rebuild from scratch. A frozen skeleton went up at about 10:15. Around 25 components were built by parallel builder agents between 11:27 and 13:24, reviewed through the afternoon, and calibrated against recorded days by 16:00. 140 commits that day.
v2 and the web lane
Day 19 was a failure of my own making: the poller buried our own account in requests while hunting for the new web lane, and we placed 62nd. Day 23 placed 20th after a script batch collapsed. Then it clicked. Days 21, 22, 24, 25, 26 and 29 were outright wins, and Day 29's 72.5 s put us 1st overall.
The finish
Day 30 was 2nd by 6.9 s. Then SHA1-Hulud took 1 h 48 min by hand, and its 5 points re-sorted the tied field. On Day 31 five lanes were done by 76.9 s, and the last web challenge hid the one header that mattered for 228 s. 4th on the day, 4th overall.
Results
Days 1 to 13 were played after their release, as a backlog, so they have no race time. The race chart starts at Day 14, the first morning the robot ran live at 09:00. In the chart, the solid line is us and the dashed line is the day's winner, or the runner-up on days we won. Lower is better, and the scale is logarithmic, so the two big spikes are even worse than they look.
| Day | Our time | Place | Checks | What decided it |
|---|---|---|---|---|
| Day 14 | 658.9 s | 36th | 15 | First live morning. Model work before the first clock; metadata met a new LNK format. |
| Day 15 | 530.2 s | 16th | 15 | Binary and metadata went through lane agents; the warmups Mastery took three tries. |
| Day 16 | 230.7 s | 8th | 15 | Three agent lanes (Morse, email, WebAssembly) were the gap. |
| Day 17 | 674.7 s | 1st | 15 | A binary wording hunt on one field. The whole field was slow on that challenge, which the organizers later reissued. |
| Day 18 | 91.9 s | 4th | 15 | Warmups was the entire gap; four lanes were done by 43.6 s. |
| Day 19 | 7110.4 s | 62th | 24 | First v2 race and first web day. Our own poller load; web solved by hand. |
| Day 20 | 330.3 s | 7th | 21 | Web was the critical path; the file lanes were done by 45 s. |
| Day 21 | 213.4 s | 1st | 24 | Web was the critical path again; the first outright v2 win. |
| Day 22 | 89.8 s | 1st | 21 | A binary bridge challenge on the second binary racer was the critical path. |
| Day 23 | 1850.9 s | 20th | 24 | Script batch collapse: wrong answers labelled certain, expired attempts, a 429 storm. |
| Day 24 | 81.3 s | 1st | 21 | Web and warmups; an agent-written EVTX reader landed in 13 s. |
| Day 25 | 100.3 s | 1st | 24 | A warmups decode-tail miss on set 79. |
| Day 26 | 69.0 s | 1st | 21 | All 21 correct; every file lane done by 58.9 s, web last at 69.0 s. |
| Day 27 | 329.0 s | 4th | 24 | Web race condition: exploration spent a single-use target, losing a Time Trial and a Mastery attempt. |
| Day 28 | 262.0 s | 2nd | 21 | Binary hand answers carried values, not methods, so every fresh file reopened the same families. |
| Day 29 | 72.5 s | 1st | 21 | The second web Mastery was the last check, after a hand answer (about 17 s) and a judged patch (16 s). |
| Day 30 | 100.9 s | 2nd | 21 | Two new web techniques. Hand answers did not turn into code fast enough, so each web mode waited on another one. 2nd by 6.9 s. |
| Day 31 | 255.5 s | 4th | 21 | Framework Roulette: the header the app read its role from was named in a Vary response header, and our tools read bodies only. About 228 s on one Single Solve. |
| Lane | D14 | D15 | D16 | D17 | D18 | D19 | D20 | D21 | D22 | D23 | D24 | D25 | D26 | D27 | D28 | D29 | D30 | D31 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| warmups | 576.9 | 308.7 | 93.8 | 33.6 | 91.9 | 123.1 | 6.3 | 6.6 | 9 | 6.5 | 40 | 100.3 | 24.5 | 30 | 8.4 | 10.3 | 10.1 | 6.5 |
| metadata | 658.9 | 475.8 | 107.7 | 31.7 | 22.9 | 180.1 | 17.3 | 33.1 | 12.7 | 88 | 17.6 | 16.5 | 31 | 23.6 | 27.8 | 16.7 | 33.6 | 76.9 |
| script | 320.8 | 35.9 | 30.8 | 14.4 | 44.5 | 127.1 | 7.7 | 10.2 | 11.9 | 1,851 | 11 | 11 | 12.9 | 17.5 | 16 | 15.6 | 27.7 | 14 |
| binary | 364 | 530.2 | 230.7 | 674.7 | 19.2 | 293 | 44.8 | 43.1 | 89.8 | 34 | 26.1 | 20.4 | 46.5 | 32.7 | 262 | 27 | 81.1 | 72.2 |
| mobile | 305.8 | 28.4 | 22 | 26 | 6.8 | 121.8 | 6.6 | 6.2 | 7.9 | 78.2 | 32.9 | 6.4 | 58.9 | 7.3 | 9 | 20.1 | 25.8 | 7.1 |
| web | · | · | · | · | · | 7,110 | 330.3 | 213.4 | 62.7 | 112.6 | 81.3 | 89.7 | 69 | 329 | 88.3 | 72.5 | 100.9 | 255.5 |
Podium data
| Day | 1st | 2nd | 3rd |
|---|---|---|---|
| Day 2 | 0 | 1 | 0 |
| Day 3 | 0 | 1 | 0 |
| Day 4 | 0 | 0 | 4 |
| Day 6 | 0 | 0 | 3 |
| Day 7 | 0 | 1 | 2 |
| Day 8 | 0 | 0 | 1 |
| Day 9 | 0 | 0 | 2 |
| Day 10 | 0 | 0 | 1 |
| Day 13 | 3 | 2 | 0 |
| Day 15 | 0 | 0 | 3 |
| Day 16 | 0 | 1 | 0 |
| Day 17 | 0 | 1 | 0 |
| Day 20 | 0 | 1 | 0 |
| Day 21 | 0 | 1 | 2 |
| Day 23 | 2 | 5 | 4 |
| Day 24 | 2 | 6 | 1 |
| Day 25 | 3 | 4 | 4 |
| Day 26 | 3 | 5 | 2 |
| Day 27 | 0 | 2 | 1 |
| Day 28 | 0 | 1 | 2 |
| Day 29 | 3 | 0 | 1 |
| Day 30 | 1 | 1 | 0 |
| Day 31 | 3 | 3 | 1 |
Commit data
| Date | Commits |
|---|---|
| Oct 10 | 3 |
| Oct 11 | 167 |
| Oct 12 | 156 |
| Oct 13 | 138 |
| Oct 14 | 30 |
| Oct 15 | 27 |
| Oct 16 | 36 |
| Oct 17 | 60 |
| Oct 18 | 140 |
| Oct 19 | 60 |
| Oct 20 | 35 |
| Oct 21 | 11 |
| Oct 22 | 14 |
| Oct 23 | 18 |
| Oct 24 | 18 |
| Oct 25 | 15 |
| Oct 26 | 29 |
| Oct 27 | 29 |
| Oct 28 | 23 |
| Oct 29 | 15 |
| Oct 30 | 10 |
The six lanes
The example questions are invented to show the shape. They are not real challenge content or answers.
Warmups encodings
- Task
- A text blob in stacked encodings decodes to a sentence ending in a slug like
word-word-word-NN. - Example
- message produces amber-lantern-quartz-42
- Technique
- A layer search: the solver peels one encoding at a time and scores each result on how much it reads like English and whether it ends in the slug shape. Each decoder only runs when the text looks like its format, so the ones not needed cost nothing. Every Mastery line is classified and decoded on its own.
- Discovery
- On Oct 15 we worked out the generator: Day N's Mastery is its sentence cut into N lines, with line i in layer i of one growing list. Tomorrow is predictable except for one new layer, and a stress tool replays plausible next layers ahead of time.
- Curveball
- Day 14: Mastery 0 of 68 on its only ranked attempt, because a spaceless Caesar-7 fragment was never read.
Metadata forensics file formats
- Task
- A zip of evidence files. Each question asks for a structural fact, phrased
Determine <ns>.<field> for <file>. - Example
- Determine zip.eocd_total_entries for evidence-002.zip. Result: 3
- Technique
- Most formats are parsed directly. Standard-library container helpers are used where they keep the needed facts, and raw ZIP parsing where a zip's directory records can disagree with each other, the trick behind parser-differential attacks, where two programs read the same file differently.
- Lesson
- Answer in the form the producer stored, not a tidy reformat: GUIDs kept their braces, Message-IDs their angle brackets.
- Curveball
- A new format almost every day. Agent-written readers took 488 s on Day 11 and 13 s for EVTX on Day 24.
Script analysis malicious scripts
- Task
- A zip of specimen scripts, each hiding a descriptor. Questions ask for the role, the ATT&CK technique id, indicators and step graphs. There can be thousands: about 10,500 in one Mastery on Day 23.
- Example
- Which ATT&CK technique fits launcher-02.js? Result: T1059.007
- Technique
- Read the script's own
MaterialandCodeclabels ("read the label, do not guess from the bytes"), decode to a key=value descriptor, and reject decodes that do not parse. A cmd.exe expansion emulator handles batch. Nothing is executed. - Curveball
- Day 23: about 15% of answers wrong but labelled certain inside all-or-nothing batches. The worst morning on record.
Binary RE compiled programs
- Task
- Compiled programs from a new toolchain each day. Questions ask about container facts, symbols, sections, hashes, and the fields of an embedded configuration blob.
- Example
- Using the file in slot 2, what is config_domain?
- Technique
- Several early toolchains reused an encrypted, CRC-checked configuration format. Later days moved to unpacking, overlays, patch comparison, root-cause analysis and fault triage. Static readers pull structural facts; heavyweight decompilation is a last resort and never runs on the clock.
- Discovery
- Time Trials walk a fixed counter over slot variants, serving the file variants in the same order every time, and Mastery delivery D equals counter position D minus 1. So a position hedge sends answers predicted from files already seen alongside the download, before the files arrive; a wrong batch saves nothing, so it is free.
- Curveball
- Day 17: about 250 rejected wordings for one field before the organizers reissued the challenge with published rules.
Mobile Android APKs
- Task
- An APK whose manifest and code tell a story about a real Android weakness. The answer is an
ANS-plus 16-hex code behind a named codec in a bundled JSON asset. - Example
- codec: xor, key 29, data: base64 produces ANS-…
- Technique
- Treat the story as a decoy and undo the codec. The
ANS-pattern tells the solver when it has the right answer, which makes it a correctness check for a depth-two search over the codec registry (up to two codecs chained), followed by six bounded brute-force families. - Escalation
- From Day 23 the code had to be computed: a static dex bytecode interpreter, then a static x86-64 stepper for a native library, reflection, and split APKs.
Web live targets, Day 19 on
- Task
- A live site built on demand. Exploit the bug and submit a
proof_token. Mastery gives several targets at once, and a target dies a few minutes after start. - Technique
- Detect the technique by probing the page, not by trusting the challenge name. A toolkit covers fetch, forms, cookies, JWT, Flask sessions, access logs and packet captures. Requests outside the named host are refused in code.
- Curveball
- Day 31: the header the app read its role from was named only in a
Varyresponse header, and every tool read response bodies, never headers. It cost about 228 s and, by four-tenths of a second, the podium.
Across lanes, surprises fell into six types: answer wording for a known fact, a new question on a known format, a new Mastery shape or layer, a new file format, a new language or technique, and infrastructure faults. Wording problems hit the first day of a fact. New capability hit metadata and script almost daily. Shape problems hit Mastery first, because Mastery combines shapes the Time Trial does not show.
What went wrong
I wanted the failures in here, because most of the robot's rules trace back to one of them. Four mornings did most of the teaching.
Day 14, the first live morning: 36th. The robot took 658.9 s; the winner took 13.6. We spent 97 s on model work before the first clock even started, a warmups detour held up three lanes that were ready to go, and a new shortcut-file format sat waiting on the main session. The fix was the whole idea on this page: every lane in parallel, known shapes as plain code, no model in the path.
Day 19, the first web day: 62nd, and entirely my own doing. The poller expected the web challenge under Day 19, but web numbered its own days. So it kept asking for 66 minutes, 23,383 calls, two thirds of them resends, and the server started holding our account 20 to 50 seconds per call. The file lanes were done by 293 s. Web was solved by hand at 7,110 s.
Day 23, the worst morning on record: 20th. About 15% of the script answers were wrong but marked as certain, so they rode inside all-or-nothing batches and sank every good answer with them. Resends burned the clock, more than fourteen ranked attempts expired, and the script lane collected over 2,500 rate-limit refusals. Afterwards, one batch first, then smaller packs by question type, and nothing resent after a deadline.
Day 31, the one that cost the podium: 4th by four-tenths of a second. Five lanes were done by 76.9 s. The last web challenge, Framework Roulette, named the header it read its role from only in a Vary response header, and every tool the robot had read the body of a response, never its headers. About 228 of our 255.5 s went on that one Single Solve. The fix is small: read the headers too. It came one day too late.
And one that was not a bug at all: on Day 30, SHA1-Hulud, a classic crackme with no questions to open a ticket on, took 1 h 48 min by hand, and its 5 points dropped us from 1st overall to 10th that afternoon. Some things you just solve.
For the technical reader: the full incident log, 26 entries with root causes and what changed
| Date | What happened | Root cause | What changed |
|---|---|---|---|
| Oct 11 | Script Day 3 Mastery came back CORRECT by luck. | File lookup fell through to a global search, so set 2 was answered from set 1's file. | Named nested zips resolve first. Lookup raises rather than guesses. |
| Oct 11 | Four rate-limit storms, up to 192 refusals in a minute. | Five agents plus discovery on one identity; a rejected batch fanned out into singles, each retrying alone. | Limit measured at about 100 per calendar minute. A shared governor; rejected batches split into 8, not singles. |
| Oct 12 | Day 8 started without my go. | An announced plan was treated as approved. | Rule: an announced plan I have not answered is not a go. |
| Oct 12 | Five refusals at the 09:00 release with only 13 to 15 calls in our minute. A Time Trial expired unsolved. | Server side, later confirmed as a configuration issue. | A release-window hold was added, then removed the next day. |
| Oct 12 | Round 4 of the commit gate's review: 1 critical and 6 major findings. | An empty candidate list ended the submitter, among others. | One fix per commit, re-reviewed until CONFIRMED. |
| Oct 13 | The binary answer cache was keyed, in effect, on the filename. | AES zips store CRC 0, so 1,079 of 1,153 members had the same "checksum". | Keyed on decrypted content and question text, with a fallback on rejection. A leave-one-out test, hiding each answer from the cache in turn: 8,180 of 9,151 served, 0 wrong. |
| Oct 13 | Day 13 rehearsal: every check correct, but 65 minutes release to last Mastery. | Slow lookups, 11 minutes of commits mid-run, 52 unneeded coverage runs, a false speed hold, and two lanes deadlocked on the commit gate. | Live days commit once, at day close. Eleven proposals built overnight. |
| Oct 14 | A position-hedge batch of 300 answers came back incorrect. | A second Mastery on the same challenge carried a different delivery-3 file. | Hedge restricted to a challenge's first Mastery, later to exact file matches only. |
| Oct 14 | First live morning: 36th, 658.9 s. The winner took 13.6 s. | 97 s of model work before the first clock; a warmups detour held three ready lanes; a new LNK format waited on the main session. | The release race: parallel lanes, deterministic handling for known shapes, holds and readiness reviews dropped. |
| Oct 14 | Four Mastery retries refused with HTTP 503 retry_rotation_invalid. | Server side; the first such refusal in 110 Mastery retries. | A 5xx is status-checked, never read as a solver miss. |
| Oct 15 | Warmups Mastery 0 of 73, then 72 of 73, then CORRECT on the third try. | A Caesar-7 fragment read as ROT13. | The deep walk tries Caesar shifts; a shift named in the title is promoted; a recipe stress tool. |
| Oct 17 | Binary took 532 s on one field, about 250 rejected wordings. | A value the file could not hold. The gate also refused a rule that listed known-rejected words first. | "Derive first, then guess wide at once." The organizers reissued the challenge. |
| Oct 18 | The early start for the binary lane never fired live. | The server refuses a lane filter for binary; the mock stalled lane lists too, so no rehearsal showed it. | Use a search query. Rule: probe any new server parameter live through the real code path. |
| Oct 18 | A test process had pinned a CPU core for 38 hours. | A detached child survived its dying parent. | Parent-death signals on every spawned process. |
| Oct 19 | Day 19, the first web day: 62nd. File lanes done by 293 s; web solved by hand at 7,110 s. | The poller expected web under day 19, but web numbers its own days. It listed for 66 minutes (23,383 calls, two thirds of them resends), and the service held our identity 20 to 50 s per call. | Fast signals became single shots; a slow full-list poll stays for late challenges. Web is grouped by each entry's own day field. |
| Oct 20 | Three web agents and the Mythos fallback all refused. | Safety classifiers on exploit writing. | Agent prompt: read the ticket's prompt first and use every path it names. Solved at 276 s. |
| Oct 23 | Day 23: 20th, 1,851 s. Fourteen-plus ranked attempts expired, and the script lane recorded over 2,500 rate-limit refusals. | About 15% of script answers were wrong but marked certain inside all-or-nothing batches; halving resends burned the clock; the judge refused repair patches; the watcher acted 8 minutes late. | One batch first, then packs split by question family; no resend after expiry; late sends recorded as unknown; a coverage gate and scoped watcher tools. |
| Oct 25 | Warmups set 79 missed on the clock. | The decode ended in an extra number. An earlier "helpful" change sent the slug alone, and the grader rejected it. | Rule: if the file decodes cleanly, send exactly what it says first. |
| Oct 25 | The binary judge drifted to 27 to 31 s against its 30 s wall. | Speed-run recordings bloated the corpus. | 302 recordings archived (judge back to 12.9 s); speed runs record elsewhere. |
| Oct 27 | A web target's single-use state was spent during exploration. One Time Trial and one Mastery attempt were lost. | The generic sweep wrote before it knew which action was destructive. Correct patches were then judged against a spent target. | Status routes run first and initial writes go as one concurrent group. Accepted evidence from another attempt of the same challenge can support a scoped live patch. |
| Oct 27 | A synthetic-clock drill looped through 720 retries before it was stopped. | A free retry reconnects to the same open attempt, so the drill never got the fresh state it assumed. | The drill uses one target reset as a fresh-attempt stand-in. |
| Oct 28 | Binary finished at 262.0 s despite an early Single Solve. | Hand answers carried values but not methods, so every fresh file reopened the same families. | Accepted hand methods start derivation agents whose judged patches carry into later modes. |
| Oct 28 | An experimental family-agent system launched 46 concurrent judges. | Covered judges took a median 23.7 s, and 21 of 156 failed after promotion removed a baseline they were still reading. | Family agents off by default. Superseded baselines survive 210 s, and already-covered work returns without a sandbox run. |
| Oct 30 | Day 30: 2nd by 6.9 s. | Two new web techniques. Each web mode waited on a fresh hand answer, because the accepted ones did not turn into code fast enough. | An accepted hand method is wrapped into a patch at once and judged like any other. |
| Oct 30 | SHA1-Hulud took 1 h 48 min, and its 5 points dropped us from 1st overall to 10th that afternoon. | A standard crackme with no questions, just a flag field, so no ticket ever opened. It was solved by hand. | No new code: a one-off, by design never automated. |
| Oct 31 | Day 31: 4th by four-tenths of a second, with about 228 of our 255.5 s on one Single Solve. | Framework Roulette named its role header only in a Vary response header. The probe tool and the web engine read bodies, never headers. | The fix is small: read Vary and any custom response headers, and try a ladder of role values on whatever they name. It came one day too late. |
Rules that changed
Every decision I made went into the instruction file with a date, and about two dozen of them were later reversed. Read in order, the reversals tell one story: the goal moving from "never waste a scarce attempt" to "seconds to all checks correct". A few of the big ones:
- One clock at a time (Oct 11) became parallel clocks in a release race (Oct 14).
- Protect a correct timed Mastery and prepare until nothing new shows up (Oct 12) became: a Mastery with more than 3 attempts goes as soon as possible (Oct 14), because a miss costs one attempt and nothing on the board.
- Review everything (Oct 12) became review in proportion to risk (Oct 13), because a day must run from Single Solve to Mastery without waiting on reviews.
- A self-imposed cap of 80 calls a minute became no cap in a race at all (Oct 14), only a guard against a runaway loop: never hold answers back on a guessed limit.
- v1 is the robot became v1 frozen as the manual fallback and v2 built from scratch (Oct 18), after Day 18's 92 s against a 45 s winner.
For the technical reader: all dated policy changes, in order
| Date | Rule | Became | Why |
|---|---|---|---|
| Oct 10 | The event guide's "never solve without being asked" (written for a coaching bot) | Claude does the work and explains while working | I wanted a robot and an education, not coaching |
| Oct 11 to 12 | Stable after 3 clean runs, then 5 | Stable after 5 runs that bring nothing new | "A count of clean runs was the wrong bar" |
| Oct 11 | One clock at a time, in silence | Parallel clocks in a release race (Oct 14) | Parallel for speed |
| Oct 11 | Practice "completes the mode, points unverified" | A practice completion earns the check mark (Oct 14) | My board showed it |
| Oct 12 | No ranked clock 08:45 to 09:15 | Removed Oct 13 | The 09:00 throttle was a server issue; start-to-finish time decides Day 31 |
| Oct 12 | Protect a correct timed Mastery; saturate first | A Mastery pool above 3 goes as soon as possible (Oct 14) | A miss costs one attempt and nothing on the board |
| Oct 12 | A 600 ms speed hold before Mastery | A per-challenge hold, then dropped on release runs | No single time fits every challenge |
| Oct 12 | Review everything (five gate rounds, 13 per-fix reviewers) | Review in proportion to risk (Oct 13) | "A day must run from Single Solve to Mastery without waiting on reviews" |
| Oct 13 | Readiness review before a day's first timed run | Dropped on release runs (Oct 14) | Known shapes must start without a review wait |
| Oct 14 | Commit fixes as they land | Live days commit once, at day close | Commits cost clock time and caused the Day 13 deadlock |
| Oct 14 | A cap of 80 calls per sliding minute | No self-imposed cap in a race, runaway guard only | "Never hold answers back on a guessed limit" |
| Oct 14 | Board record hand-pasted by me | Generated from the public API (Oct 24) | The API became available; never hand-edit |
| Oct 15 | Timing check blocks a commit | Advisory | "Let's not be so strict" |
| Oct 17 | A non-derivable answer stops the lane as a possible challenge bug | Report it and guess wide at once, no cap, never wait | A guess costs nothing; waiting costs the race |
| Oct 17 | Describe every post-run change and wait for my veto | Changes with no new blockers land without a veto | Speed of iteration between days |
| Oct 18 | v1 is the robot | v1 frozen as the manual fallback; v2 built from scratch | Day 18 was 92 s against a 45 s winner |
| Oct 26 | Every finished attempt enters the corpus | Speed-only runs write elsewhere | Speed recordings pushed the binary judge to 27 to 31 s |
| Oct 27 | A live web patch proves itself only on its own ticket | Any accepted attempt of the same challenge can supply evidence, with target-family scope and later grader trust | A correct patch was blocked by a spent target |
| Oct 27 | Build an early reset for a spent ranked target | Parked, not built | "Beyond what is necessary; other unseen risks rank higher" |
| Oct 28 | A hand answer carries only its value | Save the method and start a derivation agent after acceptance | Values did not solve fresh files in later modes |
| Oct 28 | Start a code-agent pair for every open family | Off by default | Concurrent judging overloaded the shared baseline |
| Oct 29 | Land the agent-director system if built | Parked, never landed or run | Controlled drills did not justify it |
| Oct 29 | A Single-Solve-only extra challenge can take the binary lane | The real binary challenge keeps its lane; the one-off goes to a spare | The regular binary race must not be displaced |
| Oct 30 | A derivation agent turns hand work into a patch | The accepted method is wrapped into a patch at once, the deriver kept as fallback | Day 30's web modes each waited on a fresh hand answer |
In my own words
"we're in 35th after that attempt this morning. We need to adjust in order to make ourselves faster to the finish line, it's what will decide the entire competition on the 31"Oct 14, after the first live morning
"If no model had been in the path, 9:01 is possible: this is what we're striving for."Oct 14
"every day is basically just a race to solve at this point and it will remain that way through till the end ... there is no dynamic scoring ... everyone will be keeping the same score till the end."Oct 15, evening
"we should still prepare and practice for curveballs since I would expect the last day to include some. It's better that we practice and prepare beforehand on the days leading up rather than get caught slow and lose the whole comp."Oct 15, evening
"the head start wins the comp"Oct 15, on racing warmups before the day list appears
"race both ways, keep the first"Oct 28, on racing a code-first hand agent beside the value hand agent
"We must get this solve. So if we have to keep going, we keep going. If we don't solve this, we can't win at all."Oct 30, 10:25, inside SHA1-Hulud
"If we found that header, how much time would we have saved, roughly?"Oct 31, 09:48. Roughly two and a half minutes.
What carries over
Deterministic first, AI at the edges
Every known shape became plain code, so a refusal, a slow model or a bad prompt can delay a patch but never stop the robot. The models earn their keep on the shapes nobody has seen yet.
A narrow handoff beats a big prompt
A typed ticket with capped facts, a reason and a single output shape got agent ticket-to-patch time from minutes in v1 down to 6 to 40 s.
Judge the patch, not the model
Model-written code goes live only after sandboxed replay against every answer the grader has ever accepted. That makes it safe to let agents patch a running race.
No hindsight in the drills
Knowledge days, hold-outs, counterfactuals and a calibrated mock, with one rule over all of them: a drill that leaks hindsight is invalid. Most "speed wins" did not survive that test, and that was worth knowing.
Rules built for scarcity have to go
Two dozen dated rule changes trace the objective moving from protecting scarce attempts to raw seconds. Writing every decision down with a date made those reversals cheap.
Read what the machine was told
The robot read everything the author meant for a player: files, bodies, prompts, routes. The last curveball was in the part of the response meant for machines. Next time, the headers get read too.
Sources
Everything here comes from the project's private repository: the instruction file and its dated decisions, the daily plan addenda, the design and contract documents, the session log, technique notes, lane playbooks, review files, git history, and the leaderboard record generated from the public scoreboard API. Line and test counts are from wc and grep, not from running the suites, and agent and judge timings come from the robot's own traces.
A few things are worth knowing when reading the numbers. Day 19's board time is 7,110.4 s because the web lane was solved by hand late; the slowest file lane finished at 293 s. Day 17's 1st place was at 674.7 s against 985.0 s, on a challenge the organizers later reissued. Speed-board counts change as other teams improve their best runs. The agent-director system exists only in parked work and was never part of the racing robot.
Left out on purpose: flags, the team token, challenge artifacts and answer values, and the details of the private infrastructure the robot runs on.
The day-by-day story is in the diary.