> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify in CI

> Run ctrlrun verify against your policy on every push with the CTRLRun GitHub Action, read the two shapes of report, understand the N/A line.

`ctrlrun verify` runs the kernel's own failure scenarios against your policy, in a scratch
store, with fake executors and no network, and reports what passed, what failed, and what it
could not check. In CI that is one step, and the badge it writes means one thing: the declared
guarantees pass.

**Prerequisites:** a repository with a `ctrlrun.yaml`, and GitHub Actions.

<Steps>
  <Step title="Run it locally first">
    ```yaml runnable theme={null}
    schema: ctrlrun.policy/v2

    actions:
      stripe.refund:
        effect: "refund:{payment_id}"
        rules:
          - when: { amount_gte: 0, amount_lte: 50000 }
            decision: allow
          - decision: approve
      k8s.delete_namespace:
        effect: "namespace:{cluster}:{name}"
        decision: approve
    ```

    ```bash runnable theme={null}
    ctrlrun verify
    ```

    ```text theme={null}
    CTRLRun verify — ctrlrun 0.6.0, catalogue ctrlrun.guarantees/v2
    policy     ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce)
    authority  none
    store      sqlite, scratch (created and destroyed for this run)

    G1   mutated approval refused         PASS  stripe.refund
    G2   replayed approval refused        PASS  stripe.refund
    G3   duplicate effect refused         PASS  stripe.refund
    G4   one winner under concurrency     PASS  stripe.refund (8 processes)
    G5   ambiguous blocks a blind retry   PASS  stripe.refund
    G6   unknown action refused           PASS
    G7   no principal refused            PASS  stripe.refund
    G8   expired authority refused        N/A   no authority section
    G9   delegation cannot escalate       N/A   no authority section
    G10  unknown exception is ambiguous   PASS  stripe.refund
    G11  an altered receipt is detected   PASS  stripe.refund

    9/9 declared guarantees pass. 2 not applicable: G8, G9.
    ```

    Two guarantees are not applicable: this policy has no `authority:` section, so nothing
    about grants can be exercised. They are reported with the reason, excluded from the
    denominator and listed separately. Never `11/11`, and no flag folds them in.
  </Step>

  <Step title="Add the action">
    ```yaml theme={null}
    name: CTRLRun verify

    on: [push, pull_request]

    jobs:
      verify:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: CTRLRun/ctrlrun@main
            with:
              policy: ctrlrun.yaml
    ```

    The action installs `ctrlrun`, runs `ctrlrun verify --json --junit`, renders the job summary
    and the badge JSON from that one report, and uploads the three as an artifact. It fails the
    job when a guarantee failed or the configuration was refused, and succeeds when guarantees
    are N/A: green means nothing that could be checked was wrong.
  </Step>

  <Step title="Read the outputs">
    | Input        | Default                  |                                                              |
    | ------------ | ------------------------ | ------------------------------------------------------------ |
    | `policy`     | `ctrlrun.yaml`           | the document to verify                                       |
    | `authority`  | the policy's own section | a standalone authority document                              |
    | `only`       | all                      | comma-separated guarantee ids; a partial run writes no badge |
    | `install`    | `ctrlrun`                | the pip requirement; `.` verifies with the checkout          |
    | `badge-path` | `verify-badge.json`      | where the Shields endpoint JSON goes                         |

    Outputs: `passed`, `failed`, `applicable`, `not-applicable`, `badge-message`, `report-path`.
    Pin the action to a release tag or a commit rather than `@main` once you rely on it.
  </Step>

  <Step title="Publish the badge, if you want it">
    The action writes the badge JSON and never publishes it, because publishing needs
    `contents: write` and asking every user for that is a bad trade for a tool about least
    privilege. Publish it yourself from a job that runs only on pushes to your default branch,
    with `contents: write` on that job alone, and point Shields' endpoint badge at the raw file.
    [Get the badge](/verify/get-the-badge) has the copy-paste workflow.
  </Step>
</Steps>

## What verify cannot see

Your executors, your `reconcile` hooks, where you put the decorator, your deployment, and
whether your policy is the right policy. An executor that raises `NotExecuted` after the remote
acted is invisible to it. The badge does not mean secure, safe, compliant, certified or audited.

## If it didn't work

* Exit 2, `mode: observe`: verify refuses an observed configuration, because observe mode
  executes what enforce mode would refuse and nothing can be proved about it.
* Exit 2, `nothing could be exercised`: the policy has no `approve` rule, no `effect:` template
  and no grants, so `0/0`, which is never a pass.
* Exit 1: a guarantee failed. The report names the scenario and the action; that is a bug
  report, not a configuration problem.

## Next

* [Exit codes](/reference/exit-codes).
* [What the badge means](/verify): the guarantee catalogue in full.
* [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [What verify guarantees](/security/verify-guarantees.md)
- [Run verify in GitHub Actions](/cookbook/verify-in-github-actions.md)
- [Get the verified badge](/verify/get-the-badge.md)
- [Exit codes](/reference/exit-codes.md)
- [ctrlrun verify](/verify.md)
