Governed mode for an autonomous loop.
Loops that run an AI agent around the clock share a shape: a daemon, a prompt, one engine invocation per cycle, and a markdown file carried between cycles as state. That shape has two properties nobody chose. The engine runs with permissions fully open, because that is what keeps the loop from stalling on a prompt. And the state file is rewritten each cycle by the agent itself, so nothing downstream can tell what it used to say.
cycleseal changes exactly two things and leaves the rest of your loop alone:
- The permissive cycle is refused before it starts. Policy is evaluated against the command line, and there is no path through it that reaches allow by running out of checks.
- Every cycle is sealed into an append-only signed chain — the refused ones too. A chain can still be edited. It cannot be edited undetectably, and that is the whole difference.
Status: early prototype (v0.1). Built by Mindburn Labs.
go install github.com/Mindburn-Labs/cycleseal/cmd/cycleseal@latest
cycleseal keygen # writes a signing key and prints the trust root
cycleseal demo # the whole story in a scratch directoryThen wrap the engine call your loop already makes. One line changes:
- claude -p "$PROMPT" --permission-mode bypassPermissions
+ cycleseal run --prompt-file PROMPT.md -- claude -p "$PROMPT" --permission-mode bypassPermissionscycleseal: REFUSED — permission mode "bypassPermissions" is denied by policy
cycleseal: sealed as receipt #0 34716621a424
The loop's own retry and backoff handle the non-zero exit. Change the mode to one policy accepts and the cycle runs, is sealed, and shows up in the chain.
cycleseal demo runs against /bin/echo, so it works with no engine installed:
── 1. the cycle an ungoverned loop runs today
cycleseal: REFUSED — permission mode "bypassPermissions" is denied by policy
cycleseal: sealed as receipt #0 34716621a424
── 2. a cycle policy accepts
cycleseal: sealed receipt #1 0a031a673bb7 (engine exit 0, 36 bytes out)
── 3. the chain so far
#0 34716621a424 deny echo mode=bypassPermissions exit=-1
permission mode "bypassPermissions" is denied by policy
#1 0a031a673bb7 allow echo mode=plan exit=0
── 4. verify against the trust root
VERIFIED
── 5. edit a sealed receipt, the way a markdown baton would be edited
FAILED
receipt #0: recorded hash 34716621a424… does not match the receipt body: the body was edited after sealing
receipt #0: signature does not verify against the trusted key
cycleseal keygen [-state DIR]
cycleseal run [-state DIR] [-policy FILE] [-prompt-file FILE] -- <engine> [args...]
cycleseal verify [-state DIR] [-key HEX] [-json]
cycleseal log [-state DIR]
Exit codes: 0 ran or verified, 1 engine failed or chain failed
verification, 2 usage error, 3 policy refused the cycle.
Omit -policy and you get the built-in default: engines claude, codex,
and the Codex-compatible wrapper names astra / gpt-6-astra,
modes plan / acceptEdits / read-only / workspace-write, with
bypassPermissions and danger-full-access denied by name and an unstated mode
refused outright.
{
"engines": ["claude", "codex", "astra", "gpt-6-astra"],
"permission_modes_allow": ["plan", "acceptEdits", "read-only", "workspace-write"],
"permission_modes_deny": ["bypassPermissions", "danger-full-access"],
"deny_flags": ["--dangerously-skip-permissions", "--yolo", "--dangerously-bypass-approvals-and-sandbox"],
"require_explicit_mode": true
}Astra is selected in Codex with --model gpt-6-astra; it uses Codex's sandbox
settings. For example:
cycleseal run -- codex exec --model gpt-6-astra --sandbox read-only "review the diff"Codex and the Astra wrapper names accept --sandbox / -s and
--config / -c sandbox_mode=..., including --flag=value and attached short
values. --ask-for-approval / -a alone do not state a sandbox mode. Repeated
mode settings are refused, and arguments after the engine's -- do not count
as mode flags. A mode outside the allow list reports both the value seen and
the allowed set; an empty allow list refuses every stated mode.
Two of those defaults are doing more work than they look:
require_explicit_mode. A command line that names no permission mode is
refused. An unstated mode is not a safe mode — it inherits whatever the engine
defaults to, and the loops this tool exists for default to the most permissive
setting available. Absence must not read as consent.
The allow list is closed. A mode nobody thought to put in the deny list is still refused. New permissive flags ship faster than deny lists grow.
Each cycle seals its sequence number and the hash of the previous receipt, when it started and ended, the engine and the full argv, the permission mode, the hash of the prompt, the hash of the policy that judged it, the decision and its reason, the engine's exit code, and the hash and size of its output.
The refusals are in there too. That is what makes "the loop was governed" a thing someone can check, rather than a claim about a gap in the record.
cycleseal verify re-derives every hash, walks the links, and checks every
signature — offline, against a public key supplied from outside the chain.
The key each receipt carries is checked for agreement but is never the
authority. A chain that vouches for itself with its own key establishes only
that it is internally consistent, which is not the question anyone reading it
is asking. Hand your trust root to whoever will check the chain, or pass it with
-key.
The verifier also fails on an empty chain. Nothing to check is not the same as nothing wrong.
go.mod has no require block, and make check fails if one appears. The
verifier's trust story rests on the standard library's Ed25519 and SHA-256 and
on nothing else, so an adversarial reader has a short list of things to audit.
Being clear about these matters more than the feature list.
- This is not a remote policy enforcement point. Policy is deliberately local and static so installation remains one binary with no account, network, or service dependency. A future remote policy mode must stay optional.
- Receipts use a constrained JCS profile. Version 2 sorts keys and applies RFC 8785 string escaping, but accepts integers only because this schema has no floating-point fields. Version 1 struct-order receipts remain verifiable. The chain is still local rather than anchored, and its schema is not yet a HELM Kernel receipt contract.
- The seam is the invocation, not the agent. cycleseal governs how the engine is launched. What the agent then does inside an allowed cycle is bounded by the permission mode it was allowed to run under, not by cycleseal.
- A local key is a local claim. Anyone who can read
key.ed25519can sign a chain. Custody of that key is the security boundary, and this prototype does nothing to protect it beyond file permissions.
Apache-2.0. See LICENSE.