.izri/config.yml
.izri/config.yml holds your in-repo Izri settings, one top-level section per concern. The scope: section is the vocabulary the scope analyzer reasons against: what your modules are called, what kinds of change your PR types allow, and which paths deserve a second look.
It lives in your repository, at the PR head. That's the important part — it is checked in, code-reviewable, and versioned alongside the code it governs.
Both .izri/config.yml and .izri/config.yaml are read; .yml wins if you somehow have both. Sections resolve independently, so a file carrying only one section leaves the others on their dashboard values.
Sections
| Section | Read by | What it does |
|---|---|---|
runtime: |
runner | How to start the app and run its tests. |
services: |
runner | Runtime dependencies to provision (Postgres, Redis, …). |
signals: |
runner | Which optional signals execute (hallucination, visual). |
testSelection: |
runner | Diff → spec mapping, so a small PR runs a small suite. |
scope: |
analysis | Your modules, PR categories, and sensitive paths. |
policy: |
analysis | The merge policy: threshold, floors, weights, block rules. |
tests: |
analysis | Where izri/tests gets its evidence: ci_upload, runner, or none. |
Every section is optional, and each is validated only by the component
that owns it. A malformed runtime: cannot downgrade your merge policy,
and a malformed policy: cannot break a test run.
The two readers do differ in one way worth knowing: the runner treats a
bad runtime: as fatal, because it cannot start your app without
one. Analysis treats a bad scope: or policy: as non-fatal, falls
back to your dashboard settings, and leaves an info finding on the PR.
A YAML typo never blocks a merge.
Moving from the old files
.izri/izri.yml
The runner config used to live in its own file, with quality: where
signals: now is. It still works: when .izri/config.yml carries none
of the runner's sections, the legacy file is read instead. Move
runtime:, services: and quality: (renaming that last one to
signals:) into .izri/config.yml and delete it.
A legacy file carrying both quality: and signals: is rejected rather
than silently resolved — pick one.
.izri/test-scope.json
The diff → spec mapping used to live in its own JSON file. It is still
read when .izri/config.yml has no testSelection: section. Move its
body under testSelection:, drop the version and _doc keys, and
delete it. See
scoped test selection for the schema.
.izri/scope.yml
.izri/scope.yml was the previous name for the scope section, and the whole file was the scope body. It still works: when there is no .izri/config.yml, the legacy file is read as the scope section and every delta carries one info finding (scope_config_deprecated_path) asking you to migrate. If both files exist, config.yml wins and the legacy file is ignored.
To migrate, nest the old body one level under scope: and delete the old file:
# .izri/config.yml
scope:
modules: [...] # ← the old .izri/scope.yml body, indented
categories: [...]
sensitive: [...]Resolution precedence
Four steps, in order:
| # | Condition | Source |
|---|---|---|
| 1 | Valid scope: section in .izri/config.yml at the PR head |
in_repo |
| 2 | No scope: section, but a valid legacy .izri/scope.yml |
in_repo, and emits a deprecation info finding |
| 3 | Malformed file, or a scope: section that fails validation |
falls back, and emits an info finding |
| 4 | No repo config, dashboard config exists | dashboard |
| 5 | Neither | defaults |
A scope: section that is present but invalid does not fall through to the legacy file. An explicit, broken section reports its error rather than quietly resolving from somewhere else.
In-repo wins on purpose. If the dashboard could override a checked-in file, the file in your repository would silently disagree with what actually ran — and the file is the thing your team reviews.
A malformed file degrades, it does not fail
A .izri/config.yml that doesn't parse falls back to the dashboard config or defaults and emits an info-level finding saying so. Your build does not break because of a YAML typo.
The tradeoff: a broken config is easy to miss, because info findings don't block. If scope results look wrong, check the resolved source first — the delta report records which case applied.
Schema
The scope: section takes three keys: modules, categories, sensitive.
scope:
modules:
- name: billing
paths:
- src/billing/**
- packages/payments/**
- name: auth
paths:
- src/auth/**
categories:
- label: feature
expects:
- src/**
requires_test_change: true
- label: docs
forbids:
- src/**
- migrations/**
requires_intent_statement: false
sensitive:
- name: payment handling
paths:
- src/billing/charge.ts
- name: migrations
paths:
- migrations/**modules
Named groups of paths. This is the vocabulary findings are phrased in — without it, scope can only talk about file paths.
| Field | Required | Type |
|---|---|---|
name |
yes | string |
paths |
yes | string[] |
categories
What each kind of change is allowed to touch. label matches the PR's declared type, typically from its conventional-commit prefix.
| Field | Required | Type | Effect |
|---|---|---|---|
label |
yes | string | The PR type this rule applies to. |
expects |
no | string[] | Paths this type normally touches. |
forbids |
no | string[] | Paths this type must not touch. Emits pr_type_forbidden_category at error — a hard rule that fails the umbrella. |
requires_intent_statement |
no | boolean | Require a body explaining the change. |
requires_test_change |
no | boolean | Require the diff to include test changes. |
forbids is the only user-configurable hard gate. Everything else in scope is advisory. If you want a rule that genuinely blocks a merge, express it as forbids on a category — not as a sensitive path.
sensitive
Paths worth a human glance.
| Field | Required | Type |
|---|---|---|
name |
yes | string |
paths |
yes | string[] |
Touching one emits sensitive_path at info. It does not block, and it is not meant to — it points a reviewer at something. Teams reliably assume this gates a merge. It doesn't. Use forbids for that.
tests
tests:
evidence: ci_upload # ci_upload | runner | noneWhere izri/tests gets its run. ci_upload means your CI uploads its JUnit results and coverage with izri upload; runner means Izri executes the tests itself (Playwright only); none means no tests verdict. When the key is present it is the setting — every push writes it to every project connected to this repository, and the dashboard shows it; two projects on one repository that need different sources leave the key out and set each in the dashboard. It is read from your project's default branch — never from a pull request's head or base, otherwise anyone who can open a PR could switch the project to none and merge with red tests. A change takes effect on the first push after it lands on that branch. When absent, the dashboard value stands, and a project's first accepted upload switches it from none to ci_upload on its own.
Auto-derivation
You don't have to write this by hand. Izri derives an initial config from your repository's file tree and metadata. The stored config records its origin:
| Source | Meaning |
|---|---|
derived |
Generated by Izri. |
user |
Edited in the dashboard. |
merged |
Derived, then hand-adjusted. |
Read the current one with the get_scope_config MCP tool, or from the dashboard. Export it into the scope: section of .izri/config.yml when you want it under version control.
Worked example: a minimal config
Start here. Sensitive paths alone are useful before you formalize categories:
scope:
modules: []
categories: []
sensitive:
- name: auth
paths:
- src/auth/**
- name: migrations
paths:
- migrations/**Worked example: enforcing a boundary
Stopping a docs PR from carrying a schema migration:
scope:
categories:
- label: docs
forbids:
- migrations/**
- src/**A PR titled docs: update readme that also edits migrations/0042_add_column.sql now fails the umbrella, regardless of how clean every other signal is.
Related
izri/scope— the signal this configures.- Signals overview — severities and hard rules.
Reading this with an agent? /docs/configure/config-yml.md serves the raw markdown.
Edit this page on GitHub →All docs