When I published the Lucy Syndrome paper in April, I put a section of likely objections at the back. The shortest one was the one I could answer the least: does this only work in Claude Code?
It is a fair thing to ask. For most of a year, the only place I had watched a correction survive across sessions was Claude Code. I wrote about its hooks, I shipped a tool that installs them, and every example I had ran inside one harness built by one company. From there it reads as a trick that leans on a feature Anthropic happened to expose. And if the mechanism only holds where Claude Code holds, then the paper’s fourth invariant is not really an invariant. It is a note about one product.
What I wrote back in the paper was an argument, not a demonstration. The framework, I said, does not depend on Claude Code, because any runtime that can intercept an action outside the model would satisfy it. That is reasoning from the design. You cannot call a thing platform-agnostic by running it on one platform; the word stays empty until a second platform shows up and the thing still works there.
Last month, a second platform showed up. This is what happened when I ran it there.
The boundary the invariant is about
The framework rests on five invariants — properties a system has to have for a correction to survive, stated without reference to any particular tool. The fourth, I4, is the one this essay is about. It says enforcement has to run outside the model’s trust boundary.
The trust boundary is the line between what the model decides and what it cannot. Inside it sits the model and everything the model reads: the system prompt, the knowledge base, the memory store, the skills, the prior turns of the conversation. All of that is context, and context is something the model consumes and then chooses what to do with. Every one of those layers leaves the model as the decider. That is the property they share, and it is the property that defeats them. A correction you write into memory is a correction the model can read and overwrite on the next turn, because reading is not binding. The pull of the training distribution does not care what you put in the knowledge base.
Outside the boundary is code that runs whether or not the model agrees with it. A functional scar lives there. It is a small deterministic rule that fires when the conditions for a known mistake reappear, and it can stop the action before it happens. The model gets no vote. That is the entire difference between Layer 3 — centralized memory, the thing the industry keeps prescribing — and Layer 4, the only layer that closes the loop. From memory to scar makes that argument in full.
I4 is the claim that this outside-ness is the load-bearing property. Not the syntax of any config file, not the name of any event, not the vendor. Where the code runs relative to the model. If that is right, the mechanism should reproduce on any runtime that exposes the same kind of boundary, and it should fail on any runtime that does not. Both halves are testable. Until last month I could only argue the first half.
Why one platform proves nothing
Here is the trap I had to take seriously. Claude Code’s hooks are good. They fire on every event, they can deny a tool call before it runs, they hand you a clean payload to reason over. It is entirely possible to build something that works because of how Claude Code specifically chose to do that, and then to mistake the local convenience for a general law. Plenty of things that call themselves frameworks are one vendor’s API wearing a lab coat.
The only way to tell the difference is a second implementation you do not control. Not a second example on the same platform — that just exercises the same feature twice. A second runtime, built by a different team, with a different protocol, that reached its design without consulting me or the paper. If the same correction binds there, unchanged, then the thing doing the work was never Claude Code. If it does not bind, I learned something and the invariant gets weaker or dies. That is the experiment I wanted and could not run, because the second runtime did not exist yet.
A second runtime I did not design
In early June, OpenAI shipped a native hook API for Codex. I had nothing to do with it. They designed it on their own schedule, after the paper was out, to solve their own problems.
The shape they landed on is close enough to Claude Code’s to be uncanny: a config file that maps each event to a single command entrypoint, a pre-tool event that delivers the tool name and arguments on stdin, the same short list of moments to hook into — session start, prompt submit, pre-tool, post-tool, stop — and, the part that matters, the power to deny an action before it executes by returning a decision the runtime obeys. Two teams, no coordination, converged on the same interception point: deterministic code, outside the model, holding a veto.
That convergence is itself a piece of evidence. Here is why. If “enforcement outside the model” were a quirk of one harness, you would not expect an independent vendor to reinvent the same quirk. The second serious agent runtime to ship hooks shipped this shape — pre-tool, deterministic, able to block. That shape is deliberate. You reach for it when you want a coding agent you can constrain. The invariant predicted the boundary would be reproducible. A second team reproduced it without being asked. In the weeks after, both runtimes kept growing the same further surfaces — a dedicated permission-request event, subagent lifecycle, context compaction — still tracking each other. The convergence was not a one-time coincidence.
You might push back that both adapters are mine, so of course they agree. But the adapter is the only part I control, and an adapter can only translate a boundary that already exists. I did not decide that Codex would expose a pre-tool event with the power to deny; OpenAI did. If they had not, no adapter I could write would conjure one into being — which is exactly why the chat-window case has no fix. The translator rides on a boundary the platform provides; it cannot manufacture one. My contribution is the rule and the translation. Whether there is a boundary to translate at all is the platform’s call, and the invariant is a claim about what happens once there is.
What is held constant, what varies
A demonstration is only a proof if you are precise about what changes and what does not. So here is the ledger.
Held constant: the scar. The rule that recognizes a mistake and decides to block it is the same code on both agents. And the semantics of the block are the same — the action is denied before it runs, not flagged in a note the model reads afterward. A warning the model can override is Layer 3 again with extra steps; what reproduced across the two runtimes is real blocking, the kind the model cannot talk its way past.
Make it concrete. One of the starter rules denies a destructive shell command — the rm -rf you never want running unattended. The scar’s decision is one word: block. The adapter is what turns that word into the exit code Claude Code reads as denied, or the deny decision Codex reads as denied. I checked both against live runtimes, not only the test suite: the command does not execute on either, and the rule that stopped it was written once, against neither agent in particular.
Variable: one thin layer that speaks each agent’s hook protocol. fscars calls it an adapter. It is the translator between the agent’s wire format and the engine that runs the scars. When I added Codex, the scars did not change; the adapter did.
There is one seam in that translation that is the whole essay in miniature. Codex edits files through a tool it calls apply_patch; Claude Code edits through Write and Edit. A scar written to guard file edits matches on Edit. So the Codex adapter renames the event at the boundary — it sees apply_patch, pulls the touched path out of the patch, and presents it to the engine as an Edit. The rule written against Edit fires on a Codex edit and never learns it was running under a different agent. The platform difference was real, and it was absorbed entirely inside the adapter, which is where I4 says it should live. The invariant lives in the rule; the variance lives in the adapter that wraps it. That line — between the rule and its translation — is the whole claim, and here it is about ten lines of code.
| What it is | Across Claude Code and Codex | |
|---|---|---|
| The scar | the rule that catches the mistake, plus the decision to block | identical code |
| The adapter | speaks the agent’s hook protocol; maps apply_patch → Edit | different per agent |
There was a second, smaller demonstration I did not plan, and I trust it more for being a bug. The hook writes its decision to stdout, and both runtimes read that stdout as bytes. On Windows the entrypoint was emitting the console’s legacy encoding instead of UTF-8, so an em-dash in one starter scar’s message went out as a single illegal byte. Both agents choked on it, identically, because both expect valid UTF-8 on that channel. One fix — force the stream to UTF-8 — repaired both. Same code, same bug, same fix, no per-agent special case. A shared contract that fails the same way on both sides and heals the same way on both sides is a shared contract. The wire format is platform-specific; the thing flowing across it is not.
The claim is now falsifiable
State I4 as a prediction and it gets sharper. Take any runtime that exposes a deterministic interception point outside the model, with the authority to deny an action; wire the same scar through a thin adapter; the correction holds without the model’s cooperation.
| Runtime | Interception point outside the model? | Can it deny before the action runs? | Does the correction hold? |
|---|---|---|---|
| Claude Code | yes — hooks on every event | yes — exit 2 | yes |
| OpenAI Codex | yes — native hooks | yes — permissionDecision: "deny" | yes, at the routes it covers |
| Chat window, no harness | no | no | no — memory alone doesn’t bind |
Claude Code satisfies the antecedent, and the correction holds. Codex now satisfies the antecedent, and the correction holds. Two for two, on runtimes built by two companies, is not proof of universality — nothing about a finite list is — but it is the right kind of claim now: one that could be wrong, and is not yet.
The negative case matters as much, and the framework called it in advance. Run the same model without a harness — a chat window, with memory switched on but no hook layer — and there is no interception point. The antecedent fails. I4 predicts the correction will not reliably hold there, no matter how much memory you centralize, because the only move available is to hand the model more context and hope. That is exactly what my own logbooks showed before any of this was built, and it is why the prediction is falsifiable rather than empty: it forbids something. A runtime where corrections held without enforcement outside the model would refute it. I have not seen one. The platforms where corrections hold are the ones with a boundary. The platforms without a boundary are the ones where Lucy never left.
The honest edge
I4 is satisfied where the boundary exists. It is not a promise that every route to a mistake is covered on every platform, and I will not let the cross-agent result get rounded up into one.
Codex’s pre-tool interception is a guardrail, not a complete wall. Its own documentation is clear that it does not yet sit in front of every shell path, and it does not touch tools that are neither shell nor MCP calls — a web search runs without passing the hook. So a scar that has to catch every possible route to a particular mistake cannot promise that on Codex today. A scar that catches the common routes — the Bash command, the file edit, the MCP call — can, and does.
That narrows the claim, and the narrower claim is the stronger one. The framework never needed a perfect boundary. It needed a real one. Where the interception point exists, the scar binds identically across agents; where a platform has a gap, the gap belongs to the platform and gets named in the README, not papered over in the essay. “Platform-agnostic at the interception points each platform exposes” is less quotable than “platform-agnostic,” and it is the true sentence, so it is the one I will stand behind.
Where this sits
There is a recursive detail I cannot resist, because it makes the same point one level up. The Codex adapter was implemented in my workspace by Claude Code and then reviewed, out of band, by Codex — each agent checking the other’s work on the tool that constrains them both. That review did not live inside either model’s good intentions either. It lived in the repo, the test gate, and a release that does not ship until I approve it. The thing keeping two coding agents honest with each other is, again, a boundary outside both of them. Same invariant, scaled from a single tool call up to a development process.
The paper could assert I4 and could not yet defend it, because a claim about platform-independence needs more than one platform to be more than a hope. The defense is now on the record: one correction, two independently built runtimes, identical binding, the difference quarantined in a translator. The objection that opened this essay — does this only work in Claude Code — has an answer. The result was never tied to Claude Code in the first place. It was tied to having a boundary outside the model, and a second runtime can provide that boundary as well as the first one did.
If you want the mechanics rather than the argument, The same scar, two agents walks through both adapters and the one limitation, command by command. This essay is the claim that post is the evidence for.