> ## Documentation Index
> Fetch the complete documentation index at: https://mehen.ophi.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Repository-local mehen.toml: per-metric thresholds with per-language overrides that gate mehen metrics, diff, and top-offenders.

mehen reads an optional repository-local configuration file. Without one, every command behaves
exactly as documented on its own page. With one, the configured metric thresholds become quality
gates: any command that reports a configured metric exits `1` when a limit is crossed.

```toml mehen.toml theme={null}
[thresholds]
cognitive = 15         # higher-is-worse metrics: the limit is a maximum
"loc.lloc" = 500
mi.visual_studio = 40  # higher-is-better metrics (mi.*): the limit is a minimum

[languages.python.thresholds]
cognitive = 10         # overrides the global limit for Python files only
```

## File discovery

mehen looks for `mehen.toml` (preferred) or `.mehen.toml`, starting in the current working
directory and walking up to the enclosing git repository root — the repository is the boundary,
so a config above it (for example a stray `~/mehen.toml`) can never leak into a project. Outside
a git repository, only the current directory is checked. The first file found wins, so a config
at the repository root applies from any subdirectory. When both names exist in the same
directory, `mehen.toml` is used and a warning is logged.

The global `--config <PATH>` flag bypasses discovery and pins an explicit file for any command:

```bash theme={null}
mehen --config ci/strict.toml metrics src/main.py
```

A missing or malformed `--config` file is an error (exit `1`) — a requested gate is never silently
skipped.

## Threshold limits

Each entry under `[thresholds]` maps a metric name to a numeric limit. The dotted spelling
(`loc.lloc = 500`) and the quoted spelling (`"loc.lloc" = 500`) are equivalent.

The limit's meaning follows the metric's polarity:

| Polarity         | Metrics                                                                          | Limit means | Violation               |
| ---------------- | -------------------------------------------------------------------------------- | ----------- | ----------------------- |
| Higher is worse  | `cognitive`, `cyclomatic`, `loc.*`, `abc`, `halstead.*`, most others             | Maximum     | measured value > limit  |
| Higher is better | `mi.*`, `halstead.level`, and quality scores such as `sql.maintainability_index` | Minimum     | measured value \< limit |

A value exactly at the limit passes. mehen knows the direction for every metric it publishes — the
same polarity rules that drive [`mehen top-offenders`](/commands/top-offenders#polarity) ranking
and diff trend arrows.

Metric names are the same ones `--metric` accepts — the relationship goes both ways, so any
configurable key is also selectable as a diff/top-offenders column: the source-code families
(`cognitive`, `cyclomatic`, `loc.*`, `halstead.*`, `mi.*`, `abc`, `nargs`, `nexit`, `nom.*`,
`npa.*`, `npm.*`, `wmc`, including aggregate spellings such as `cognitive.max` or
`nom.functions.max`), the fixed [history family](/metrics/history/overview) (`history.hotspot`,
`history.churn.relative`, …), and the namespaced [SQL](/metrics/sql/overview) and
[Markdown](/metrics/markdown/overview) families (`sql.*`, `markdown.*`). Names that no analyzer
publishes — a bare `mi`, an invented `cognitive.maximum` — are rejected when the config loads.

## Per-language overrides

A `[languages.<lang>.thresholds]` table overrides the global limit for files of that language,
metric by metric. Language identifiers accept the usual aliases (`python`/`py`,
`typescript`/`ts`, `ruby`/`rb`, …); two sections that name the same language through different
aliases are rejected at load time.

```toml mehen.toml theme={null}
[thresholds]
cognitive = 15

[languages.python.thresholds]
cognitive = 10      # Python files use 10; every other language keeps 15

[languages.sql.thresholds]
"sql.change_risk_score" = 3
```

An override does not need a global counterpart — a threshold that exists only under a language
gates only that language's files.

## What gets gated

Thresholds apply to the metrics a command actually reports:

| Command                                          | Evaluated against                                                       | Thresholds considered                                        |
| ------------------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`mehen metrics`](/commands/metrics)             | The analyzed file's full root metric set                                | Every configured threshold                                   |
| [`mehen diff`](/commands/diff)                   | The **head side** of each changed file, before the unchanged-row filter | Thresholds matching the selected (or default) metric columns |
| [`mehen top-offenders`](/commands/top-offenders) | **Every analyzed file**, not just the displayed top N                   | Thresholds matching the `--metric` selection                 |

Two consequences worth knowing:

* A metric the analyzed file does not publish is skipped, never read as `0`. A
  `sql.change_risk_score` threshold does not fire on Python files, and an `mi.visual_studio`
  minimum does not produce false violations on files where MI is unavailable.
* In `mehen diff`, deleted files have no head side to gate, and Markdown files route through the
  [documentation pipeline](/guides/pr-comment-design) with its own
  [`--fail-on` gates](/commands/diff#fail-on-thresholds).

Files whose parse produces a blocking diagnostic are never threshold-gated on partial metrics:
`mehen metrics` and `mehen diff` fail with exit `1` through their existing analysis-error
contract, while `mehen top-offenders` keeps its contract of skipping unparsable files from the
ranking (so a broken file cannot fabricate a passing measurement — but it does not fail the run
by itself either).

## The violation report

Crossed limits print a grouped report on stderr — after the command's normal stdout output, so
JSON and Markdown reports stay complete for machine consumers — and the command exits `1`:

```text theme={null}
mehen::thresholds

  × 2 metric threshold violations (config: /repo/mehen.toml)
  │
  │ src/app/core.py
  │   cognitive = 23 — exceeds max 10  (set by languages.python.thresholds)
  │   loc.lloc = 640 — exceeds max 500  (set by thresholds)
  help: adjust or remove the limit at the configuration path shown, or bring the file back within it.
```

Each line names the measured value, the crossed limit, and the configuration path that set it
(language aliases preserved as written), so the fix — either direction — is one jump away.
Violations are sorted by path, then metric, for deterministic CI logs. Colors engage only on a
terminal with `NO_COLOR` unset. With
`--output-format json`, `mehen diff` additionally embeds the violations as a top-level
`threshold_violations` array (present only when the gate fired), so machine consumers can tell a
quality-gate exit from an analysis failure.

## Validation

The configuration is validated when it loads. A metric name no analyzer publishes — a typo, a
bare family root like `mi` (the analyzers publish `mi.visual_studio`, `mi.original`, `mi.sei`),
an invented aggregate like `cognitive.maximum`, or a misspelled `history.*` key — fails
immediately with a pointer into the file, as do unknown languages, non-numeric limits, and two
spellings of the same metric in one table (`cognitive` and `cognitive.sum` are one logical
threshold; contradictory duplicates would silently disable the stricter gate):

```text theme={null}
mehen::config

  × unknown metric `cognitve` in [thresholds]
   ╭─[/repo/mehen.toml:2:1]
 1 │ [thresholds]
 2 │ cognitve = 5
   · ────┬───
   ·     ╰── not a published metric
   ╰────
  help: did you mean `cognitive`?
```

The command fails with exit `1` before any analysis runs. Namespaced `sql.*` and `markdown.*`
names validate against the owning analyzer's published catalogue too (including the dynamic
`sql.statement.kind_count.<kind>` and `sql.dialect.is_<dialect>` families), so
`sql.modularit_health` is caught just like `cognitve`.

## Relationship to other gates

| Gate                                                        | Where it runs                    | What it checks                        | Exit code      |
| ----------------------------------------------------------- | -------------------------------- | ------------------------------------- | -------------- |
| `mehen.toml` thresholds                                     | Inside the binary, every command | Absolute values at head               | 1              |
| [`mehen diff --fail-on`](/commands/diff#fail-on-thresholds) | Inside the binary, diff only     | Markdown documentation band crossings | 2              |
| [GitHub Action `thresholds`](/guides/github-action)         | Action post-processing           | Adverse per-file **deltas**           | Action failure |

The gates are complementary: the Action input limits how much a PR may *move* a metric, while
`mehen.toml` limits where the metric may *be*. A repository that adopts a `mehen.toml` gets the
same gating locally, in pre-commit hooks, and in any CI system — not only in the GitHub Action.
When the gate fires inside the Action, the PR comment and the `report_json` / `report_markdown`
outputs still publish with the complete report before the workflow step fails.

## See also

* [Commands overview](/commands/overview) — the three commands that enforce thresholds.
* [Concepts → Thresholds and diffs](/concepts/thresholds-and-diffs) — gating strategies in CI.
* [Code metrics](/metrics/code/overview) — the metric names you can threshold.
