Trust & security

corral audits code it did not write, so it runs untrusted code by design: your test suite against deliberately broken copies of your code, tests a model wrote, and, in corral review, reproduction scripts a model wrote. A model can also be steered by text inside the code it reads. The posture starts from that: nothing a model says is trusted, and nothing a model wrote runs unconfined unless you choose that. A verdict is decided by execution. Local certification records are signed; repository and review ledger entries are signed when a certify key is configured and otherwise remain unsigned and hash-linked. SECURITY.md is the full statement; this page is the tour.

The jail

By default corral never runs a mutant against your repository. What it does write is its record: certify --repo and corral review write an entry to <repo>/.corral/ledger (see The record), and certify --local writes nothing under the repository. certify --local and certify --repo copy the tree into a disposable sandbox — bwrap on Linux, --jail container (docker or podman) where bwrap cannot run, sandbox-exec on macOS — and every mutant is applied to that copy. There is no unsandboxed option on this path: if no sandbox can start, the run refuses.

  • Offline. The sandbox has no network. The one exception is vendoring a project whose dependencies live outside the tree (go mod vendor, cargo vendor): that step runs in a throwaway copy, inside the sandbox, with network for that step only — never on the host, because the package manager reads configuration the repository controls.
  • Read-only dependencies. node_modules, vendor, .venv, venv and .bundle are bound in read-only rather than copied (macOS sandbox-exec cannot relocate a directory, so there they are copied).
  • No keys. Model keys are removed from the environment of everything corral runs to grade your code.
  • Only /usr and the checkout. Your home directory is not visible, which is why a toolchain installed there is not either.

The one mode that touches your checkout

certify --repo --substrate workspace runs on your real checkout instead of a copy. It is opt-in, and correct where the caller already is the isolation boundary — an ephemeral CI runner, which is why the GitHub Action passes it. Writes go through a rooted handle so a symlink cannot escape the repository, originals are journaled, files that did not exist are removed rather than left behind, and restores run on a failing command, a timeout, a panic and an interrupt (SIGINT/SIGTERM). The residual risk, stated exactly: a process killed outright — kill -9, out of memory, a runner reclaimed mid-step — can leave a mutant behind, and nothing detects it. Use it on a runner, a scratch clone, or a tree with no uncommitted work.

The test command gets your environment, minus corral’s own model keys and your desktop session: DISPLAY, WAYLAND_DISPLAY, WAYLAND_SOCKET, XAUTHORITY and DBUS_SESSION_BUS_ADDRESS. The suite runs once per mutant, so a suite with UI tests would otherwise open windows on your screen again and again. If yours needs a display, give it one in the test command itself, for example -- xvfb-run -a cargo nextest run.

corral review runs a reviewer’s reproduction scripts in the same offline sandbox by default, with --substrate workspace as the same opt-in for a CI runner.

Why a hijacked model cannot change a verdict

  • The kill rate is your suite’s exit code, run against each planted fault. No model reports it, so no injected instruction can raise it.
  • A proven gap is execution too: a written test counts only if it fails with the fault in and passes on your real code, both run.
  • The critic is advisory. Its opinion is recorded as unverified and never moves a status. Its answer is parsed strictly, so it can say only sound, vacuous or dead check, and where corral can run one test alone, the claim is checked by execution.
  • Review claims are demoted unless their script reproduces, and a second seat tries to refute what stands.
  • The critic must be a different model from the writer. That is the one decorrelation rule enforced; corral warns when every graded seat is one vendor, and does not refuse.

Keys and seats

  • There are no default models. Every seat is one you named; a run with an unnamed seat is refused before any sandbox, store or spend.
  • Keys come from the environment or the keystore (corral secret: OS keyring, then an age-encrypted file; set reads stdin, never argv).
  • Seats start cold. The built-in claude-code seat loads no settings, MCP servers, skills or session file, so an audit never sees your own CLAUDE.md or memory, and no audit reads shared memory or skills.
  • An agent seat is confined only as far as its CLI allows. claude-code gets no tools in certify; codex’s --sandbox read-only stops writes, not reads, so it can still read the host. The CLI inherits corral’s environment, so an exported vendor API key reaches it and may be billed. A certify prompt carries the audited source, which can carry text aimed at a model: audit code you are willing to show a coding agent running as you. See agent seats.
  • A run spends money. --max-tokens caps an audit’s tokens across every audit seat; corral review has no token cap. In the GitHub Action, GitHub withholds secrets from a pull request opened from a fork, so with no guard the run fails (exit 2, no provider key) rather than spending yours; see the GitHub Action for the guard.

The record

  • Unless recording is disabled, certify --repo and corral review write a hash-linked entry, signed (Ed25519) when a certify key is configured (a CI runner has none unless you give it one), to a ledger directory — by default .corral/ledger, and in CI a workflow can preserve it on the orphan branch corral/ledger. certify --repo and corral review also keep a copy of their entry in the machine ledger under corral’s state directory (CORRALAI_MACHINE_LEDGER moves it); certify --local writes there by default and, unless you pass --ledger, nowhere else. A machine ledger that resolves inside the audited checkout is not written, unless it is the repo ledger itself (that directory is written, as the primary entry, and no skip is printed). --no-ledger skips the machine copy, and a certify --repo tree with no commit writes no entry at all. Without a key the entry is unsigned. corral ledger verify --pub <key> checks retained entries against the ledger’s root public key (and, for an entry signed by a registered signer, the key the chain registered; see Signers). Removing an interior entry breaks its successor’s link; removing the tail leaves the remaining chain valid. Detecting tail truncation requires an external record of the expected head. The ledger owner can delete files and recompute unsigned hashes: tamper evidence is not immutability or guaranteed completeness.
  • certify --repo --attest --transparency logs the signed statement to Sigstore Rekor, a public, append-only log, so a record can be checked by someone who trusts neither you nor us.
  • A record’s own embedded key is never a trust anchor. corral certify verify takes the key from you (--pubkey), and refuses an unanchored record unless you pass --allow-unanchored. See Verify a record.

Signers

A ledger written by more than one machine would otherwise need every machine to hold the one key everyone verifies against. Instead the ledger names a root key, kept offline, and the root registers the keys that actually write: a registration or revocation is an entry in the chain, signed by the root, so the set of signers is part of the record and not a file beside it. We did not publish a list of keys next to the ledger because a list a writer can edit vouches for nothing; the cost of the chain-borne design is that the root must sign every change, and that every entry this corral writes is format corral-ledger-5, so an older corral cannot read any ledger this one has appended to, signers or not.

  • Register and revoke. corral ledger signer add <dir> --name <name> --pub <hex> and corral ledger signer revoke <dir> --keyid <id> --reason "…". Both read the root seed from CORRALAI_LEDGER_ROOT_KEY and from nowhere else: not a file corral finds, and never the machine’s certify key. They refuse, writing nothing, when the seed is missing, when CORRALAI_LEDGER_PUBKEY names a different root, when the root has never signed anything in the directory (a mistyped path, an empty directory, an unsigned chain), or when the directory does not verify under the seed’s root. corral ledger signer list <dir> [--pub <hex>] shows the active signers, the entry that registered each, then the revoked ones.
  • The verify rule. With the root key (--pub), each entry’s signature is checked against the key the chain had registered for its keyid at that point; an entry signed by a key not yet registered, or already revoked, is a problem. A revocation holds only as far as the chain’s head is anchored from outside (--expect-head, the branch’s git history): delete the trailing revoke entry and the revoked key’s later entries verify clean, which is the tail-truncation limit described above. corral ledger verify prints who signed each entry as signed by <name> (<keyid>). A key is named by its fingerprint, ed25519: plus 16 hex characters of the SHA-256 of the public key; the full key travels in its registration and is what signatures are checked against.
  • With no key, nothing is authorized. A keyless read does not check signatures, and it believes any signer entry that names the root, so anyone who can write the directory could appear to register their own key. Only a read under the root key says a registration was real.
  • Pruning. A checkpoint on a chain with signers carries the active set forward and is signed by the root, which is what stands in for the pruned registrations. corral ledger checkpoint therefore needs the root seed and --expect-head <hash> from outside the directory: a root checkpoint over a chain whose newest entries (a revocation, say) were deleted would make the revoked key permanent.

Keyless entries

A ledger entry can instead be signed keylessly, by the GitHub repository whose Actions run wrote it. With CORRALAI_LEDGER_SIGNER=sigstore in a run that has id-token: write, corral signs each entry’s hash through Sigstore with the workflow’s OIDC identity and stores the Sigstore bundle in place of an Ed25519 signature (format corral-ledger-6, keyid sigstore). There is no key to keep, which is the point on a disposable runner; the cost is that trust moves from a key you hold to a repository identity and to Sigstore’s public-good root. We did not reuse the root-key path because an ephemeral runner cannot hold a root key, and we did not accept “any GitHub identity” because that would make every keyless entry from every repository look alike.

Keyless signing has been exercised only against an in-process test Fulcio and Rekor; it has not yet been run end to end in GitHub Actions. The run must have the id-token: write permission, which keyless signing requires.

Verification is therefore against a repository you name:

corral verify --ledger <dir> --ledger-repo owner/name

A keyless entry verifies only if its certificate names exactly that repository and its bundle checks against the Sigstore root embedded in the release. --ledger-repo is required only when the chain holds a keyless entry; without it that entry fails with keyless entry: the ledger's repository is not named (pass --ledger-repo owner/name). It must be owner/name and is never guessed from the scan’s repo, the directory or git config, because the repository the entries claim is the thing being checked. corral ledger verify and corral ui take the same flag.

corral ledger checkpoint and corral ledger signer add|revoke|list verify under the root key only, so they cannot operate on a ledger that holds keyless entries. Each refuses before writing or deleting anything, saying the ledger holds keyless entries: signer changes and checkpoints are root-key operations on key-signed ledgers, and pruning would delete entries a repository’s identity vouched for.

Entries written this way are corral-ledger-6, and a corral release before the one that introduced them cannot read, and refuses the whole ledger, once it holds one. Key-signed and unsigned entries are still corral-ledger-5.

Honest scope

corral measures whether a suite catches planted faults and proves the gaps it can; it does not guarantee the correctness of your code, and it does not call an unproven survivor a bug. A generated test’s pass/fail distinction does not establish that its assertion matches the intended specification. Review it and run it in your full suite: tests proven individually can still interfere when combined. A reproduction script’s successful exit likewise does not establish that its interpretation of a defect is correct. It does not make a model immune to instructions in the code it reads — the control is that no model’s word decides a verdict.

The optional daemon

The human gate

Nothing that changes what a future audit run does — a memory promotion, a skill proposal, a fleet-published skill, a promoted control, a superuser grant — happens without a human approving it. One rule, isHumanAdmin, guards every admin write path — a count is deliberately not quoted here, because a number in prose goes stale the moment a path is added and then the page is the thing lying. Among them (approve_proposal, reject_proposal, add_memory(shared=true), promote_memory, promote_reference, the UI’s proposal-approve/reject endpoints, sync_put/sync_delete, promote_endpoint, and the four principal-management writes): isAdmin and no subagent claim on the token. This is the same gate described in the control gate tools and the learning loop, stated here as the security property it actually is — an agent cannot unilaterally make itself, or a later agent, more trusted.

In dev mode there’s no cryptographic identity to check, so the same rule becomes a truthfulness guardrail, not a security boundary: a session that names itself corral-agent at the MCP handshake, or that calls bootstrap/report_host (every shipped worker does; corral-admin never does), is marked a worker for that session’s life and refused at the same gates — workers propose, the operator disposes.

Delegation, and its documented limit

An agent can spawn an out-of-process subagent with a scoped, TTL-bound delegation token: the subagent acts under its own identity, accountability rolls up to the spawning principal, and the token dies on schedule (depth- and fan-out-capped). In-process subagents share their parent’s session and token, so they’re indistinguishable from the parent at the gate — the boundary is per-session, and out-of-process delegation is the spawn mode that actually matters for autonomous workers. This is a disclosed limitation, not a hidden gap: it’s why the human gate is described as per-session enforcement, not per-agent.

Observer tokens

A read-only observer token, minted via mint_observer (or corral-admin mint-observer), grants view-only access to a brain’s live state. corral-observe uses exactly this token shape, and as defense in depth refuses non-GET methods locally even if handed a non-read-only token by mistake. Full write access is scoped to authenticated members and superusers via the brain’s own principal store, seeded day-0 from CORRALAI_ALLOWED_PRINCIPALS/CORRALAI_ADMIN_PRINCIPALS and canonical in the database after.

Sandbox jails

The daemon’s agent-executed commands run inside the same sandbox boundary (internal/sandbox) — a bwrap namespace jail on Linux, workspace-confined with secret-free env — rather than directly against the operator’s own filesystem. The demo’s CLOBBER=1 mode (see claims & leases) still runs inside this same sandbox; it only disables coordination claims, not the sandbox itself.

Egress scan

Before the brain pushes a mission’s branch and opens a pull request, it scans the cumulative changed files for problems the herd must not ship (internal/egress). Committed secrets — API keys, tokens, private-key blocks — are blocking: the push is withheld, a critical finding is filed, and the mission is parked until an operator intervenes. New or vulnerable dependencies and license conflicts are flagged as advisory findings. This is the last brain-side checkpoint before code leaves for any forge — the forge-agnostic floor beneath GitHub’s own secret scanning, which self-hosted Gitea and GitLab don’t get. Containment runs both ways: the jail bounds what an agent can do; the egress scan vets what it can ship.