L0 — Unsigned, local.
Recomputes identically to any other bundle; provenance is unverified. Five minutes to your first receipt.
For the skeptic
A completion message is a claim. Flow Agents can compare it with recorded execution evidence on supported paths and show missing checks. Start with the checks, setup, and limits below, then use the runtime table to choose the enforcement your host provides. These controls do not certify that the resulting work is correct.
A recorded review is not by itself evidence of an independent reviewer. The local workflow records the supplied reviewer identity; it does not attest that a separate person or runtime performed the review. The operator must establish that independence separately.
Supported gate paths compare completion claims with captured command results. Revision-bound gates reject evidence when the recorded workspace snapshot no longer matches. These checks do not establish test coverage or factual correctness.
Install a supported runtime adapter, declare meaningful verification commands, and require the relevant CI checks. Protect workflow and CI configuration with owner review. Installation alone does not configure repository protection.
Review test adequacy, reviewer independence, exceptions, and administrator authority. Advisory adapters report gaps without blocking; local command checks have bypass limits. The runtime table and historical notes below explain those boundaries.
What that looks like
Use the runtime table for current enforcement and the linked source for its limits. A past incident shows what a review found on that candidate; it does not qualify today's release.
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 security review built a working exploit against that patch and confirmed it, and the fix went back. It was re-planned and reviewed again: the second review re-ran its own exploit, reported five new bypass probes that all failed safe, and the pull request merged with CI's reconciliation check green on its head.
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. Its description records both review iterations, the critical confirmed in the first fix, and the second review's verdicts, including the five probes; those counts are the reviewing session's own report, not a committed artifact. Its Trust Reconcile check passed on the head commit; Flow Agents retired that CI step in 8.0.0.
The bypass list
You can't judge a boundary you can't see. So here is where ours are, and where they end.
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. Make your pull requests' own checks required, and every pull request re-runs the real verification fresh on its exact head, in a clean environment the agent can't pre-disable. CI doesn't take the bundle's word for anything: a claimed pass the tests don't reproduce simply fails the check. Flow Agents' merge step adds the other half — it won't land a change unless the delivering session's evidence is committed at that same head. (Flow Agents used to ship a separate action that reconciled the bundle against CI, run in its own repo as Trust Reconcile; 8.0.0 retired it, because the required checks already caught what it caught.)
Historical account; the linked ADRs, issues, and source below carry the implementation details. This is not a new exploit or runtime qualification.
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 tracked in one open issue 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 your checks required — installing the tool doesn't configure your branch protection for you. (This site's own repo requires its check, no-bypass — and its workflow definitions and CI scripts require owner review via CODEOWNERS.)
Fabricate a green result, or launder the exit code (npm test || true). Two things catch it. The local gate rejects any verify command carrying ||, and checks claimed passes against the log of what actually ran. Then the pull request's required checks re-run the real tests on its exact head, from the code rather than from anything the agent wrote, so a fabricated "green" in the bundle doesn't make a failing test pass there. Flow Agents' merge step only confirms that the session's evidence is complete and committed at that head; whether the tests pass is the required checks' call.
On your laptop, tampering is detectable, not impossible. It stops mattering at the required checks, which run where the agent can't reach: tamper-evident locally, checked independently in CI — never "tamper-proof." That holds only once the checks are required, and only for the tests they run: an edit that makes the CI workflow itself swallow failures is for code-owner review to catch, not a re-run.
Run the tests, watch them pass, then keep editing. The old pass no longer advances a cursor-moving gate: it is bound to the revision it observed, and freshness is checked again before the run moves on. CI still supplies the independent clean-environment rerun. A narrowed or weakened test can survive both, so that part remains review's job. 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 required checks against the final code, and Flow Agents' merge step won't land the change unless the session's evidence was committed at that exact head.
Where this stops: an edited test survives the trip, because CI runs the test you wrote. A narrowed test suite also needs review. The local revision-bound checks address stale evidence on the paths that use them; they do not establish that the tests cover the intended behavior.
A person with sufficient repository authority may change protections or use an available bypass. Configure administrator enforcement and code-owner review for gate and CI changes, limit bypass permissions, and inspect exceptions. These workflow checks do not establish that an administrator's decision was appropriate.
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 was issue #1008, closed as folded into #1009 rather than fixed. 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
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.
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.
Make your own checks required and they re-run on the exact head of every pull request, in an environment the agent never touched, without reading anything the agent wrote. Flow Agents adds the other half at merge: its merge step requires the delivering session's evidence committed at that exact head, so the commit that merged is the commit the evidence was recorded against (merge-change). That is the whole of it: since 8.0.0, Flow Agents ships no CI check of its own that re-verifies the bundle (why it was retired).
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
Start unsigned and local in five minutes. Turn the dial to a CI identity's keyless signature, 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.
Recomputes identically to any other bundle; provenance is unverified. Five minutes to your first receipt.
A level the Hachure spec defines: the record is signed with a short-lived key bound to an identity an OIDC provider asserts, such as a CI workflow's, and optionally logged in a public transparency log. A default Flow Agents setup doesn't reach it. You get it only by running Flow Agents' checkpoint seal yourself in a CI job granted id-token: write — and Flow Agents does not currently verify that signature.
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. The signer is Surface's (identical in 5.0.0, which Flow Agents 8.0.0 depends on, and in 6.0.0): it attempts to sign when it sees GITHUB_ACTIONS or SIGSTORE_ID_TOKEN, and otherwise falls back to unsigned without saying so (source). Flow Agents ships no workflow that grants it a token, its merge step accepts an unsigned checkpoint, and nothing in Flow Agents has verified a signature since the CI trust anchor was retired. Not to be confused with Flow Agents' own "L1" and "L2" in its verifiable-trust guide, which name its local gate and your required CI checks, not signing levels.
Runtime by runtime
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.
| Runtime | Enforcement | What it means |
|---|---|---|
| Claude Code | Blocking | The Stop hook refuses or escalates on unbacked "done." |
| Codex | Blocking | Same blocking path, wired through Codex's hooks.json. |
| Kiro | Advisory / opt-in block | All four policies ship and config protection blocks at tool-call time. Flow Agents' docs list Kiro's stop hook as blocking, but as of 8.0.0 its done-check ships in warn mode: set FLOW_AGENTS_GOAL_FIT_MODE=block, or goal_fit.mode "block" in .flow-agents/config/core.config.json, to make it stop the turn. |
| opencode | Advisory / partial | Stop-goal-fit reports but can't block there; config protection still blocks at tool-call time. |
| pi | Advisory / partial | The shutdown callback runs the done-check but does not act on its result, so it cannot block completion; config protection still blocks at tool-call time. |
| AWS Strands (Python & TS) | Advisory / partial | Our 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 harnesses | Spec-only | The open format runs; no runtime hook yet. |
Blocking has two forms. A tool-call block refuses that action and returns the reason to the agent; a blocking Stop hook can keep the turn from ending on unproven work before escalating the decision to you. Advisory runtimes compute and surface the evidence without owning the final stop decision.
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.
Administrative decisions
Review repository permissions and exceptions directly. An agent's reported refusal is not an enforcement mechanism.
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.