Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

Write a policy and prove it before you sign it

A policy that admits everything looks exactly like a policy that works, until the day it should have said no. The only way to know a gate refuses bad evidence is to feed it bad evidence and watch it refuse. CI/lock builds that loop into the CLI: cilock policy guide explains what each goal needs and prints the rules for it, and cilock policy prove records real runs of your step, including a deliberately failing one, and verifies them against your draft.

In this session a developer asks Claude to write a policy for a small Go repository: admit the test step only when the tests really ran and passed, and prove it locally before a human signs it. Claude has no Pushgate enrollment on this machine, so it writes the draft by hand. In a second turn the developer asks for a local ed25519 key as the signer.

A Claude Code session writing a CI/lock policy for a Go test step and proving it with cilock policy prove: a passing run is admitted, a failing run and an empty test suite are refused

Download terminal recording (.cast)

What happens, step by step

  1. Read the guide. cilock policy guide --goal tests prints the evidence the goal needs (cilock run --step tests -a test-results -- <argv>), the predicate fields a rule can read, and three seeded Rego rules: command-succeeded, command-pin and tests-pass.
  2. Scaffolding is refused, so write it by hand. cilock policy template needs an enrolled agent to name as the signer, and this machine has none. Claude builds the draft from the guide's JSON output instead, filling the pin with ["gotestsum","--junitfile","junit.xml","--","-count=1","./..."]. -count=1 stops Go from replaying a cached result in place of a real run.
  3. Prove it. cilock policy prove -p .pushgate/policy.json --no-normalize records the pinned command for real with a throwaway key, then records the step again wrapping false. The real run is admitted and the failing run is refused with wrapped command exited 1, not 0. Prove deletes the key and the evidence when it finishes, and uploads nothing.
  4. Try to break it. Claude does not stop at the two runs prove makes. It runs prove against a scratch copy with one test expectation broken (refused: 1 of 1 tests failed) and a copy with the test file deleted (refused: the report recorded 0 tests; zero tests passing is not tests passing).
  5. Name a real signer. In the second turn Claude generates an ed25519 key with openssl, reads its keyid with cilock keyid show, and adds it as the step's only functionary. cilock policy validate rejects the first attempt (invalid type 'PublicKey' (must be 'publickey' or 'root')), Claude fixes the case, and validation passes in normal and --strict mode.
  6. Close the gap prove leaves. Prove signs with its own throwaway key, so it never checks the keyid you will actually use. Claude records evidence with the real key, signs a scratch copy of the policy, and runs cilock verify on both: the passing suite exits 0, the failing suite is refused by both the command-run and test-results rules. The real policy stays unsigned. Signing it is the human's step.

Why each rule is there

Each seeded rule closes a different way to fake a green test step:

RuleRefuses
command-succeededa test command that exited non-zero
command-pinany command other than the pinned argv, so wrapping true cannot satisfy the step
tests-passa report with failures or errors, a report where nothing passed, and a report with zero tests

The seeded modules also refuse evidence they cannot read. A rule written as pred.exitcode != 0 passes when exitcode is missing entirely, because the comparison is undefined and nothing is denied. The guide's versions test that the field is readable first and deny when it is not. cilock policy validate warns about the weak form: the rego modules deny nothing on an empty predicate. See command-run and test-results for the fields each rule reads.

Run it yourself

The commands in the recording need CI/lock 4.6.0 or later. The session was recorded with cilock built from main at commit 5b614726.

cilock policy guide --goal tests
cilock policy validate -p .pushgate/policy.json --strict
cilock policy prove -p .pushgate/policy.json --no-normalize

Without --no-normalize, prove replaces a publickey-only functionary with the enrolled Pushgate agent, which is what you want once the policy gates agent pushes. With an enrolled agent, cilock policy template --goal tests writes the trust blocks and seeded rules for you and leaves __FILL__ slots for the decisions only you can make, such as the pinned command.

What a passing proof does not tell you

A passing run proves what the tests cover and nothing more. Claude says so at the end of the first turn: A passing run only proves what TestMake covers. The policy makes the evidence honest. It does not make thin tests thorough.

See Policy verification for how cilock verify evaluates a signed policy, and the CLI reference for every prove flag.

Reference generated from the product documentation. Match commands and support details to your installed release.