Skip to main content
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.
mehen.toml

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:
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: 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 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 (history.hotspot, history.churn.relative, …), and the namespaced SQL and Markdown 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.
mehen.toml
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: 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 with its own --fail-on gates.
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:
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):
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

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