# Ship the binary you built and tested

Source: https://www.testifysec.com/docs/cilock/use-cases/ship-what-you-built/

A Claude Code session records a Go build and its tests with cilock, writes a signed policy that chains the test step to the build with artifactsFrom, and shows a swapped binary and a source edit between build and test are both refused.

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)](https://www.testifysec.com/media/docs/recordings/build-chain.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:
   
    
   
   ```bash
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`:
   
    
   
   ```bash
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

 

| Change | Caught by |
| --- | --- |
| Released file differs from the build's product | `cilock verify <file>`: the computed digest must be a subject of the signed evidence |
| Tests ran on different source than the build | `artifactsFrom: ["build"]` on the test step |
| Someone wraps `true` instead of the real command | the pinned-command Rego rule on each step |
| Evidence signed by an unknown key | the 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](https://www.testifysec.com/docs/cilock/concepts/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](https://www.testifysec.com/docs/cilock/concepts/policy-verification) for how `artifactsFrom` and subjects are evaluated, and [Release gates](https://www.testifysec.com/docs/cilock/use-cases/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.
