Configuration reference (.opendrs.yml)
.opendrs.yml at the repository root configures the score, the gate and the Action; this page is the full file with every default, followed by how the path lists combine and what opendrs refuses.
| Section | What it controls |
|---|---|
thresholds |
The green, yellow and red cut-offs, in absolute scores or percentiles of your calibrated history (ordering rule under What opendrs refuses) |
autoApprove |
Whether auto-approval is on, the AI-review checks that must have passed, the merge method, the share of eligible PRs still routed to a human, the changed-lines ceiling and which lines count toward it |
paths |
Blocked, unblocked, critical, instructions, ignored and test globs (see Why the default block list is long, Your lists add to the defaults and Inert lines and the size cap) |
llm |
The optional LLM judge: provider, model, endpoint, API key variable, whether the PR body is included, diff size limit |
history |
Lookback window, fix-commit pattern, commit cap, the git log timeout and the label horizon that calibrate and backtest skip |
graph |
The import graph: on or off, depth and file cap |
model |
Where opendrs calibrate writes the fitted model; priors are used until it exists |
brief |
Token budget for the review brief’s Markdown |
captain |
The captain brief: the module map for the layers detector (auto-detected by default, or a flat depth / literal globs), the change-coupling detector’s thresholds, and extra schema/contract globs |
content |
JS regexes matched against each added line (see What opendrs refuses) |
freezes |
Windows in which auto-approval is suspended or held to a stricter green (see What opendrs refuses) |
sources |
Per-source policy for agent, bot and human PRs; every setting only tightens the gate |
The full file with every default
All keys are optional. This is the full file with every default (opendrs init writes it):
thresholds:
mode: absolute # absolute: scores in 0..1; percentile: percentiles of your
# calibrated history (e.g. green: 50, yellow: 90, red: 95)
green: 0.3 # below green: eligible for auto-approval
yellow: 0.6
red: 0.8
autoApprove:
enabled: false # false = comment and label only (dry run)
requireAiReview: # every listed check must have passed on the PR's head commit
checks: [] # check-run names that must have concluded success, e.g. ["claude-review"]
mergeMethod: queue # queue | auto-merge | none
sampleRate: 0.1 # share of eligible PRs still routed to a human
maxChangedLines: 400 # never auto-approve above this, whatever the score
sizeCapExempt: [] # never counts toward maxChangedLines, whatever its language family
countInertLines: false # true restores the old behaviour: every changed line counts
paths:
# block and critical are matched ignoring case, and a list you set adds to these defaults.
block: [".github/**", ".opendrs.yml", ".opendrs/**", "**/CODEOWNERS",
"**/migrations/**", "**/migration/**", "**/migrate/**", "**/*.tf",
".gitmodules", "**/.gitattributes",
# files that execute code on install, build or CI
"**/package.json", "**/.npmrc", "**/.yarnrc", "**/.yarnrc.yml", "**/.pnpmfile.cjs",
"**/setup.py", "**/setup.cfg", "**/pyproject.toml", "**/pip.conf",
"**/build.gradle", "**/build.gradle.kts", "**/settings.gradle*", "**/gradle/wrapper/**",
"**/pom.xml", "**/.mvn/**",
"**/Cargo.toml", "**/build.rs", "**/go.mod",
"**/Gemfile", "**/Rakefile", "**/*.gemspec",
"**/requirements*.txt", "**/conftest.py", "**/.pre-commit-config.y*ml", "**/tox.ini",
"**/noxfile.py",
"**/*.csproj", "**/*.sln", "**/Directory.Build.*", "**/nuget.config",
"**/CMakeLists.txt", "**/*.cmake", "**/.cargo/**", "**/.husky/**",
"**/Makefile", "**/GNUmakefile", "**/*.mk",
"**/Dockerfile*", "**/docker-compose*.y*ml", "**/compose.y*ml",
"**/Brewfile", "**/Vagrantfile", "**/Procfile", "**/*.nix", "**/flake.lock",
"**/.devcontainer/**", "**/.envrc", "**/Taskfile.y*ml", "**/justfile",
".gitlab-ci.yml", "Jenkinsfile", ".circleci/**", "azure-pipelines.yml", ".buildkite/**",
"**/bitbucket-pipelines.yml", "**/.drone.yml", "**/.travis.yml", "**/cloudbuild.y*ml",
"**/Earthfile", "**/BUILD", "**/BUILD.bazel", "**/WORKSPACE*", "**/MODULE.bazel", "**/*.bzl",
# AI agent and editor config that can run commands on a developer's machine
".claude/**", "**/.claude-plugin/**", "**/hooks/hooks.json", "**/.mcp.json",
".cursor/mcp.json", ".vscode/mcp.json", ".vscode/tasks.json",
# CI pipelines under other names and folders
".gitlab-ci.yaml", ".gitlab/**", "**/Jenkinsfile*", "**/azure-pipelines*.y*ml",
".gitea/**", ".forgejo/**", "**/.woodpecker.y*ml", ".woodpecker/**", ".semaphore/**",
".teamcity/**", "**/appveyor.yml", "**/.appveyor.yml",
# more install scripts, build wrappers and git hooks
"**/gradlew", "**/gradlew.bat", "**/gradle.properties", "**/mvnw", "**/mvnw.cmd",
"**/.yarn/plugins/**", "**/.yarn/releases/**", "**/pnpm-workspace.yaml", "**/bunfig.toml",
"**/deno.json", "**/deno.jsonc", "**/lefthook.y*ml", "**/.lefthook.y*ml", "**/Pipfile",
"**/Podfile", "**/composer.json", "**/mix.exs", "**/build.sbt", "**/project/*.sbt",
"**/go.work", "**/rust-toolchain*", "**/configure", "**/configure.ac", "**/Makefile.am",
"**/Makefile.in", "**/meson.build", "**/Containerfile*", "**/Tiltfile", "**/.bazelrc",
# infrastructure and schema changes outside the migrations folders
"**/*.tfvars", "**/*.tf.json", "**/*.tfvars.json", "**/.terraform.lock.hcl",
"**/terragrunt.hcl", "**/*.bicep", "**/Pulumi*.yaml", "**/serverless.y*ml",
"**/schema.prisma", "**/alembic/versions/**",
# who reviews and what bots may merge
"**/OWNERS", "**/OWNERS_ALIASES", "renovate.json", "renovate.json5", ".renovaterc",
".renovaterc.json"]
unblock: [] # default block globs to drop, written exactly as above, e.g. ["**/go.mod"]
critical: ["**/auth/**", "**/payment*/**", "**/billing/**", "**/security/**"]
# ignore and tests: a list you set replaces the default.
# Scored as no risk (a lockfile bump stays green) but never auto-approved, like binary files.
ignore: ["**/*.lock", "**/package-lock.json", "**/pnpm-lock.yaml", "**/go.sum", "**/dist/**",
"**/vendor/**", "**/node_modules/**", "**/*.min.js", "**/*.snap"]
tests: ["**/test/**", "**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*",
"**/*_test.*", "**/test_*.py", "**/spec/**", "**/*_spec.rb", "**/Tests/**",
"**/*Tests.swift"]
# Never excluded from the size cap, whatever the extension; additive like block and critical.
instructions: ["**/CLAUDE.md", "**/AGENTS.md", "**/GEMINI.md", ".claude/**", ".cursor/**",
".cursorrules", ".windsurfrules", ".github/copilot-instructions.md",
".github/instructions/**", "**/SKILL.md", "**/*.prompt.md", "**/prompts/**",
".opendrs/**"]
llm:
enabled: false
provider: anthropic # anthropic | openai (any OpenAI-compatible endpoint)
model: claude-sonnet-5
# baseUrl: https://api.openai.com/v1
# apiKeyEnv: MY_KEY_VAR # a remote baseUrl or custom apiKeyEnv needs OPENDRS_TRUST_REPO_LLM=1 outside the Action
includeDescription: false # the PR body is untrusted; title only by default
maxDiffChars: 60000
verify: # the verification pass, see verification.html
enabled: false # one batched call that keeps, drops or downgrades each brief item
maxTokens: 40000 # estimated prompt tokens above which the call is skipped (fails open)
history:
lookbackDays: 365
fixPattern: "\\b(fix(es|ed)?|bug|hotfix|regression|incident|sev\\d?|revert)\\b"
maxCommits: 20000
gitTimeoutMs: 600000 # timeout for the one-time `git log` history scan; raise this (or lower maxCommits)
# if brief/score fails with "git log timed out ..." on a large or blobless clone
labelHorizonDays: 60 # calibrate/backtest skip the newest 60 days: their bugs may not be found yet (0 = off)
graph:
enabled: true
maxDepth: 3
maxFiles: 50000
model: .opendrs/model.json # written by `opendrs calibrate`; priors are used until it exists
brief:
maxTokens: 2500 # token budget for the review brief's Markdown (characters / 3.5); min 300, max 6000
captain:
enabled: true # false: no captain line, block, labels, output or brief section
# moduleDepth: 2 # unset by default (auto-detection on); set to disable it and use a flat depth everywhere
modules: [] # literal globs overriding auto-detection, e.g. one per package's source tree
coupling:
enabled: true # set by the ApacheJIT experiment; see docs/experiments.md
minSupport: 8 # set by the ApacheJIT experiment; see docs/experiments.md
minConfidence: 0.6 # set by the ApacheJIT experiment; see docs/experiments.md
contracts:
paths: [] # extra schema/contract globs, added to the defaults
content:
block: [] # JS regexes matched against each added line, e.g. ["\\beval\\(", "dangerouslySetInnerHTML"]
freezes: [] # while a window is active: auto-approval suspended, or held to a stricter green
# - name: weekend
# timezone: America/New_York # IANA name; default UTC
# days: [sat, sun]
# - name: black-friday
# from: "2026-11-26" # inclusive local dates, quoted
# to: "2026-11-30"
# green: 0.15 # same units as thresholds; must be below thresholds.green
sources: # per-source policy; every setting only tightens the gate
agent:
green: 0.2 # stricter green for AI-agent PRs (thresholds.mode units)
requireSafe: true # LLM judge must call every hunk safe (needs llm.enabled)
dailyCap: 20 # at most 20 agent commits landed in the last 24 hours
trackRecord: { days: 60, minCommits: 10, maxRevertRate: 0.05 }
patterns: [] # extra regexes over commit messages / author emails
bot: {}
human: {}
Why the default block list is long
Besides the gate’s own files, paths.block lists
files that execute code on install, build or CI: package manifests and their registry settings
(package.json, .npmrc, setup.py, pyproject.toml, requirements.txt, …), build scripts
and project files (build.gradle, pom.xml, build.rs, Makefile, CMakeLists.txt, *.csproj,
Dockerfile, Bazel and Nix files, …), developer hooks and environments (.husky/,
.pre-commit-config.yaml, conftest.py, .envrc, .devcontainer/, gradlew, …), CI pipelines
outside .github/, and AI agent config that can run commands (.claude/, .mcp.json, plugin
manifests and hooks). It also lists infrastructure files (Terraform variables,
Bicep, Pulumi, a Prisma schema) and OWNERS and Renovate files, which decide who reviews and what
bots may merge. Plain agent instructions such as CLAUDE.md and SKILL.md stay under
paths.instructions instead: they count in full toward the size cap but do not block. Block and
critical globs are matched ignoring case, so makefile or an EF Core
Migrations/ folder cannot slip past **/Makefile or **/migrations/**. A
one-line change there (a postinstall hook, a registry pointed at another host, a new build
step) scores as small and low-risk, but runs on every developer machine and CI runner. Dependency
bumps also change a lockfile, which is in paths.ignore and so already goes to a human.
Inert lines and the size cap
autoApprove.maxChangedLines no longer counts every changed line by default. Blank lines, plain
comments, and whole files in the docs family (.md, .rst, .txt, .patch, .diff) are
excluded, in this order (first match wins): a paths.instructions file always counts in full;
then a paths.tests file always counts in full; then a file matching autoApprove.sizeCapExempt
never counts; then a docs-family file never counts; then, in any other file, a hunk where every
changed line is blank or a plain comment in that file’s own syntax never counts (a sibling
real-code hunk in the same file still does); everything else counts in full. A binary file always
counts as 1. The reason shown when the cap fires names both numbers, e.g. “500 changed lines, 200
counted toward maxChangedLines after excluding 300 inert docs/comment lines (cap 400)”.
Docs folders with executable content are not exempt by default. A Jekyll layout
(docs/_layouts/*.html), a notebook (.ipynb), or site JS/CSS under docs/assets/** is not
.md/.rst/.txt, so it is classified "other", not "docs" — those lines keep counting in
full unless you explicitly add them to autoApprove.sizeCapExempt (e.g. sizeCapExempt:
["docs/assets/**"]) on the default branch, the same trust boundary every other .opendrs.yml
setting has. countInertLines: true turns this whole feature off and restores the old,
unfiltered count.
Your lists add to the defaults
A paths.block, paths.critical or paths.instructions you set extends the
default list; it never replaces it, so block: ["infra/**"] blocks infra/** as well as
everything above. To drop a default block entry, name it under paths.unblock on the default
branch, written exactly as in the list, for example unblock: ["**/go.mod"] if you trust go.mod
edits to code review alone. An unblock entry that is not a default block glob is a config error
that lists the valid ones, so a typo cannot pass silently. paths.unblock wins over the same glob
under paths.block. Default critical and instructions globs cannot be removed —
instructions has no unblock-style escape at all. paths.ignore and paths.tests
work the other way: a list you set replaces the default. In the Action, the self-modification
guard keeps .github/**, .opendrs.yml, .opendrs/**, the action, .gitmodules and
.gitattributes blocked whatever the lists say.
Repositories that use Conventional Commits
The default history.fixPattern counts any commit subject matching
\b(fix(es|ed)?|bug|hotfix|regression|incident|sev\d?|revert)\b as a prior fix for
history.prior_fixes. On a repository that uses Conventional Commits heavily, this also counts
routine housekeeping — fix(deps): update dependencies, chore(deps), fix(ci), fix(docs),
fix: typo — as if it were a real bug fix. The score-saturation
experiment
measured this on vitejs/vite: excluding those commits dropped the share matched by
fixPattern by 21.6 %. It was not significant pooled across ApacheJIT’s mostly pre-Conventional-
Commits Java projects, so it is not a new default — but it is worth setting per repo if you use the
convention:
history:
fixPattern: "^(?!(?:deps?|dependencies|ci|docs?|build|chore|style|tests?)(?:\\([^)]*\\))?:)(?!\\w+\\((?:deps?|dependencies|ci|docs?|build|chore|style|tests?)\\):)(?!fix:\\s*typo\\b).*\\b(fix(es|ed)?|bug|hotfix|regression|incident|sev\\d?|revert)\\b"
This is the same keyword match as the default, unless the subject opens with a Conventional-Commit
header whose type or scope is deps[/dependencies], ci, docs, build, chore, style or
test[s], or is the literal fix: typo.
What opendrs refuses
opendrs refuses a config that would fail silently, naming the offending value:
- Globs are repo-relative and support
**,*,?and{a,b}. A glob with[or](character classes are not supported), a backslash, or a leading/or./is an error. paths.unblockentries must each be a defaultpaths.blockglob, character for character.- Thresholds must be ordered:
0 ≤ green < yellow ≤ red ≤ 1in absolute mode, and0 < green < yellow ≤ red < 100in percentile mode. content.blockentries must compile as JS regular expressions. JS has no inline flags such as(?i), so write[Dd]ropinstead.freezesneed at least one ofdays,fromorto, a real IANA timezone,YYYY-MM-DDdates withfrom≤to, and agreenbelowthresholds.green.
Updated 10 Nov 2026 · Edit this page on GitHub