Skip to content

chant workspace verify

chant workspace verify [--base <rev>] [--head <rev>] [--require attested | attested-runs] [--json]

chant workspace verify reads the trust policy at the base revision and uses it to judge every commit in base..head. The change under review can edit its own copy of the policy, but those edits don’t count until they’re merged.

Attestation is opt-in. With no signers file at base, the command checks nothing and passes. It says so, unless --require attested asks for more. A project without these files sees no change in any other command.

Two files hold the policy. Both are optional, and both are read at base.

FileHolds
.chant/allowed_signersThe signers, in ssh-keygen’s allowed signers format. It’s the same file git log --show-signature can use.
.chant/allowed_signers.rotation.jsonThe current version of the signer set, its threshold, and the signatures that admitted it. See chant workspace signers.
.chant/trust.jsonWhere the signers file is, if not at the default path; the role grants; any adopted commit ranges; the runner keys that sign evidence; and the signers admitted for a return.
{
"schema": 1,
"signers": ".chant/allowed_signers",
"roles": { "admin": ["alice@example.com"] },
"adopted": [{ "to": "<full commit id>", "note": "history before signing" }]
}

The same signers file checks sealed review verdicts, which are signed in the chant-review namespace (#2687), and records sealed by their author, signed in the chant-record namespace (#2688). A line restricted with namespaces="git" signs commits only. A reviewer’s line needs chant-review in its namespaces, an author’s needs chant-record, and a line with no namespaces option signs all three. Sealing a verdict and Sealing a record have the rest.

An entry under admitted, { "return": "ret-<12 hex>", "signers": [{ "principal": "...", "key": "ssh-ed25519 ..." }], "note": "..." }, admits keys for one return only. They verify the commits and seals that return carries, and never a commit made in this repository. chant workspace admit writes it.

Role grants live in trust.json for now. Once chant.workspace.json exists (#2534), they move into the declaration.

Some signer lines are never used, and the command reports each one with the reason:

LineWhy it isn’t used
valid-after or valid-beforegit checks these against the commit’s own date, which whoever makes the commit sets, so a backdated commit would pass. Revoke a key by removing it.
cert-authorityA certificate’s validity is also a date the signer controls.
A principal pattern (*, ?, !)Role grants name signers, so each signer needs one exact name.
CheckRule
AttestationA commit is attested when its ssh signature, in the git namespace, verifies against a key listed at base. Trailers, author names and environment variables count for nothing.
Protected writesA commit that changes the signers file, its rotation file or .chant/trust.json needs a signature from someone the base policy trusts. If an admin role is granted, only an admin’s signature counts. Deleting the signers file is a protected write too.
RotationA change to the signer set must be the next version, signed by a threshold of the set at base. In the --json report, rotation holds the version proposed and who signed it, or the version it had to follow with a reason code. See chant workspace signers.
HistoryThe signer history at base must be unbroken. Every version has to be signed by a threshold of the one before it. If it isn’t, nothing verifies.
Agent runsA commit an agent run made, named by its Chant-Run trailer or listed by the run’s end, is attested with the attestor agent-run when a run statement that a runner or steward key at base signed names it (#3192). The principal is the runner, so a reader tells a person’s signed commit from a run’s attested one by the attestor. The runs part of the report lists each commit of the change a run made, the runs that name it, and whether a statement covers it.
--require attestedEvery commit in the change must be attested, by a signer or by a run statement. A merge with no changes of its own is skipped, and the commits it brings in are checked instead.
--require attested-runsEvery commit in the change that an agent run made must be covered by a verified run statement. A commit no run names needs nothing, so a person’s commits pass unsigned. It needs runner keys in .chant/trust.json at base and no signers file.

The command doesn’t call git verify-commit, because that reads its signing program and signers file from git config. It takes the signature and the signed bytes out of the commit object and passes them to ssh-keygen -Y verify, along with a signers file built from the base policy.

A signature this machine can’t check is reported as attested-unverifiable-here. That covers an OpenPGP signature, such as the one a forge adds to a squash merge, and any signature when ssh-keygen is missing. It doesn’t satisfy --require attested.

OptionEffect
--base <rev>The revision the policy is read from. Defaults to the target branch: origin/HEAD, then main, then master. Never HEAD.
--head <rev>The tip of the change. Defaults to HEAD.
--require attestedFail unless every commit in the change is attested.
--require attested-runsFail unless every commit an agent run made is covered by a verified run statement.
--jsonPrint the report as JSON.
CodeMeaning
0The change passes, or attestation is off and nothing was required.
1The change fails, the base or head can’t be found, or the policy at base can’t be read.
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: npx chant workspace verify --base origin/${{ github.base_ref }} --require attested

Without --require the report fails nothing over agent runs, so a workspace can see which agent commits are attested before it requires them (studio-035 d). Run statements live on chant/lifecycle, so a job that checks them fetches it first, git fetch origin chant/lifecycle; the command reads the local branch, then origin/chant/lifecycle, and never fetches.

- run: git fetch origin chant/lifecycle
- run: npx chant workspace verify --base origin/${{ github.base_ref }} --require attested-runs

The base has to be a revision the change can’t move, which is the target branch tip. With squash merges, the commit that lands is signed by the forge rather than the author. Records then read as attested-unverifiable-here on the target branch, so merge commits or rebase merges keep attestation.

The chant repository uses this on its own pull requests (#2547). .chant/allowed_signers lists the maintainer’s signing key, the one git config user.signingkey names, and the trust job in .github/workflows/chant.yml runs chant workspace verify against origin/<base branch> and the pull request’s own tip. A commit that changes the signers file or .chant/trust.json needs a signature by that key. Every other commit needs none, because the job doesn’t pass --require attested: the maintainer’s merges are signed by the forge, so the target branch’s records read attested-unverifiable-here or unattested, not attested. The job only runs on pull requests, since a push to the target branch has no base to read a policy from.

chant workspace records reports each record’s provenance level, and can require attested too. Beside it, a record’s attested says whether its author sealed it, checked against the same signers file at base. chant workspace signers rotates the signer set. chant workspace evidence signs and checks runner evidence, and chant workspace runs sign signs an agent run.