CLI
madr-lint [OPTIONS] [PATHS...]madr-lint init [OPTIONS]Arguments
Section titled “Arguments”One or more files or directories to lint. Directories are searched recursively
for .md files.
When omitted, madr-lint lints the configured adrDir (default: docs/adr).
# lint the configured adrDirmadr-lint
# lint explicit pathsmadr-lint docs/adr docs/decisions/0007-use-x.mdOptions
Section titled “Options”| Flag | Default | Description |
|---|---|---|
--format <format> | text | Reporter: text, json, sarif, or github. |
--quiet | off | Report errors only; suppress warnings from output. |
--max-warnings <n> | (none) | Exit 1 when warning count exceeds n. 0 means any warning fails CI. Negative = no limit. |
--config <path> | (auto) | Load exactly this config file (TS or JSON), bypassing discovery. |
--cache / --no-cache | --cache | Use the per-file content-hash cache. |
--cache-dir <dir> | .madr-lint/cache | Cache directory. |
--baseline / --no-baseline | --baseline | Subtract .madr-lint/baseline.json when present. |
--update-baseline | Rewrite .madr-lint/baseline.json from a full lint, then exit 0. | |
--fix | off | Apply autofixes in place, then report the problems that remain. |
--fix-dry-run | off | Print a unified diff of the fixes --fix would apply; write nothing. |
--help | Show help. | |
--version | Print the version. |
CLI flags win over the config file — e.g. --no-cache overrides cache: true.
--quiet × --max-warnings interplay
Section titled “--quiet × --max-warnings interplay”--quiet filters warnings from output but the original warning count is still
used for the --max-warnings threshold — mirroring ESLint’s documented semantics.
This lets you run --quiet --max-warnings 0 to keep CI logs free of warning noise
while still failing the build when warnings exist.
When the threshold is exceeded, the reason is printed to stderr for every
--format, so stdout payloads stay clean for machine consumers:
madr-lint: 3 warning(s) found, exceeds --max-warnings 0# CI: fail on any warning, but keep output cleanmadr-lint --quiet --max-warnings 0Warnings absorbed by the baseline do not
count toward --max-warnings — the baseline is subtracted before the threshold
is checked, so inherited debt never fails CI. Only fresh warnings count.
--update-baseline always exits 0, regardless of --quiet or --max-warnings.
madr-lint init
Section titled “madr-lint init”Scaffold a config file. Non-interactive by design — every decision is a filesystem heuristic or a flag — so it is safe in CI and behind pipes:
npx madr-lint initinit detects three things and writes a config extending
madr-lint:recommended:
- ADR directory — the first of
docs/adr,docs/decisions,doc/adr,adr,docs/architecture/decisionswhose top level contains at least oneNNNN-*.mdfile. When none qualifies it falls back todocs/adr(the linter’s default) and says so. - MADR version — samples up to 20 existing ADRs and lets the majority
win: YAML frontmatter with
decision-makerscounts as v4, other frontmatter as v3, a v2 metadata list as v2. An empty directory, a tie, or no recognizable metadata yieldsauto(the default, so it is omitted from the written config). - Config format —
madr-lint.config.tswhen the project looks TypeScript-ish (atsconfig.json, ortypescriptamongpackage.jsondependencies),.madrlintrc.jsonotherwise.
init refuses to overwrite an existing config file (exit 2); pass
--force to replace it. After writing, it runs a cheap in-process lint of
the detected directory — when that finds violations, the next-steps output
suggests --update-baseline so legacy
debt does not block adoption.
| Flag | Default | Description |
|---|---|---|
--force | off | Overwrite an existing config file instead of exiting 2. |
--dir <path> | (detected) | ADR directory to write into the config, overriding detection. |
--json | off | Emit a machine-readable JSON summary (what was detected and written) instead of text — for agents and scripts. |
# monorepo: point the config at a specific package's ADRsnpx madr-lint init --dir services/api/docs/adr
# machine-readable summarynpx madr-lint init --jsonThe --json payload reports written, configPath, configFormat,
adrDir, adrDirSource (detected / fallback / override),
madrVersion, filesChecked, errors, warnings,
suggestUpdateBaseline, and docsUrl (the getting-started guide).
Autofix
Section titled “Autofix”Some diagnostics are mechanically fixable. madr-lint marks them with a dim
🔧 fixable tag in text output and a "fixable": true field in json.
# apply fixes in place, then report anything left overmadr-lint --fix
# preview the exact changes without touching any filemadr-lint --fix-dry-run--fix rewrites files (only those that actually change), then re-lints the fixed
content and reports the remaining problems — the exit code reflects what is
left, so --fix in CI still fails on anything a fix could not resolve.
--fix-dry-run applies the same fixes in memory and shows a per-file unified
diff, writing nothing; its exit code is what --fix would have produced. If both
flags are given, --fix-dry-run wins (nothing is written).
Where the dry-run diff goes depends on --format, so machine-readable stdout is
never polluted: text prints it to stdout (below); json embeds it in the
payload as a top-level diffs array (see json); sarif / github
send it to stderr so their stdout stays parseable.
--- a/docs/adr/0003-use-postgres.md+++ b/docs/adr/0003-use-postgres.md@@ -1,3 +1,3 @@ # ADR-0003
-- Status: Accepted+- Status: accepted✓ All clear.1 problem fixable (dry run; no files written)Fixing composes with the other flags:
--fix+--quiet/--max-warningsoperate on the remaining diagnostics.- Suppressed (
madr-lint-disable) and baselined (.madr-lint/baseline.json) problems are never rewritten — a fix you chose to keep stays put. --update-baselinecannot be combined with--fix/--fix-dry-run(ambiguous intent — rewrite files vs snapshot violations); the combination exits2.- The cache is bypassed while fixing; a fixed file re-enters the normal pipeline on the next run with a fresh content hash.
Rules that currently offer fixes:
madr/status-enum— normalizes a v2 body-list status onto the configured enum (case, curated misspellings, prefix case/typo).madr/date-iso8601— normalizes an unambiguous v2 body-list date (year-first numeric, English named-month) toYYYY-MM-DD.madr/supersedes-bidirectional— inserts a missing back-reference into the target ADR’s existing frontmatter (the one cross-file fix).
Each rule fixes only the unambiguous cases and leaves the rest reported — the rule’s page spells out exactly what it will and will not touch. In particular, values in YAML frontmatter are never rewritten today.
Reporters
Section titled “Reporters”text (default)
Section titled “text (default)”Human-readable, grouped by file. Where a rule offers a concrete fix, an indented
→ line shows it; a 🔧 fixable tag flags a diagnostic that --fix can repair;
the rule’s documentation URL is printed once per rule per file group (never per
diagnostic, so output stays compact):
docs/adr/0003-use-postgres.md error madr/date-iso8601 Date "2026-13-01" is not a valid ISO 8601 calendar date (YYYY-MM-DD) → use the YYYY-MM-DD calendar-date format, e.g. 2025-03-14 error madr/required-sections Missing required section: "Consequences" → add a "## Consequences" heading to the document body madr/date-iso8601 https://knktkc.github.io/madr-lint/rules/date-iso8601/ madr/required-sections https://knktkc.github.io/madr-lint/rules/required-sections/
2 errorsStructured output for tooling. Each result carries suggestion — a
machine-actionable fix, or null when the rule defines none for that message —
docsUrl, the rule’s documentation URL, and fixable, whether --fix can
repair it. When a fix pass ran, summary also carries fixed (the number of
fixes applied). Under --fix-dry-run, the payload additionally carries a
top-level diffs array — one { "path", "diff" } entry per changed file, with
diff holding the unified diff text — so stdout stays pure JSON:
madr-lint --format json{ "version": 1, "summary": { "total": 1, "errors": 1, "warnings": 0, "baselineHidden": 0 }, "results": [ { "path": "docs/adr/0003-use-postgres.md", "ruleName": "madr/required-sections", "messageId": "missingSection", "severity": "error", "message": "Missing required section: \"Consequences\"", "suggestion": "add a \"## Consequences\" heading to the document body", "docsUrl": "https://knktkc.github.io/madr-lint/rules/required-sections/", "fixable": false, "data": { "section": "Consequences", "found": ["Context and Problem Statement", "Decision Outcome"] } } ]}SARIF for code-scanning integrations (e.g. GitHub code scanning):
madr-lint --format sarif > madr-lint.sarifExit codes
Section titled “Exit codes”| Exit code | Meaning |
|---|---|
0 | No errors; warning count within --max-warnings limit (if set) |
1 | One or more error-severity diagnostics, or warning count exceeds --max-warnings. With --fix / --fix-dry-run, this reflects the problems that remain after fixing |
2 | Usage or configuration error (invalid --max-warnings value, missing --config file, invalid rule options, unknown --format, --update-baseline combined with --fix, existing config on madr-lint init without --force) |
Caching
Section titled “Caching”The cache stores per-file diagnostics keyed by content hash and is invalidated when the package version or resolved config changes. Cross-file rules always re-run.
# force a clean runmadr-lint --no-cache
# use a custom cache directorymadr-lint --cache-dir .cache/madr-lintBaseline
Section titled “Baseline”Adopting madr-lint on a repo that already has violations? Snapshot them into
.madr-lint/baseline.json so only new violations fail the build:
# snapshot today's violations and commit the filemadr-lint --update-baseline
# subsequent runs subtract the baseline automaticallymadr-lint
# audit everything, ignoring the baselinemadr-lint --no-baselineSubtraction runs after the cache and after inline suppression, and never touches the cache — so editing or deleting the baseline takes effect immediately. See the Adopting on an existing repo guide for the full workflow.