CI
The agent hooks only see agents that run them. CI closes the gap: it runs the same checks on every pull request, so a change made without the hooks (by a person, by another agent, or with the hooks off) cannot merge. cagr writes the GitHub Actions workflow for you.
Turn it on
[ci]
github = true # sync writes the workflow, the PR template and CODEOWNERS
review = true # the workflow reviews each PR; needs the ANTHROPIC_API_KEY secret
nightly = ["src/**"] # reviews old code every night and opens an issue
Then run cagr sync and commit what it writes:
| File | Written when | Who owns it |
|---|---|---|
.github/workflows/cagr.yml | ci.github = true | cagr. Edit cagr.toml and run cagr sync instead. |
.github/pull_request_template.md | ci.github = true and review.pr_sections is set | You, once you remove the “Generated by cagr sync” mark. |
.github/CODEOWNERS | ci.github = true and some [[risk.paths]] entry has owners | You, once you remove the “Generated by cagr sync” mark. |
While a file still has the mark, cagr sync rewrites it. Once you take the mark out, sync leaves the file alone and prints a note.
Sync also leaves these out when you keep your own copy somewhere else GitHub reads it, so it never shadows yours. For CODEOWNERS that is CODEOWNERS at the root or docs/CODEOWNERS. For the PR template it is a pull_request_template.md at the root, in docs/ or in .github/ in any case (such as PULL_REQUEST_TEMPLATE.md), or a PULL_REQUEST_TEMPLATE/ folder in any of those. Sync prints a note naming your file. If sync had already written its own .github/ copy and it still has the mark, sync deletes it, because GitHub reads .github/ first and would ignore yours. The note names the deleted file, and cagr sync --check reports it as out of date until you run cagr sync.
The workflow
.github/workflows/cagr.yml runs on pull requests (opened, synchronize, reopened, ready_for_review, edited), on the merge queue (merge_group), and, when ci.nightly is set, every night at 03:00 UTC. It has up to three jobs:
checks: on pull requests and the merge queue. It checks out the full history, sets up each package manager your stacks name and installs dependencies (for exampleoven-sh/setup-bun@v2andbun install --frozen-lockfile), installs cagr withci.install, and runscagr ci.review: whenci.review = true, on pull requests that are not drafts. It checks out the pull request’s head commit, installs Claude Code withnpm install -g @anthropic-ai/claude-code, installs cagr, and runscagr review --ciwithANTHROPIC_API_KEYandGH_TOKEN. It haspull-requests: writeso it can comment.nightly: whenci.nightlyis set, on the schedule. It runscagr review --scan '<globs>' --issue, which opens an issue when it finds something. It hasissues: write.
The workflow uses contents: read by default and cancels an older run of the same pull request when a new one starts.
ci.install is the shell command that installs cagr. The default is the install script from the latest release:
[ci]
install = "curl -fsSL https://github.com/krtinshet/cagr/releases/latest/download/install.sh | sh"
Pin a version with CAGR_VERSION, for example install = "curl -fsSL https://github.com/krtinshet/cagr/releases/latest/download/install.sh | CAGR_VERSION=0.1.0 sh".
The ANTHROPIC_API_KEY secret
The review and nightly jobs run Claude Code, which needs an Anthropic API key. Add it as a repository secret named ANTHROPIC_API_KEY: on GitHub, open Settings, then Secrets and variables, then Actions, and add a new repository secret. GH_TOKEN is the workflow’s own token, so you do not set it.
cagr ci
cagr ci runs the checks the hooks ran, on the files the branch changed. The base is --base, else GITHUB_BASE_REF, else [git] protected_branch, else main. Deleted files are left out.
| Check | What it does |
|---|---|
| Generated files | The same as cagr sync --check. A drifted file fails: “… is out of date. Run cagr sync and commit.” |
| Branch name | The branch (GITHUB_HEAD_REF, or the current branch) against [git] branch_name. |
| Commit messages | Each commit subject on the branch against [git] commit_message, skipping merges. |
| Edit checks | The on_edit commands on the changed files, each with a 5 minute limit. If they change any tracked file, this fails, since the branch was not formatted or fixed before commit. |
| Checks | The on_turn_end commands, each with a 30 minute limit. A command with {files} gets the changed files of its stack and is skipped if there are none. A command without {files} always runs. |
| Code rules | The code rules on the changed files. |
| Layout | The layout rules on the changed files, when there are any. |
| Risk | The tier, approvals and owners of the change. Never fails. |
| Pull request body | When the run is for a pull request and review.pr_sections is set: the body must have every heading. “The description lacks “## Risk”. Edit it and fill them in.” |
Output is one line per check, PASS or FAIL, with the problems under each failure. It exits 1 if any check failed.
In GitHub Actions it also:
- prints an
::errorannotation for each code rule finding, on its file and line; - writes a job summary with a table of the checks and the risk tier;
- sets the step output
tierto the risk tier, for later steps.
cagr review --ci
cagr review --ci reviews a pull request in GitHub Actions and posts the findings on it. It needs a pull_request event (GITHUB_EVENT_PATH and GITHUB_REPOSITORY) and a checkout of the pull request’s head commit. If the checkout is another commit, it stops and says to set ref: $ in actions/checkout, or to run cagr sync to rewrite the workflow.
How it works:
- It reviews the pull request against its base, with the same effort, rounds and findings as a local review.
- The findings go up as one review of type COMMENT, with an inline comment on each finding’s line. If GitHub rejects the inline comments, the findings go in the review body instead.
- The review state (rounds and findings) is kept in a comment on the pull request, written by the workflow. The next run reads it back, so later pushes get an incremental review. Local
cagr reviewrecords and CI state are separate. - Resolving a thread counts as an answer. When a person resolves the review thread the workflow started for a finding, that finding is marked answered as intentional (“Thread resolved on GitHub by
."). That is why agents may not resolve threads (see [agent-approval](/cagr/guards.html#agent-approval)). Reply on the thread instead, and let a person resolve it.
The job fails while an Important finding is open. It fails open: if the reviewer could not run, the job passes, unless findings from an earlier run are still open.
If the review or the state comment cannot be written, the job says so in its report and carries on, and the next run starts from no state. Pull requests from forks get a read-only token and no secrets, so this job cannot review them: the reviewer cannot run and nothing can be written. The report says so. Review fork pull requests by hand.
Make review a required check (see below) so the gate holds even for pull requests opened in the browser.
Check branch protection
Some rules only GitHub can enforce. cagr doctor --github reads the protected branch’s protection and rulesets through gh and checks:
- The branch has a protection rule or ruleset at all.
- At least one approval is required.
- New pushes dismiss stale approvals, so an approval covers the commit it saw.
- Every review thread must be resolved before merge (conversation resolution).
- Code owners must approve, when
[[risk.paths]]names owners. - The
checksjob, and thereviewjob whenci.reviewis on, are required status checks, whenci.githubis on. - The
cagr/reviewstatus, andcagr/checkswhen[git] pre_pushis set, are required status checks, when[review] statusis on. See Commit statuses without a workflow. - A merge queue (optional, never fails).
It needs gh installed and signed in (gh auth login). Each failing line says what to turn on and links to https://github.com/<owner>/<repo>/settings/rules.
cagr does not enforce the approval count of each risk tier. Set the counts you want in branch protection.
Commit statuses without a workflow
A repo that does not want to spend Actions minutes on every pull request can let GitHub see the local runs instead:
[review]
status = true
[git]
pre_push = "bun run verify"
Two commit statuses go to GitHub through gh api repos/{owner}/{repo}/statuses/<sha>:
| Status | Sent by | State |
|---|---|---|
cagr/review | cagr review and cagr review answer, on HEAD | success when the gate passes, failure with the count of open Important findings, pending when the commit is not reviewed. |
cagr/review | the pre-push hook, on each pushed branch | The same, from the branch’s review record. |
cagr/checks | the pre-push hook, after the [git] pre_push commands pass | success. Sent only when the pushed commit is HEAD and the work tree has no uncommitted changes, since that is what the commands ran on. |
GitHub only takes a status for a commit it has, and the pre-push hook runs before the push. So the hook starts cagr post-status in the background and lets the push go on. That process waits up to two minutes for GitHub to have the commit, then sends the statuses. It writes what happened to .git/cagr/status.log. A review that runs before the push gets “GitHub does not have
Branch protection can then require cagr/review and cagr/checks with no workflow. Add them under required status checks; GitHub lists a status there once it has been sent. cagr doctor --github reports whether each is required.
Sending a status never blocks anything and never changes an exit code. When gh is missing, not logged in, or cannot write statuses, cagr prints a note that names the fix (gh auth login, or [review] status = false) and goes on.
The statuses are sent from the author’s machine, so they show what the author ran, not what a neutral runner saw. Anyone with write access can set them. Use the workflow when you need checks nobody can skip.
With [ci] github = false, cagr sync deletes a .github/workflows/cagr.yml that still has the “Generated by cagr sync” mark, so turning the workflow off stops it running. A workflow without the mark is yours and stays.
Without GitHub
cagr ci works in any CI. Install cagr, check out the full history so the base branch is there, and run cagr ci --base <branch>. cagr review --ci is GitHub only. Elsewhere, run cagr review against the base: it exits 1 when an Important finding is open.