Configuration

cagr reads its config from cagr.toml, or from a [tool.cagr] table in pyproject.toml. It looks in the current folder, then in each parent folder, and uses the first one it finds. In each folder, cagr.toml wins over pyproject.toml. The folder that holds the config is the repo root for cagr: globs and {files} paths are relative to it.

A full example:

extends = ["recommended"]

[git]
protected_branch = "main"
branch_name = "type/name"
commit_message = "conventional"
pre_push = "bun run verify"

[[stack]]
language = "typescript"
package_manager = "bun"
paths = ["**"]
on_edit = ["biome check --write {files}"]
on_turn_end = ["bun run lint", "bunx tsc --noEmit"]

[[stack]]
language = "python"
package_manager = "uv"
paths = ["services/ml/**"]
on_edit = ["ruff check --fix {files}", "ruff format {files}"]
on_turn_end = ["pytest -q services/ml"]

[files]
protected = [{ glob = ".env*", except = [".env.example"], reason = "Tell the user which key is missing." }]

[commands]
blocked = [{ match = "prisma db push", reason = "Use `bun run db:migrate --name <change>`." }]

The same config in pyproject.toml puts every table under tool.cagr, for example [tool.cagr.git] and [[tool.cagr.stack]].

When the config has an error, cagr names the key and, for a misspelled key, suggests the closest known one (“Did you mean …?”).

After you change the config, run cagr sync to rewrite the generated files. See Generated files.

Where config lives

Keep cagr.toml short: presets, the stack and single settings. Put anything long, list-like or specific to the repo in the .cagr/ folder at the repo root:

cagr.toml              extends, [git], [[stack]], [review] settings
.cagr/
  guards.toml          [[files.protected]], [[commands.blocked]]
  risk.toml            [[risk.paths]]
  rules/*.yml          code rules and layout rules (see Code rules)
  review.md            the review guide

cagr reads every *.toml file under .cagr/, in any subfolder, as part of the config. The one exception is .cagr/review-evals/, which holds review evals. Each one is a fragment with the same keys and tables as cagr.toml. The file names above are a convention only; split the config however suits the repo.

How fragments join cagr.toml:

  • They are read after cagr.toml, in sorted path order.
  • Lists of tables ([[stack]], files.protected, commands.blocked, risk.paths) add up across all files, in that order. An empty list adds nothing.
  • Any other value, including a plain list, may be set in one file only. Setting it in two, or setting a table in one file and a plain value at the same key in another, is an error that names both files: “review.effort is set in both …/cagr.toml and …/.cagr/review.toml. Keep it in one of them.”
  • extends may only be set in cagr.toml.
  • Each fragment is checked against the same schema, and an error names the fragment.
  • Any file may set "$schema" for editor completion. See JSON schema for editors.

Every command and hook sees the merged config. cagr doctor lists the fragments it read on its Also config: line. cagr init writes a short cagr.toml plus a starter .cagr/guards.toml, .cagr/risk.toml and .cagr/review.md. A config in pyproject.toml reads fragments too, from the .cagr/ folder beside it.

Presets

extends lists presets the config builds on. v0.1 has one preset, "recommended". Presets from git URLs are planned for v0.2. Any other name is an error.

How a preset merges with your config:

  • Tables merge key by key, at every depth.
  • Lists of tables ([[stack]], files.protected, commands.blocked, risk.paths) add your entries to the preset’s. An empty list adds nothing and removes nothing.
  • Any other value, including a plain list, replaces the preset’s.

So you cannot remove a preset’s protected file by leaving it out. To change what recommended does, set the single values you want to differ, such as block_force_push = false.

Key Value
git.protected_branch "main"
git.branch_name "type/name"
git.commit_message "conventional"
git.block_force_push true
git.block_hook_skips true
review.enabled true
review.pr_sections ["Testing", "Risk", "How to revert"]
review.agents_may_merge false
risk.paths .github/workflows/** is high (“CI workflows run with the repo’s secrets.”). {**/*.md,docs/**} is low (“Docs and copy.”).
files.protected .env*, except .env.example, .env.sample and .env.template. The lockfiles bun.lock, bun.lockb, package-lock.json, pnpm-lock.yaml, yarn.lock, uv.lock, poetry.lock, Pipfile.lock, Cargo.lock and go.sum.

The reasons shown for the protected files:

  • .env*: “Env files hold real values. Do not edit them; tell the user which key is missing.”
  • Lockfiles: “Lockfiles are written by the package manager. Change dependencies with its add or remove command instead.”

Top-level keys

Key Type Default Meaning
$schema string none Path or URL of the JSON schema, for editors. cagr ignores it.
extends list of strings [] Presets this config builds on. See Presets.
brief list of strings [] Extra lines for the brief the agent gets at session start.

[agents]

Key Type Default Meaning
enabled list of "claude", "codex" ["claude", "codex"] Agents whose hook files cagr sync writes.
command string "cagr" How the generated hook files call cagr. {root} becomes the repo root for each agent.
nested_docs boolean false Write a CLAUDE.md that imports AGENTS.md next to every AGENTS.md that has none. See below.

{root} becomes "$CLAUDE_PROJECT_DIR" in the Claude Code hooks and "$(git rev-parse --show-toplevel)" in the Codex hooks and the git hooks. Use it when cagr is not on PATH, for example command = "{root}/node_modules/.bin/cagr".

Nested AGENTS.md files

Codex reads AGENTS.md and Claude Code reads CLAUDE.md. A monorepo often keeps an AGENTS.md in each app or package. With nested_docs = true, cagr sync writes a CLAUDE.md beside every AGENTS.md that git tracks (or would track) and that has no CLAUDE.md:

<!-- Generated by `cagr sync`: Claude Code reads CLAUDE.md, so this imports AGENTS.md. Edit AGENTS.md instead. -->

@AGENTS.md

A CLAUDE.md you wrote stays as it is, and so does a shim once you add a line to it or replace it with a symlink. The option needs git to find the AGENTS.md files; if git ls-files fails, sync says so and stops. Sync deletes a CLAUDE.md it wrote when the AGENTS.md beside it is gone, or when you turn nested_docs off. cagr sync --check, cagr doctor and cagr ci report a missing or leftover one as out of date.

[git]

Key Type Default Meaning
protected_branch string none The branch agents may not push to. An empty string turns the check off.
branch_name "type/name", "off", or a table off Branch name rule, checked on push.
commit_message "conventional", "off", or a table off Commit subject rule, checked by the commit-msg hook.
commit_types list of strings feat, fix, refactor, perf, test, docs, chore, ci, build, revert Types allowed in branch names and commit subjects.
pre_push string or list of strings [] Commands the pre-push hook runs. A failure stops the push.
block_force_push boolean false Block force pushes.
block_hook_skips boolean false Block --no-verify, changing core.hooksPath, and setting the skip variable.
skip_env string "CAGR_SKIP" The environment variable a human sets to skip the git hooks. Agents may not set it.
hooks_path string ".githooks" Folder for the git hooks, set as core.hooksPath.
worktree_only boolean false Keep the main checkout clean: agents edit and commit only in worktrees.
worktree_dir string ".worktrees" Folder inside the main checkout where worktrees live.

Name rules

branch_name takes "type/name" and commit_message takes "conventional". Both also take "off" or your own rule.

Value Pattern Example
"type/name" <type>/<short-kebab-name>: a type from commit_types, a slash, then lowercase words joined by dashes. feat/deal-pipeline
"conventional" <type>(<scope>): <summary>: a type, an optional scope in parentheses, an optional !, a colon, then a lowercase summary with no trailing period. At most 72 characters. feat(deals): add pipeline board
"off" No check.  

Your own rule is a table with both a pattern (a regular expression) and an example (a valid value, shown in the block message):

[git.branch_name]
pattern = "^[a-z]+/[A-Z]+-[0-9]+$"
example = "feat/ENG-123"

An invalid regular expression is a config error.

Some subjects are never checked: those that start with Merge , Revert , fixup! or squash! . The branch rule is not checked for a detached HEAD or for the protected branch.

[[stack]]

One entry per language area of the repo. language is required.

Key Type Default Meaning
language string required typescript, javascript, python, go, rust, or any name.
package_manager one of bun, npm, pnpm, yarn, uv, pip, poetry, pipenv none The only package manager agents may use for this ecosystem. See package-manager.
paths list of globs ["**"] Globs, relative to the repo root, this stack covers.
exclude_paths list of globs [] Repo-relative paths this stack skips, including nested modules and crates detected by init.
extensions list of strings by language File extensions this stack checks, without the dot.
on_edit list of strings [] Commands run after each edit, on the changed files only.
on_turn_end list of strings [] Commands run before the agent may end a turn that changed files in this stack.

Default extensions:

Language Extensions
typescript ts tsx mts cts js jsx mjs cjs json jsonc css
javascript js jsx mjs cjs json jsonc css
python py pyi
go go
rust rs
any other name none, so every file under paths counts

A file belongs to a stack when it matches one of its paths and has one of its extensions.

{files}

In on_edit and on_turn_end commands, {files} becomes the changed files that belong to the stack, as repo-relative paths, each quoted for the shell and separated by spaces.

  • A command with {files} is skipped when no files match.
  • An on_turn_end command without {files} runs once for the whole repo.

See Hooks and events for when each list runs and how long it may take.

[files]

Key Type Default Meaning
protected list of tables [] Files agents may not edit.
protected[].glob string required A glob. Without a slash it matches the file name anywhere.
protected[].except list of globs [] Globs that stay editable.
protected[].reason string required, not empty What to do instead. Shown to the agent.

[commands]

Key Type Default Meaning
blocked list of tables [] Commands agents may not run.
blocked[].match string required, not empty Words that, in order, inside one command, block it. Text in quotes and heredoc bodies does not count.
blocked[].reason string required, not empty What to run instead. Shown to the agent.

See blocked-command for how matching works.

[rules]

Key Type Default Meaning
dirs list of strings [".cagr/rules"] Folders holding rule files (*.yml).

The rules themselves, code rules and layout rules, are YAML files in these folders, not keys in cagr.toml. See Code rules.

[review]

Key Type Default Meaning
enabled boolean false Gate pull requests on a review by a reviewer agent in a fresh context.
gate list of strings ["gh pr create", "gh pr ready"] Commands that need a passing review of the current commit, matched like commands.blocked.
reviewer string "auto" "auto", "claude", "codex", or a command. A command gets {prompt_file} and {schema_file} and prints the findings as JSON.
model string none Model for the reviewer agent, if not its default.
guide string ".cagr/review.md" The repo’s review instructions, read into every review. When .cagr/review.md is missing and REVIEW.md exists, REVIEW.md is used. A missing file is fine.
effort "quick", "standard", "deep" "standard" How many agents review. See Effort.
security boolean true Run a security finder in standard and deep reviews.
max_rounds integer, at least 1 3 After this many rounds the gate asks the author to step back and rethink the approach.
timeout_seconds integer, at least 30 600 How long one reviewer agent may run.
skip list of globs [] Files the reviewer does not review.
pr_sections list of strings [] Headings a pull request body must have. gh pr create without them is denied.
agents_may_merge boolean true Let agents run gh pr merge and gh pr review --approve. When false, approval stays with a person.
status boolean false Send the commit statuses cagr/review (from cagr review) and cagr/checks (from the pre-push hook) to GitHub through gh, so branch protection can require them without a workflow. See Commit statuses.

See Review.

[risk]

Key Type Default Meaning
default "low", "medium", "high" "medium" Tier of a file that no risk.paths entry matches.
paths list of tables [] A file takes the highest tier of the entries it matches. The change takes the highest tier of its files.
paths[].glob string required Files this entry covers.
paths[].tier "low", "medium", "high" required Their risk tier.
paths[].owners list of strings [] Code owners for these files (@user or @org/team), written to CODEOWNERS.
paths[].reason string none Why these files carry this tier.

A low change needs no human approval, a medium one needs one, and a high one needs two. See Risk tiers.

[ci]

Key Type Default Meaning
github boolean false cagr sync writes the GitHub workflow, the pull request template and CODEOWNERS.
review boolean false The workflow reviews each pull request with cagr review --ci. Needs the ANTHROPIC_API_KEY secret.
nightly list of globs [] Globs the workflow reviews every night with cagr review --scan, opening an issue for findings.
install string curl -fsSL https://github.com/krtinshet/cagr/releases/latest/download/install.sh \| sh The shell command the workflow uses to install cagr.

See CI.

Globs

Globs work like .gitignore patterns:

  • A pattern with no slash matches the file name at any depth. .env* matches .env and apps/web/.env.local.
  • A pattern with a slash matches from the repo root. src/auth/** matches only under the root’s src/auth.
  • A leading / is dropped, so /src/** is the same as src/**.
  • Braces list choices: {**/*.md,docs/**}.

Generated files and cagr sync

cagr sync writes these files from the config:

  • .claude/settings.json (when claude is in agents.enabled).
  • .codex/hooks.json (when codex is in agents.enabled).
  • <hooks_path>/commit-msg and <hooks_path>/pre-push.
  • With agents.nested_docs = true: a CLAUDE.md beside each AGENTS.md that has none. See Nested AGENTS.md files.
  • With ci.github = true: .github/workflows/cagr.yml, .github/pull_request_template.md (when review.pr_sections is set) and .github/CODEOWNERS (when some risk.paths entry has owners). See CI.

Things to know:

  • In the agent hook files, sync manages only cagr’s own entries. Hooks you added stay.
  • The PR template and CODEOWNERS belong to you once you edit them. Sync writes them only while they still carry the “Generated by cagr sync” mark. If the mark is gone, sync keeps your file and prints a note. If you keep your own CODEOWNERS or PR template in another place GitHub reads (the root, docs/, or a PULL_REQUEST_TEMPLATE/ folder), sync does not write its own. See CI.
  • JSON files are compared as data, so a formatter that changes whitespace does not count as a change.

cagr sync --check writes nothing. It lists the files that are out of date (“Out of date: … Run cagr sync.”) and exits 1, so CI can fail on drift. cagr ci runs this check too.

JSON schema for editors

cagr schema prints the JSON schema for cagr.toml and for each .cagr/*.toml fragment. The repo also keeps a copy at schema/cagr.schema.json. To get completion and checks in an editor that reads $schema (for example with the Even Better TOML extension or Taplo), save the schema in your repo and point to it:

cagr schema > cagr.schema.json
"$schema" = "./cagr.schema.json"
extends = ["recommended"]

Existing lint findings

By default, every failed check blocks ([findings] mode = "all"). To compare supported edit checks with the file’s source before this session first touched it:

[findings]
mode = "new"

Only direct ESLint, ruff check, biome lint and biome check commands with one {files} target are compared. Plain commands and the launchers bunx, npx, pnpm exec, uv run and uvx are recognized. Fixers still run on the live file. The read-only comparison removes fix flags. ESLint and Ruff use stdin with the original filename for config lookup. Supported flags are ESLint --fix, --quiet, --no-ignore, --no-warn-ignored, --max-warnings, and Ruff --fix, --unsafe-fixes, --no-cache, --select, --ignore, --extend-select, --extend-ignore, --target-version.

Scripts such as bun run lint, shell expressions, extra targets and other flags stay strict. Biome supports --write, --fix and --no-errors-on-unmatched. It uses the repo-local node_modules/.bin/biome and the root biome.json or biome.jsonc through --config-path. The saved source runs at the same relative path in a private temporary workspace outside the repo, removed after the check. The live file must pass ignore eligibility first; VCS lookup is disabled only for the temporary copy to avoid looking for its Git root. Missing configs or binaries, nested configs, extends, overrides, plugins, custom VCS roots, ignored or unprocessed paths, incomplete JSON, unexpected stderr and timeouts stay strict. Only complete lint diagnostics are compared; formatter, assist and parser failures stay strict. Biome reports are not cached. Turn-end commands, code rules and CI must still fully pass.

Only identical rule, message and severity findings on unchanged, unambiguous lines are left out. Editing the error’s line makes it new. This compares diagnostics, not semantic causality. Config and project dependencies are live, rather than a snapshot of the whole project. Cached reports are keyed by command, source, ancestor config and lockfile contents, and installed executable metadata. Imported config helpers and external plugin inputs are not tracked; start a new session after changing these inputs.

Go and Rust stacks need no package_manager setting. init uses file-scoped gofmt and rustfmt after edits and toolchain checks at turn end. See detected stacks for commands, workspace editions and golangci-lint handling. These commands stay strict in new-findings mode.


MIT license. Copyright 2026 Krtin Shet.