---
title: .izri/config.yml
description: The checked-in config that teaches the scope signal your project's modules, categories, and sensitive paths.
order: 10
---

# `.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](/docs/signals/tests) 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:

```yaml
# .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`.

```yaml
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`

```yaml
tests:
  evidence: ci_upload   # ci_upload | runner | none
```

Where [`izri/tests`](/docs/signals/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](/docs/install/mcp), 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:

```yaml
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:

```yaml
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`](/docs/signals/scope) — the signal this configures.
- [Signals overview](/docs/signals/overview) — severities and hard rules.
