Skip to main content
View Markdown ↗

Copy this page

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

On this page

Ship the binary you built and tested

A green test job and a signed release do not, on their own, say the tests ran against the code you shipped. Between the build and the release, a file can be replaced, and between the build and the test, the source can change. Each step looks fine in isolation.

CI/lock records what each step read (materials) and wrote (products) by SHA-256. A policy step with artifactsFrom requires its materials to match what an earlier step used or produced, and cilock verify <file> hashes the file you are about to ship and requires that digest in the signed evidence. In this session the prompt asks Claude to set that up for a small Go command and to show it refusing a release with a swapped binary.

How this was recorded

TestifySec wrote the prompt and sent it into the session. Claude Code and cilock ran unedited, and the recording was not changed afterwards.

A Claude Code session recording a Go build and test with cilock, writing a policy that chains test to build, and showing that a swapped binary and a source edit between build and test are refused

Download terminal recording (.cast)

What happens, step by step

  1. Two keys. Claude generates two ed25519 keys with openssl: ci.key signs the evidence and policy.key signs the policy. Whoever holds only ci.key cannot rewrite the rules; in the recording Claude holds both, so that separation exists only once policy.key goes to someone else.

  2. Record the build and the tests. Claude wraps go build -o bin/greet ./cmd/greet as step build and go test -count=1 ./cmd/greet as step test, each with cilock run -k ci.key --material-manifest, with no platform. --material-manifest keeps the per-file material digests that artifactsFrom compares. In the recording Claude spells "no platform" as an empty --platform-url; --offline is the documented flag and does the same thing:

    cilock run --step build -k ci.key --offline --material-manifest -a environment \
      -o "$EVIDENCE/build.att.json" -- go build -o bin/greet ./cmd/greet
    cilock run --step test -k ci.key --offline --material-manifest -a environment \
      -o "$EVIDENCE/test.att.json" -- go test -count=1 ./cmd/greet
  3. Write the policy. Each step must be signed by the CI key, run exactly the pinned command and exit 0. The test step adds artifactsFrom: ["build"]. cilock policy validate warns that the first Rego deny rules pass a predicate with the fields missing; Claude rewrites them with object.get defaults so a missing field is denied, then signs the policy with policy.key.

  4. Evidence moves out of the checkout. The first verify fails: the attestation files were written into attestations/ inside the repository, so the test step saw inputs the build never produced. Claude moves the evidence outside the working tree rather than adding an allowedUntracked exception, which would open a gap in the chain.

  5. The real release passes. Verifying the file computes its subject (sha256:ae1de0ce…) and returns Verification succeeded:

    cilock verify bin/greet -p policy/policy.signed.json -k policy/policy.pub \
      -a "$EVIDENCE/build.att.json" -a "$EVIDENCE/test.att.json" --offline
  6. A swapped binary is refused. Claude builds a different bin/greet (sha256:aa876a6b…) that still prints hello, ada, puts it in place, and verifies again: supplied artifact digest(s) … not present in any subject of step "build", exit 1.

  7. A source edit between build and test is refused. Claude appends a comment to main.go, re-records the test step and verifies the real binary: the build step passes, the test step fails with mismatched digests for cmd/greet/main.go, exit 1.

What each check covers

ChangeCaught by
Released file differs from the build's productcilock verify <file>: the computed digest must be a subject of the signed evidence
Tests ran on different source than the buildartifactsFrom: ["build"] on the test step
Someone wraps true instead of the real commandthe pinned-command Rego rule on each step
Evidence signed by an unknown keythe step's functionary names the CI key

The chain is only as strong as the key behind it. Anyone holding ci.key can sign a fake build, which Claude points out at the end of the session. In CI, sign keyless with the job's OIDC identity instead; see Signing and identity. The recording ran offline, so the signatures carry no timestamp. Add --timestamp-servers, or use the platform signer, when you need one.

Run it yourself

The recording used cilock built from main at commit 5b614726. Write the .att.json files outside the checkout (for example $RUNNER_TEMP on GitHub Actions) or the next step records them as materials. See Policy verification for how artifactsFrom and subjects are evaluated, and Release gates for a gate that adds Rego over the evidence.

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