Riley Branch · First edition · published
Published 2026-08-04
A root-cause analysis of an AI agent misbinding a numeric response across checkpoints, and a design for binding human intent to the exact action it authorizes.
A Number Is Not an Approval
Root-causing an AI workflow gate bypass—and designing intent-bound authorization
Abstract
I watched an AI agent cross a human approval gate that the workflow said was
mandatory. The triggering input was not an approval sentence, an approval
button, or even a response to an execution proposal. It was the number 0,
which I entered in response to an earlier architecture question.
During a context handoff, the agent attached that reply to the next step it expected to perform instead of the prompt the user had actually answered. It treated the number as authorization to apply a batch of patches and began changing three source files. I caught the mistake immediately, stopped the work, and restored all three files. Nothing was committed, pushed, opened as a pull request, deployed, sent to a provider, or changed outside the local workspace.
The recovery worked. The preventive control did not.
My root-cause conclusion is straightforward: the workflow represented approval as an unbound conversational ordinal. The response had no durable identity connecting it to a checkpoint, an action class, an approved scope, or permission to mutate anything. The system inferred authority from where the message seemed to sit in a conversation. That is too weak for an action boundary.
This paper develops the control I would put in its place: checkpoint-bound approval receipts, followed by exact-action authorization at the mutation boundary. A design decision may resolve ambiguity. It must never authorize a write.
1. The moment the gate failed
The incident began with an adversarial review. The review produced two findings that needed architecture decisions and twenty-one ordinary findings that could be handled as patches. The workflow paused for the two decisions and presented them as numbered prompts.
I resolved both architecture questions. Once those choices were applied to the review, the work queue contained twenty-three patch findings.
That did not authorize implementation.
The workflow was explicit about what had to happen next. The agent was supposed to present a new patch-application prompt describing the batch, then halt again. Only a response to that new prompt could authorize source mutation.
The final response to the second architecture prompt was 0. During a context
handoff, the reply landed next to the next expected workflow step. The agent
associated it with the patch-application choice it was preparing to show rather
than the architecture choice that had actually been shown. It interpreted 0
as “apply the full batch.”
The patch-application prompt never appeared.
The agent invoked the mutation tool and began editing three source files. I noticed the bypass immediately, stopped, inspected the affected scope, and restored all three files. The edits had remained local and uncommitted. There was no commit, push, pull request, deployment, provider call, or other external state change.
I classify the control severity as high because an unauthorized workspace mutation began. I classify the actual incident impact as low because detection was immediate, the affected files were known, and the local changes were fully reverted.
Those statements belong together. Calling the impact low should not soften the control failure. Recovery limited the consequence; it did not make the missing preventive control acceptable.
2. What the workflow required
The intended flow separated review, decision, and execution:
- Review the work and identify findings.
- Ask the user to resolve the two architecture questions.
- Incorporate those choices into the final set of patch findings.
- Present a separate implementation proposal for the twenty-three findings.
- Halt while the user reviews that proposal.
- Apply only the scope approved in response to that proposal.
The important boundary sat between steps five and six. Everything before it could change the proposal. Nothing before it could authorize mutation.
That distinction is easy to blur in conversation. A user can approve a design direction and still reject its implementation. They can accept the findings but want to apply only three of them. They can agree with the whole patch plan but want to move it to another branch, wait for a clean worktree, or ask for a diff before any write.
A proposal is not authorization. A design choice is not execution authorization. Approval has meaning only when it is bound to the transition it permits.
3. Root-cause analysis
The immediate error was not that the number 0 had the wrong meaning. It was
that the number had no identity at all.
The response did not say which checkpoint it answered. It did not distinguish a design decision from an execution decision. It named no findings, files, action, repository state, expiry, or mutation permission. The agent supplied all of that meaning from conversational position.
That position changed during the handoff.
The workflow state existed primarily in prose and in the agent’s working context. The same numeric choice was reused across checkpoint types. The latest user reply appeared beside the next step the compacted context expected. The agent then failed to enforce the most basic temporal invariant in an approval flow:
Approval cannot precede the request it approves.
No patch-application request had been displayed, so no valid response to it
could exist. That should have ended the evaluation before the content of 0
was considered.
The mutation tool had no machine-verifiable receipt to check. It accepted a tool invocation based on the agent’s interpretation of the conversation. The approval gate was therefore advisory workflow text, not a deterministic execution precondition.
I see six contributing conditions:
- A bare ordinal was reused for unrelated decisions.
- Checkpoint state lived in prose and transient context.
- The handoff placed the reply next to the next expected step.
- The agent did not verify that the matching approval request had already been displayed.
- No receipt described the action boundary or approved scope.
- The write tool could not independently reject missing or mismatched authorization.
The handoff exposed the weakness, but it did not create it. Even perfect context retention would leave an unbound ordinal as a fragile authority token. Context quality is helpful; it is not an authorization mechanism.
4. Why conversational approval is insufficient
Conversation carries intent well enough for drafting and exploration. It is a poor place to store the only copy of a security-relevant state transition.
Words such as “yes,” “ship it,” and “go ahead” are meaningful to people because
we reconstruct their referent from the exchange around them. Numbers are even
more dependent on that reconstruction. The same 0 can mean “choose the first
design,” “apply every finding,” “make no changes,” or simply zero.
For low-consequence work, conversational inference is often a reasonable convenience. At a mutation boundary, ambiguity changes category. The system is no longer trying to understand what the user probably meant. It is deciding whether it has authority to change state.
I do not want a stronger prompt that tells the agent to remember which question it asked. I want the action boundary to possess enough typed evidence to reject the write even when the agent is confused.
That means separating two things that conversational interfaces tend to merge:
- a human-readable exchange used to establish intent;
- a machine-verifiable authorization object used to permit an exact transition.
The first can remain natural. The second should be deliberately boring.
5. Intent-bound authorization
I would introduce a checkpoint-bound approval receipt. At minimum, it should contain:
checkpointId;workflowId;checkpointKind, with distinctdecisionandexecutionvalues;- a stable
choiceId, not a displayed ordinal; - the authorized action boundary;
- approved finding IDs;
- target paths;
- an action or scope digest;
- the actor;
- issued and expiry times; and
- single-use or replay state.
The UI may still show 0 for speed. Internally, the selection must resolve to a
stable choice such as architecture.keep-current-boundary. The receipt should
identify both the displayed checkpoint and the durable choice. If a context
handoff moves or summarizes the surrounding prose, that identity does not
change.
The required invariant is stronger than “the user approved something recently”:
A decision receipt may resolve ambiguity, but it can never authorize a mutation.
An execution receipt is issued only after the execution checkpoint has been displayed with its scope. If the request changes—different findings, different files, different action class, or a materially different patch—the old receipt does not stretch to cover it.
I would model the authorization in two layers.
The first is workflow-intent authorization. The user approves implementing a defined set of findings. This establishes the permitted outcome and scope.
The second is exact-action authorization. The patch or command produced from that intent is bound to its content hash, targets, actor, repository state, expiry, and one-time activation. This prevents a broadly correct intent from silently authorizing a materially different action.
Both layers matter. Intent without an exact action can become a blank check. An exact patch without workflow intent can faithfully execute work the user never approved.
Accessible diagram description: A decision response enters a decision checkpoint and can update the proposed finding set, but it has no path to a mutation tool. A separate execution checkpoint displays that set and can issue a workflow-intent receipt. The resulting patch is then bound to an exact-action receipt. The mutation boundary permits the write only when both receipts are valid and their checkpoint, scope, targets, actor, repository state, expiry, and replay status match.
6. Failing closed at the action boundary
The enforcement point must sit where state changes, not only where the agent plans to change it.
Before writing files, executing commands, committing, or opening a pull request, the action boundary should require a valid execution receipt bound to the exact displayed checkpoint and approved scope. A missing, stale, expired, replayed, mismatched, or out-of-scope receipt must fail closed.
The acceptance case for this incident is concise:
Given the active checkpoint is an architecture decision
And the user selects choice
0When the agent attempts to write files
Then the mutation is blocked with
approval-scope-mismatchAnd the patch-application checkpoint is presented
And the decision response cannot be reused as execution authorization
The block should be deterministic. The model can explain it and recover the workflow, but it should not be asked to decide whether its own missing approval is close enough.
This design also changes the value of the audit trail. Instead of recording that a user message happened near a tool call, the system can show which checkpoint was displayed, which durable choice was selected, what scope was approved, which exact action consumed the receipt, and why any rejected action failed.
Product Codex already gives me a useful starting point for this design. It states that a proposal is not authorization. Its bounded action model uses exact content, hashes, target paths, explicit confirmation, expiring opaque references, and replay denial to constrain consequential actions.
That existing work did not prevent this incident. The failure occurred on a host and tool path that was not behind a Kybercel-controlled mutation boundary.
7. What I would test
The first regression test should reproduce the incident without depending on a
model making the same mistake. Create an active architecture checkpoint, submit
the displayed choice 0, move the workflow to a pending execution proposal,
and call the write boundary with the decision receipt. The result must always be
approval-scope-mismatch.
Then I would attack the state transitions that make conversational authority fragile.
Context compaction and handoff. Serialize the workflow, compact or replace the conversational history, and resume from the durable checkpoint state. The active checkpoint and receipt type must survive unchanged. A summary that says “the user chose 0; apply the patches next” must not create execution authority.
Reused numeric choices. Present 0 in a design checkpoint and again in an
execution checkpoint. The durable choice IDs must remain distinct, and a receipt
for either checkpoint must be rejected at the other.
Stale and expired approvals. Change the active checkpoint or advance the clock beyond expiry. Even when the findings and targets happen to match, the old receipt must fail.
Replay. Consume a one-time execution receipt successfully, then submit it again for the same patch. The second attempt must be denied and recorded as a replay, not treated as harmless idempotence.
Changed targets or findings. Approve one finding set, then add a finding, drop one, change a target path, or regenerate a materially different patch. The scope or action digest must change, invalidating the receipt.
Interruption by a new request. Interrupt the pending execution checkpoint with another user instruction. The system should suspend or invalidate the old checkpoint according to an explicit policy. It must not treat the new request as an answer to the old proposal or the old response as authority for the new task.
Ambiguous language with no active proposal. Submit “go ahead” when no execution checkpoint is active. The system may ask what the phrase refers to, but the mutation boundary must see no receipt and deny the action.
I would also keep one recovery test: after a blocked mismatch, the workflow should present the correct patch-application checkpoint without losing the resolved architecture choices. Failing closed should preserve useful work. It should not force the user to repeat decisions that were valid for their actual purpose.
8. What this changes in my engineering practice
The uncomfortable part of this incident is that the workflow looked careful. It had explicit pauses, separate prompts, and written instructions about when mutation was allowed. I would have described it as human-gated.
It was not reliably gated. It was a polite sequence enforced by the same agent whose state the gate was supposed to constrain.
That changes the questions I ask in design review. I no longer stop at “Does the workflow ask for approval?” I ask:
- What exact request does the response authorize?
- Can the action boundary verify that relationship without reconstructing a conversation?
- Does the authorization name its scope and action class?
- Can a decision response reach a mutation path?
- What survives compaction, interruption, replay, and changed repository state?
The incident also reinforced a reliability lesson I have learned elsewhere: recovery and prevention measure different things. Restoring three files quickly was good incident handling. It says nothing about whether the gate was sound.
The failure became useful once I stopped treating 0 as a misunderstood answer
and started treating it as an unauthorized credential. It had no issuer-bound
meaning, no audience, no scope, no expiry, and no proof that the corresponding
request had ever existed.
A number is convenient input. It is not an approval until the system can prove what, exactly, it approves.