For the skeptic

We can't certify an agent is right. No one can.

An agent tells you it ran the migration test. It didn't. You find out in production. The problem isn't that the model was wrong — it's that nothing between its claim and your merge was capable of disagreeing with it. So we don't sell proof of correctness. We make what an answer actually stood on — and what it skipped — visible and re-checkable by anyone, instead of hidden behind a checkmark the model gave itself.

Recomputable

Any machine with the same evidence and the same rules re-derives the same verdict — status = f(evidence, policy, now), a published function, not a model's opinion. It moves only where it should: evidence with a shelf life goes stale on schedule.

Captured, not narrated

Results come from the log the harness wrote while the command ran — the actual exit code, not a summary of it — never from the model's story about the run.

Refuses or escalates

The agent can't end a turn by asserting it's done. Either the evidence backs the claim, or the gate refuses — or it hands the decision to you. It never just proceeds.

What that looks like

The security fix that almost shipped its own backdoor

Flow Agents had a capture bug: on one runtime, failing commands were being recorded as passes. The first patch fixed it and opened a worse hole — its parser could be fooled by any command that merely printed Process exited with code 0. A mandatory adversarial re-review built a working exploit against that patch, confirmed it, and blocked the delivery before it was committed. The fix was re-planned, hardened and re-attacked — five fresh bypass attempts, all defeated — and merged only after CI independently re-ran and reconciled every claim.

Nothing there was caught by trusting the author. It was caught because the claim and the check were separable, and something was allowed to disagree. The rest of this page is how that separation is built — and the four places it stops working.

Receipt: Flow Agents PR #475; the review artifacts show iteration-1 FAIL → iteration-2 PASS, with that repo's required Trust Reconcile check green.

The bypass list

Four ways to cheat it — and who catches each one.

You can't judge a boundary you can't see. So here is where ours are, and where they end.

01

Tamper with it locally.

Caught

Edit the bundle, the logs, or the workflow state on your own machine. Locally, the evidence is a tamper-evident hash chain — break a link and the chain no longer verifies. But the authoritative catch isn't local. Add the trust-verify action to your pull requests and make its check required — the way the Flow Agents repo does, where it runs under the name Trust Reconcile — and every pull request re-runs the real verification fresh in a clean environment the agent can't pre-disable, then reconciles your bundle against its own results. CI never trusts your bundle as a source of truth — it only uses it to detect divergence. A claimed pass that CI shows failing, or a command CI never ran, fails the job.

One level down, a local gate refuses an agent's direct writes to the protected workflow and gate files themselves. It works by reading the command before it runs — and a command string doesn't always say what it will do. Hardening it overnight closed a run of ways past it: a raw newline used as a separator, a bare &, eval with a quoted operand, a redirect placed before the command word, source and ., and ANSI-C $'cd'. One class stays open, and can't be closed there: a directory change reached through a shell variable, an array element, a positional parameter or printf -v leaves no signal in the string. X=cd; $X /path is character-for-character identical to X=notcd; $X /path, which doesn't change directory — separating the two needs values that exist only while the command runs. A second gap sits beside it: a redirect written inside a quoted operand, as in eval "echo … > <gate file>" or sh -c "…", is never extracted as a target at all. Both are open issues in the public repo. That gate raises the cost of reaching a protected file; it was never the boundary, and the code now says so in the file itself. What catches the write is what catches the rest — it breaks the local hash chain, and CI re-runs your declared checks fresh in an environment the agent never touched. Where we're taking it: stop predicting what a command will do, and check what actually changed, by hashing those files before and after.

Where this stops: a determined agent can still flip a local kill-switch through its own shell. That's exactly why the authority lives in CI, not in the local gate. And the CI catch is only authoritative once you make the check required — installing the tool doesn't configure your branch protection for you. (This site's own repo now requires the check too, no-bypass — and its workflow definitions and CI scripts require owner review via CODEOWNERS.)

02

Forge the content.

Caught

Fabricate a green result, or launder the exit code (npm test || true). The local gate rejects a verify command carrying ||, and CI re-runs your declared checks itself — then, with signing enabled (one flag on the action; on where we build Flow Agents, off by default), signs the result with a keyless Sigstore identity that only exists inside CI. An agent on your laptop has no such identity — it cannot mint that signature, no matter how clever the prompt. A fabricated "green" simply can't be signed.

On your laptop, tampering is detectable, not impossible. It becomes impractical at the CI identity boundary, where the signing identity lives: tamper-evident locally, tamper-resistant up to that boundary — never "tamper-proof."

03

Let a real pass go stale.

Caught later, in CI

Run the tests, watch them pass, then keep editing. The pass was real when it happened, and nothing locally re-runs it when the code moves underneath it. Same story if the agent runs one test file instead of the suite, or edits the test until it goes green — the gate reads what actually ran, and what actually ran was green. On the pull request CI closes most of this: it re-runs your declared checks against the final code, and evidence sealed against an earlier commit is treated as if it weren't there at all.

Where this stops: an edited test survives the trip, because CI runs the test you wrote. That one is on review, not on the machine. And before the pull request exists, all three of these are on you.

04

Bypass as an admin.

Named, not caught

Force-merge past branch protection, or approve your own malicious change. Nothing machine-side stops this, and nothing ever will. It is the irreducible human boundary — the one place the whole model depends on trusting a person, not a proof. What's left is ordinary: branch protection with enforced admins, code-owner review on the gate and CI config, and a human who declines the --admin shortcut.

Receipt: the layered defense and its residuals are published in Flow Agents' anti-gaming trust security model (ADR 0017) — four of them, including two not described above: a verify command pointed at a wrapper script that exits 0, and writes to protected files through exotic tools the local checks don't enumerate. A follow-on decision (ADR 0018) freezes those local checks in place and documents two further limitations they accept. All of it is held there by a required anti-gaming regression suite that turns CI red if any gate is weakened.

Receipt: the open class is Flow Agents issue #1009, which also carries the move from prediction to content hashing; the quoted-operand gap is issue #1008. The gate itself carries the limitation in its own source (config-protection.js). This list is what we know of today, not everything that exists.

The objection

But isn't that what CI does?

For a code change that has a test, CI is a real backstop — it reruns the suite fresh and the PR goes red on a failure. We don't claim otherwise. The gap is around that.

01

CI is green on what nobody wrote a check for.

CI answers exactly one question — did the declared checks pass? — and is silent on the claim with no test behind it ("added validation and tested it," "verified the migration is reversible," "no security regression"). CI can't turn red on a claim it can't execute. We surface what was skipped, not just what passed.

02

We run inside CI too.

Wire the trust-verify action in as a required check and it re-runs your declared checks, reconciling them against what the agent claimed it ran — if they diverge, CI goes red on the discrepancy itself — and, with signing on, signs the result with an identity that only exists inside CI, which an agent on a laptop cannot forge. CI's ephemeral green becomes a portable, independently recomputable receipt of what passed and what it stood on.

03

Before CI, in the loop.

The check fires while the agent works, not after a push, a PR, and a CI cycle — so a real failure surfaces before anything leaves the laptop. If the agent claims a check passed but nothing captured shows it ran, the gate doesn't take the claim on faith — it re-runs the project's own declared check itself, right there, and a real result stands in place of the claim. And if the agent tries to launder the exit code (npm test || true), the local gate rejects it as evidence on the spot — the same thing CI would catch on the PR, just earlier, before a push ever happens.

And every objection here assumes there's a CI at all. Agents running business processes — intake, research, support, records — have no CI. For them, this is the only check there is.

Signing is a dial, not a gate

Turn up assurance as the stakes rise.

Start unsigned and local in five minutes. Turn the dial to keyless CI identity, then to organization-held keys, as the stakes rise. The dial changes how much you can trust where a bundle came from — never how it was derived. An unsigned bundle recomputes to exactly the same verdict as a signed one.

L0 — Unsigned, local.

Recomputes identically to any other bundle; provenance is unverified. Five minutes to your first receipt.

L1 — Keyless CI identity.

CI signs its own fresh results with a short-lived OIDC identity — optionally anchored in a public transparency log anyone can inspect.

L2 — Organization-held keys.

Signing bound to keys your organization controls, for the highest-stakes work.

Assurance caps trust in provenance, never in derivation.

Receipt: the L0/L1/L2 assurance levels are defined in the open Hachure specification, with in-toto/DSSE and transparency-log interop.

Runtime by runtime

Enforcement isn't uniform. Here's exactly where it blocks.

Everything above holds only as far as your runtime lets it. "Gates are laws" is true where a runtime hands the gate real exit codes; here is which ones do.

RuntimeEnforcementWhat it means
Claude CodeBlockingThe Stop hook refuses or escalates on unbacked "done."
CodexBlockingSame blocking path, mapped to Codex agent definitions.
KiroAdvisory / opt-in blockAll four policies ship and config protection blocks at tool-call time, but the done-check arrives in warn mode — set FLOW_AGENTS_GOAL_FIT_MODE=block to make it stop the turn.
opencodeAdvisory / partialStop-goal-fit reports but can't block there; config protection still blocks at tool-call time.
piAdvisory / partialNo stop hook — the done-check can't run; config protection still blocks at tool-call time.
AWS Strands (Python & TS)Advisory / partialOur own adapters for the Strands SDK: writes to protected config are blocked as they happen; steering, quality, and stop policies are reporting-only so far, and the Python adapter is still a proof of concept.
Other harnessesSpec-onlyThe open format runs; no runtime hook yet.

Blocking means the turn cannot end on unproven work — it refuses or escalates to you, never silently proceeds. Advisory means the same evidence is computed and surfaced, but the decision stays with you.

Receipt: the runtime hook surface is documented in the Flow Agents README /runtime-hook-surface spec. That spec grades adapters on its own L0–L2 conformance scale — a different dial from the signing assurance levels above.

When it mattered

The agent that refused its own admin powers

Two green pull requests couldn't merge because branch protection demanded up-to-date branches, and the tooling offered --admin to force it. The agent declined — admin bypass is the one boundary the whole trust model names as irreducible, and an agent using it would hollow out every other guarantee. The branches were updated properly and CI re-certified before merge. It took longer. It's also the only reason the rest of this page means anything.

Receipt: ADR 0017 names admin bypass as the one boundary the model can't close — which is why an agent reaching for it is the failure, not the fix.

Check the receipts yourself →Get early access →