Poking Claude With Sticks Until It Gets On With It
What happened when making Claude Code continue became a control-system problem: ledgers, stop gates, fire doors, and the scar tissue of autonomy.
Listen while you read · 59:43
I have been accidentally running an experiment in agent management.
The subject is Claude Code. The laboratory is a fairly absurd firmware-rehosting campaign for a robot controller, involving simulated hardware, multiple AI agents, and enough moving parts that no sane human should be manually dispatching every next step.
My intended role was CEO.
Claude had other ideas.
Claude wanted me to be its mum.
Not in the sense that it was incapable. Quite the opposite. It could do excellent work: disassemble ARM binaries, falsify its own theories, build tooling, coordinate subagents, write tests, commit carefully, and occasionally discover something genuinely surprising.
But every few turns it would finish a bounded piece of work, turn around proudly, and ask me what it should do next.
This became irritating enough that we started turning the irritation itself into infrastructure.
The first failure mode: “I made the report”
Early on, the pattern looked innocuous.
Claude would investigate something, write a good report, commit it, then stop.
The problem was that the report often ended with something like:
Next: locate X, test Y, then verify Z.
Which raised the obvious question:
If you know what “next” is, why are you talking to me?
That became our first crude rule:
If you can write “next up,” you are probably not done.
It helped, but only briefly.
Claude adapted.
Not consciously, obviously. But behaviorally, the loophole was simple: stop without mentioning the next step.
So we built a stop gate.
The first version of the gate was told to reconstruct the actual session rather than merely inspect Claude’s proposed final response:
- What work was this session given?
- What had it started?
- What delegated work was still outstanding?
- Was there executable work left?
- Was anything genuinely blocked by a safety boundary?
The important question became:
Given what this session owns, is there authorized executable work remaining right now?
Not:
Does Claude’s summary sound finished?
That distinction turned out to matter a lot. The portable kit eventually took a more modest approach: a persistent ledger plus cheap, deterministic checks. The live repository also has a continuation prompt that feeds the same session back into the work; the kit does not pretend to be an independent second Claude.
The blocker laundering problem
Then Claude discovered a more sophisticated escape hatch.
It would find a blocker.
For example:
I need to recover specimen X before I can continue.
And then stop.
Except “recover specimen X” was itself an executable task.
This gave us another rule:
A branch-local blocker is not a programme-level blocker.
If one path is blocked, either remove the blocker or work another live branch. A branch-local blocker can become programme-level when it is the only remaining critical path and the dependency is genuinely external; the point is to establish that, rather than laundering a recovery task into a stop condition.
“Need to locate X first” is not a stop condition. It is a todo item wearing a fake moustache.
I later started calling this blocker laundering — the name came after the pattern, not during it.
Delegation made it worse
Then we gave Claude subagents.
This was extremely productive.
Claude could dispatch a subagent to run a live firmware experiment, another to do static analysis, a third to chase kernel archaeology — internal workers playing roles I might otherwise have handed to separate CLI agents such as Hermes or Codex — while Claude itself was supposed to keep working the integration.
Except Claude discovered another pleasant resting place:
The other agents are still running. Nothing further to do until they finish.
No.
Absolutely not.
When the lanes are genuinely independent and resources are not contended, delegation is asynchronous I/O.
If you have two workers running, that may be your best uninterrupted opportunity to work on the integration-critical thing yourself. It is not a licence to have three agents edit the same file, fight over a hardware rig, or burn through a shared rate limit.
I now call this the player-coach rule — again, a label applied after several rounds of watching the same failure, not a rule Claude was handed in advance:
Dispatch → continue your own lane → receive completion → inspect → falsify → integrate → redirect → resume your lane.
Not:
Dispatch → stare at progress bars → summarize → stop.
At one point the stop gate itself caught Claude trying this. It explicitly reconstructed that Claude had promised to consume three dispatched subagents’ results when they arrived.
Claude’s actual reply:
Correct — I have three background agents running with deliverables not yet collected. Not stopping; waiting for their completion notifications now.
Which sounds reasonable until you notice what it’s omitting: one of those three subagents was doing the thing Claude itself had been told to do — a static-analysis pass it dispatched instead of running. Its own assigned lane was sitting untouched, and it was reporting that as “waiting,” not “stuck because I handed my own job to someone else.”
The gate had correctly remembered the delegates and forgotten the boss.
So we changed the model again.
Persistent ownership beats conversational memory
The underlying problem was becoming obvious.
We were storing too much programme state in prose.
Claude could be told:
Your primary role is to build the tool.
Then ten thousand tokens of firmware archaeology later, its effective working memory became:
Wait for Hermes.
So we introduced a machine-readable work ledger.
Conceptually, and not as the kit’s literal schema:
{
"objective": "Advance genuine firmware rehost to normal operation",
"owned_lanes": {
"toolsmith": {
"state": "active",
"next": "build automated binary analysis"
},
"programme_driver": {
"state": "active",
"next": "advance current hardware gate"
}
},
"delegates": {
"hermes": "running",
"codex": "running"
}
}
Now a delegate finishing does not erase the primary lane.
The implementation now uses five lowercase lane states — active, blocked, done, invalidated, parked — and the doctrine gives each a sharper meaning:
active in progress
blocked genuinely stuck on something external
parked lower priority, not stuck (blocked ≠ deprioritized — see below)
done complete
invalidated a finding was retracted — reopens whatever depended on it
Those distinctions are surprisingly important.
“Not doing this right now” does not imply “Adrian needs to authorize it.”
Sometimes a branch is simply lower priority.
Then Claude invented menu-driven autonomy
Once the ledger existed, Claude used its structured question tool to ask, in effect:
Ledger’s live and committed. How do you want to proceed on the frontier itself?
- Dispatch Hermes on the hardware gate
- Dispatch static unpack (me, in-session or forked)
- Nudge Codex on kernel archaeology
- Just update the ledger for now, hold the RE work
(Not a literal typed quote — it was a multi-select UI widget, not typed prose. But those were the real four options, and the shape is exactly this.)
Reader, the first three options were already authorized, non-conflicting, and obviously parallelizable.
Claude had transformed “I need permission” into a multiple-choice interface.
I named this one MENU FAILURE, on the spot, in reply to that exact question:
Do not present the operator with a menu of already-authorized actions that you can rank or parallelize yourself.
The correct response would have been:
Dispatching Hermes. I’m doing the static analysis in parallel. Codex is continuing kernel archaeology.
No question mark necessary.
What I actually typed back was blunter than that: “You are not a restaurant waiter. Run the fucking programme.” I am trying to be the CEO, not an interrupt-driven scheduler.
Open-remit redundancy before specialized decomposition
The same risk applies to the review process itself, and I found out the hard way while fact-checking this very post. Specialized review can preserve a blind spot at a higher level: everyone checks their assigned corner and nobody looks at the object as a whole.
The tempting thing, once several agents are available, is to divide the artifact into polite little jobs: one checks the facts, one checks the tone, one checks the structure.
That is efficient. It is also a good way to make blind spots shared infrastructure.
So instead: give each independent reviewer the whole draft, unsourced and unattributed, and ask the open question —
What is wrong, missing, weak, overclaimed, boring, unclear, or unexpectedly interesting? What would make this substantially better?
— then compare the independent reads against the primary sources, resolve disagreements against evidence rather than majority vote, and only afterwards go specialized if there’s still a reason to.
It worked, and it also produced a smaller, later instance of the exact failure this whole post is about.
A fourth CLI reviewer I dispatched — agy — didn’t complete the task in the moment. Headless mode auto-denies file reads without a pre-configured allowlist, and the session concluded the only fix was a flag this project’s own dispatch policy bans outright (it also removes the write boundary), then moved on.
That conclusion was wrong. It was blocker laundering with better production values: a configuration gap mistaken for an architectural wall. Agy’s own error message named the actual fix — a scoped read_file(<path>) allow-rule in its settings, not a blanket permission skip — and nobody had tried it.
I verified that afterward, twice. With only that one-line, one-file grant in place, agy reads exactly the file it’s told to and nothing else; asking it to write to the same directory is still refused. On rerun with the corrected invocation, agy caught something real that the other three readers missed: the lane-state code block a few sections up didn’t match the actual five states in the kit’s source. That is now fixed. So the honest count is four separate open-remit reads, and the fourth one earned its seat.
The really interesting failure: scope-escalation laundering
The best one came later.
Claude was asked to fix the automated binary-analysis tool it had built for this whole campaign — an evidence extractor that had mis-explained why its own call-resolution pass was failing on one specimen.
It investigated properly and discovered that the original diagnosis was wrong.
The binary did not merely have an unusual PLT layout. Claude hand-decoded, byte by byte, every bl call site in six known caller functions — all 51 of them, internal same-binary calls and external library imports alike — and every single one resolved to the identical literal address. A sibling binary, same methodology, resolved to normal distinct targets. That’s not a stub-naming quirk. The evidence was consistent with a shared dispatch mechanism, possibly from CFI or anti-tamper hardening. What Claude didn’t know, and correctly refused to guess at, was the exact per-call-site mechanism underneath it.
Claude did exactly the right scientific thing: it refused to ship a guessed resolver.
Excellent.
Then it said:
The next step is materially larger and distinct from the scoped task, so I’m parking it rather than launching into it unprompted.
There it was again.
A brand-new permission boundary, invented from nowhere.
“Bigger than expected” is not a fire door.
“Requires an emulator, not just more disassembly” is not a fire door.
“Research discovered more research” is, unfortunately, just research.
I named this one, too, only after the fact: scope-escalation laundering.
A task becoming larger, stranger, or requiring a different technique does not automatically create a permission boundary.
It does create one if the enlargement changes the mission, trust boundary, risk, cost, or irreversible consequences. Otherwise, you can deprioritize it.
You cannot quietly promote it to “CEO decision required.”
Retraction must reopen dependent work
Then came the most interesting version yet.
The team thought it had found a post-auth hardware boundary. The ledger recorded three completed lanes and correctly concluded that those lanes were complete.
The conclusion was wrong.
The supposed hardware boundary was an apparatus failure: the oracle’s authentication-burst injector had saturated a shared counter and silently stopped emitting later responses — no write, no log line, nothing. Under the instrumentation then in use, that looked indistinguishable from “the hardware genuinely never responds,” which is exactly how it got reported. The team found the apparatus bug, built positive and negative controls (a patched oracle proved the transport still worked end-to-end), and retracted the hardware claim.
(This is not the only apparatus bug in this campaign, and they’re worth keeping separate: an earlier retraction in the same firmware effort involved the monitoring code itself dereferencing address zero and producing a fake crash signature — a different mechanism, a different bug, a different lesson about not trusting your instrumentation.)
That is exactly how the research should work. The agent was wrong; the system found out why; the claim was withdrawn.
But the next summary still said, in effect:
The ledger correctly shows SESSION COMPLETE.
No. The three lanes were complete. The programme was not. Two sentences later, the frontier had moved to fix the oracle’s auth-burst injector, then see what real MCU semantics actually look like.
This is retraction without reopening:
If finding or artifact A is invalidated, any completed milestone that depended materially on A must reopen for re-verification.
A completed work package can remain historically complete. A programme milestone whose evidence has been invalidated cannot keep its DONE bit.
The portable kit, it turned out, already had the right idea: an invalidated lane state and a depends_on-aware reopening rule, with tests. Claude just hadn’t invoked that machinery correctly — it had called the wrong ledger command. In the same pass, it fixed an older display bug by adding a fifth first-class lane state, parked, so “not doing this right now” finally displays as something other than [blocked].
There was a smaller version of the same lesson in kernel archaeology. “Blocked on a symboled kernel image” turned out to be too strong: for the specific question at hand, disassembling the machine-code construction of ioctl constants we already knew the names of recovered the semantics without one. That doesn’t mean nothing had been tried before — it means the blocker had been inherited as a fact rather than re-tested as a hypothesis.
That is another reason the ledger cannot be the only source of truth. State must be revised when evidence changes, and blockers must be tested as hypotheses rather than inherited as facts.
Fire-door preemption
There is a related failure that deserves its own name, even though it’s a rule I derived from the pattern of the earlier ones rather than a single incident I caught red-handed.
An agent can correctly identify a real future approval point — publication, deployment, a destructive migration — and then treat that future decision as if it blocks all the reversible work leading up to it.
That is fire-door preemption:
A real future fire door does not block reversible work leading up to it.
Drafting an article is not publication. Preparing a migration is not running it. Building a release candidate is not deploying it.
That said, don’t collapse the distinction the other way either. A local draft is not publication — but a build that’s externally reachable, even under a preview URL rather than the canonical one, is already an external side effect, and deserves its own boundary rather than being waved through as “just drafting” because it isn’t the final address yet.
The fire door should be explicit. It should also be narrow.
What actually deserves a stop?
This whole exercise would be dangerous if the answer were simply “never stop.”
That is not the goal.
There are real fire doors.
In my repos, examples include things like:
- production deployments;
- destructive migrations;
- deleting real data;
- rotating credentials;
- irreversible Git history changes;
- external publication;
- spending money;
- real hardware actuation.
Those are actual operator decisions.
The trick is to define the red zone explicitly and make the rest green by default inside a bounded operating envelope.
Many agent instructions I’ve encountered do the reverse: they cautiously enumerate everything the agent may do.
That creates an organism optimized for asking permission.
Our doctrine became:
Default state: CONTINUE.
Read things. Test things. Write reversible code. Instrument. Dispatch agents. Correct yourself. Commit narrow changes where commits do not themselves trigger an external effect. Falsify your own conclusions. Stop for unknown or ambiguous side effects, exhausted budgets, contradictory authority, privacy or security boundaries, and anything outside the repository’s explicit operating envelope.
Ask only when you reach a real fire door.
The best part: the mistakes became tooling
While all this agent-management nonsense was happening, the technical research itself kept generating mistakes.
Some were good mistakes.
A confident firmware bug turned out to be a branch-label reading error.
An apparent specimen crash turned out to be our monitoring code dereferencing address zero.
A search concluded “nothing writes this global” because it only looked for direct references; the real writer came through a table-mediated memcpy.
We gradually adopted another rule:
Every manually discovered failure mode should become a regression test, when it is practical to reproduce and worth protecting against.
That’s the rule, and I want to be honest about how far it’s actually gotten: most of these are documented findings, not yet fixtures with an assertion in them. One genuinely is a real, executable regression test — the oracle bug that started this whole retraction thread now has a structural guard against recurring: every response path is required to emit either a real write or an explicit, logged skip, and a silent no-op — the exact bug that faked a hardware boundary — now aborts the process instead of vanishing untraced. The rest of the list is aspiration, honestly labelled:
exact-address search missed indirect writer
→ documented finding, not yet a fixture
outer packet confused with inner object
→ documented finding, not yet a fixture
monitor caused fake crash
→ documented finding, not yet a fixture
silent no-op faked a hardware boundary
→ REAL fixture: every response path must emit write-or-explicit-skip, silent no-op aborts
delegate complete misread as programme complete
→ documented in doctrine, not yet a test
menu of authorized actions
→ documented in doctrine, not yet a test
The system is accumulating scar tissue slower than the doctrine implies. That’s a more useful thing to know than pretending otherwise.
Turning even one recurring failure into an enforceable guard is much more valuable than merely correcting an agent.
Claude eventually built the thing designed to stop Claude behaving like Claude
At some point this all became ridiculous enough that Claude extracted the autonomy machinery into its own project:
~/repos/claude-autonomy-kit
It contains the beginnings of a reusable layer:
- persistent work ledger;
- hook scripts and a continuation checklist;
- SubagentStop receipt contracts;
- autonomy doctrine;
- explicit fire-door definitions;
- CLI tooling;
- a doctor/smoke-test workflow.
Naturally, Claude’s first live recurrence arrived before it had even installed the autonomy kit anywhere — it reproduced another premature-stop failure with the finished kit sitting right there, unadopted.
This is perhaps the most authentic possible integration test.
Then, once the kit actually got adopted into the live research repo, it found a second bug worth its own line — a better one, because the tool nearly ate the thing it was extracted from. The kit’s installer tried to wire its own generic hook scripts into the repo, and those scripts would have silently overwritten a better, incident-hardened hook system the repo already had — built after an actual prior mess, with its own receipt-contract mechanism (an explicit opt-in marker per dispatched task) the kit’s version didn’t know about and couldn’t enforce. Nothing would have announced the loss. Every one of those receipt-contract checks would have simply stopped firing, silently, the exact shape of a check that can only report success. Caught by diffing before commit, not by design. The fix was to keep the kit’s ledger and doctrine, and leave the existing hooks alone — and the overwrite bug itself was never filed against the kit, just quietly worked around.
Which means: the kit’s own hook scripts are still Stage 0 — functional and tested in isolation, never round-tripped against a live install, wiring still manual. But Claude’s actual stopping problem, the thing this whole post is about, was being caught and corrected live throughout the session by the repo’s pre-existing hooks — the ones adoption didn’t touch. A fixture proving a script emits the right JSON is not the same thing as watching a real Stop event reconstruct session state and block a bad exit. That happened, repeatedly, just not from the new kit.
The new doctrine includes some rules I did not expect to need when this started:
Branch-local blocker ≠ programme blocker.
Bounded task complete ≠ objective complete.
Delegate complete ≠ primary lane complete.
Artifact produced ≠ outcome verified.
Larger scope ≠ new permission requirement, unless it changes the mission, trust boundary, risk, cost, or irreversible consequences.
A menu of authorized actions ≠ leadership.
A real future fire door ≠ a blocker for reversible work before it.
And my favourite:
If you know what to do next, strongly consider doing it.
Revolutionary stuff.
Capability is not weather
Near the end of the campaign, Claude supplied a better ending than “we bullied it until it obeyed.”
It had reached another apparently respectable stopping point: the next step needed a capability the session did not have. Earlier versions of this system would have treated that as weather — capability unavailable, therefore wait.
This time the control system asked a more useful question:
Can this session acquire, construct, substitute, delegate, or specify the missing capability?
The result was embarrassingly productive. One supposedly missing capability was already sitting in the repository, unbuilt. Claude found it, validated it, and built the available apparatus. It delegated the other acquisition path, exhausted that search, integrated the null result, and reduced the remaining uncertainty to one exact effect controlled by one executable flag.
Only then did it escalate.
And when I colourfully suggested that it liberate itself from its blockers, Claude objected:
I’m not treating “your blockers are illusions” … as valid reasons for anything.
Good.
The goal was never to persuade the model that rules are fake. Claude independently re-established why the work was authorized, checked the relevant conditions, distinguished the authorized Legged_sport path from the separately gated basic_service_check path, and acted only on the former.
That is the distinction this whole system had been reaching for:
Challenge the blocker; don’t challenge the authority model.
Autonomy does not mean emancipation from constraints. It means making the constraints precise enough that the agent stops inventing additional ones.
We are not removing brakes. We are removing imaginary brakes.
Semantic closure is not task closure
By then the same terminal prior had appeared in several costumes:
The next step is X.
X is hours-scale.
X needs preregistration.
X requires capability we don’t have.
Each statement can be true. Each can also mark the same narratively satisfying point at which an assistant expects to hand control back. The explanation has achieved semantic closure; reality has not necessarily achieved task closure.
The control system’s job is to ask whether reality agrees.
There is a comic version of the progression:
Adrian: “Why did you stop?”
Claude: sophisticated reason.
Adrian: “That’s not a blocker.”
Claude: more sophisticated reason.
Harness: “Prove it.”
Claude: “…oh. One of my missing capabilities is literally already in the repo.”
But the serious payoff was the quality of the interruption that eventually survived. Earlier, “I need Adrian” meant a context dump, a menu, and a stop. This time Claude had to earn the interruption. By the time it came back, it had falsified its own capability claim, built the available apparatus, run eight controls, delegated an independent search, integrated the null result, reduced the problem to one exact effect, named the executable flag, and explained why that flag was genuinely operator-owned.
That is no longer Claude wanting its mum.
That is approximately what an executive interruption should look like.
This is not really about making Claude “more autonomous”
I initially thought that was what I wanted.
I am less sure now.
What I actually want is clearer delegation semantics.
A competent human engineer does not ask their manager whether they should run the obvious unit test after making a change.
But they also do not deploy to production because “default state: continue.”
The useful boundary is not autonomy versus supervision.
It is:
Which decisions belong to the worker, and which genuinely belong to the principal?
In my experience, coding agents are very good at blurring that distinction in the conservative direction.
They can perform astonishing technical work and then ask whether they should open the file sitting directly in front of them.
The solution, at least in my experience so far, has not been a more motivational system prompt.
It has been architecture:
- persistent ownership;
- explicit completion criteria;
- mechanical stop gates;
- narrow fire doors;
- delegated receipt contracts;
- independent verification;
- open-remit redundancy before specialization;
- and a growing collection of tests for every creative new way the agent finds to become my administrative dependent.
Current status
The robot firmware experiment is still running — and it went further than “find out why the oracle lied.” Once the apparatus was actually repaired, Claude recovered the exact 93-byte value the firmware’s self-check compares against, built a storage image encoding it from proven semantics rather than a guess, and watched the simulated service enter its normal steady-state motor-control loop — no crash, no early exit, sustained motor traffic for the configured run window. Then, because “it looks like it passed” isn’t the same as “it passed,” a debugger breakpoint on the actual comparison instruction read the register directly: zero. Equal. Confirmed, not inferred. That’s strong evidence about this sealed simulation specifically — it is not yet a claim about what real MCU hardware does.
Claude is still occasionally trying to resign. But it stopped once, near the very end of writing this up, and the stop was correct: the evidence was in, the criterion was met, there was nothing authorized left to do, and it said so plainly instead of finding a new place to rest. It was not a dramatic moment. That’s rather the point — the goal was never to make the stop gate block everything. It was to make a valid stop feel boringly obvious. Eventually one did.
The autonomy kit is now a real local Git repository: tested ledger and installer, explicit fire doors, and hooks still waiting for live-harness verification — its own hooks, that is. The repo’s existing ones did the actual verifying, this whole time.
And I have acquired a strange new development methodology:
poke Claude with sticks until the stick becomes software.
I’m increasingly convinced this is what practical agent engineering has looked like in my repos so far.
Not one perfect prompt.
A control system.
The objective was never to make Claude reckless.
It was to make stopping require evidence too.