Overview
A non-blocking linting layer over your project's markdown — problems as you write, a Problems panel, `ok lint` for CI, and advisories for AI agents. Enable it per project.
Content rules are a lightweight linting layer over your project's markdown. Enable a linter for a project and it checks your markdown as you write. Findings are non-blocking: they surface as warnings, never stop a save, and never gate an agent's edit.
Three linters ship today. markdownlint is the standard engine for markdown style — hard tabs, heading increments, list markers, and the rest. Frontmatter schemas validates each doc's frontmatter against standard JSON Schema files. OKF checks portability to the Open Knowledge Format. The system is pluggable and more linters are planned.
This page covers what's the same whatever linter runs; each linter's page covers its own rules and configuration.
Where problems show up
- Source mode — wavy underlines on the offending range, markers in the lint gutter, hover tooltips, and (for auto-fixable rules) an inline Fix action.
- WYSIWYG — the block a problem falls in is marked, so issues stay visible without raw markdown lines to underline.
- The Problems panel — a tab in the document panel on the right, with a live count badge and two scopes:
- This doc — live diagnostics for the open document, in both WYSIWYG and source mode. Click a problem to jump to it: source mode lands on the exact line and column, WYSIWYG scrolls to the block it falls in.
- Project — an on-demand audit of every in-scope document. It runs when you first open the scope and on the refresh button — never in the background. Results group per file with error/warning totals; configuration problems (a malformed config file, a broken
extends) surface at the top. Clicking a problem opens the offending doc at that position.
- The file sidebar — a file with problems has its row tinted (red for errors, amber for warnings) and carries a count badge. The badge is a control: click it, or tab to it and press Enter or Space, and the file opens with the Problems panel showing that file's problems in This doc scope. Keyboard activation moves focus into the panel; clicking preserves pointer focus. Set
validation.fileTreeIndicators: falsein the project'sconfig.ymlto turn the tint and the badge off (see the configuration reference).
Skill documents are excluded from link checking: files in a skills folder (.claude/skills, .agents/skills, .github/skills, and the like) and the skill documents you open from the Skills panel. A skill's links routinely name files it creates only when it runs, so they report as broken by construction. What goes quiet is the Problems panel in both scopes, and with it the squiggles and gutter marks the panel drives. What stays is the link styling the editor resolves on its own as you type, so a wiki link to a file the skill has not created yet still renders as unresolved, and the Links panel still lists it under Missing. In short: the skill's Problems tab reads clean while the link itself may still look broken in the text. links({ kind: "dead" }) and GET /api/dead-links list them too.
The exclusion is scoped to the skills folder itself, not to the dot-directory containing it, so ordinary documents that happen to live under a dot directory (.github/CI_RUNBOOK.md, a .changeset entry) keep their link findings. So do folder templates under .ok/templates, which you author like any document. A skills folder you added yourself at a visible path, such as team/skills, is ordinary content and is still checked. Links from your other documents to a skill still validate normally.
In both scopes, each row tags the validator that produced it in uppercase — for example MARKDOWNLINT, FRONTMATTER, or LINKS — next to the rule code, and a finding that repeats collapses into one row with an instance count you can expand to reach the individual lines.
The panel is also available in single-file sessions (ok <file>).
Enabling a linter
Open Settings ▸ This project ▸ Plugins and turn on a linter; open editors react live. Each linter has its own toggle, off until you enable it. The choice is saved to the project's config.yml (see the configuration reference), so committing it shares the setting with every collaborator — the whole-project equivalent of a committed lint config.
From the command line
ok lint runs content rules headlessly, with the same config resolution as the editor:
ok lint # audit the whole project
ok lint guides/ # scope to a folder
ok lint guides/intro.md # or a single file
ok lint --fix # apply auto-fixes in place
ok lint --json # structured JSON output
ok lint --errors-only # exit non-zero only on error-severity problemsThe exit code is non-zero when any problem is found. Findings are warnings unless your .markdownlint.* promotes a rule to "error", so --errors-only gates CI on just the rules you chose to enforce. Only markdownlint rules can be promoted — frontmatter findings are always warnings, so --errors-only never gates on them.
What it returns
The text report lists each finding as line:column (1-based), severity, message, and a composed source/code id naming the linter and the violated rule:
docs/guide.md
7:5 warning Hard tabs: Column: 5 markdownlint/MD010
1:1 warning Frontmatter property "owner" is required frontmatter/required
2:1 warning Frontmatter property "status" must be one of: draft, review, published (got "shipped") frontmatter/enum
3 problems (0 errors, 3 warnings) across 1 file.
Checks run: markdownlint, frontmatter.Configuration problems (a malformed config file, a broken schema) print as ! lines after the findings — they describe your setup, not a document. --json emits the same data as a machine-readable object:
{
"contentDir": "/path/to/project",
"files": [
{
"file": "docs/guide.md",
"fixed": false,
"diagnostics": [
{
"range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 5 } },
"severity": "warning",
"source": "markdownlint",
"code": "MD010",
"message": "Hard tabs: Column: 5",
"fixes": [
{ "range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 5 } }, "newText": " " }
]
},
{
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } },
"severity": "warning",
"source": "frontmatter",
"code": "required",
"message": "Frontmatter property \"owner\" is required"
},
{
"range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 15 } },
"severity": "warning",
"source": "frontmatter",
"code": "enum",
"message": "Frontmatter property \"status\" must be one of: draft, review, published (got \"shipped\")"
}
]
}
],
"warnings": [],
"fileCount": 1,
"errorCount": 0,
"warningCount": 3,
"fixedCount": 0,
"ran": ["markdownlint", "frontmatter"]
}Conventions to know:
- JSON ranges are 0-based and end-exclusive (LSP-aligned); the text report displays 1-based positions.
fixesappears only on auto-fixable findings — its presence is how tooling knows--fixwould resolve the problem.fixedistrueon each file--fixrewrote, and the top-levelfixedCountcounts files, not problems.- The top-level
warningsarray carries configuration or runtime degradation problems. - The top-level
ranarray names the enabled source families selected for the run. A family absent fromranwas not checked. An explicit[]means no checks were selected at all. It does not mean the document was checked and found clean.
With --fix, fixable findings are applied in place, fixed files are marked (fixed), and the report lists what remains:
docs/guide.md (fixed)
1:1 warning Frontmatter property "owner" is required frontmatter/required
2:1 warning Frontmatter property "status" must be one of: draft, review, published (got "shipped") frontmatter/enum
2 problems (0 errors, 2 warnings) across 1 file.
Fixed 1 file.
Checks run: markdownlint, frontmatter.Every linter ships off, so a project that has not enabled one closes its report with No checks ran. in place of the Checks run: line. The ✓ No problems above it then means only that nothing was checked. Turn a linter on under Settings ▸ This project ▸ Plugins.
ok audit widens the same report to the full validation plane — content-rule problems and broken internal links, each finding tagged with its source:
ok audit # audit the whole project (lint + links)
ok audit guides/ # scope to a folder or a single file
ok audit --json # the full structured diagnostic plane
ok audit --errors-only # exit non-zero only on error-severity problemsUnlike ok lint, ok audit needs the project's server running (ok start or OK Desktop) — the links validator reads the live backlink index. There's no --fix because the audit is read-only (lint fixes go through ok lint --fix, link repairs are content edits).
--json returns the same per-file grouping as ok lint --json, with two differences: the audit never writes, so there's no contentDir, fixed, or fixedCount; and a links finding carries linkTarget — the unresolved target verbatim, so tooling never parses it back out of the message:
{
"files": [
{
"file": "docs/guide.md",
"diagnostics": [
{
"range": { "start": { "line": 11, "character": 0 }, "end": { "line": 11, "character": 0 } },
"severity": "warning",
"source": "links",
"code": "dead-link",
"message": "Link target \"guides/setup\" does not resolve to an existing document.",
"linkTarget": "guides/setup"
}
]
}
],
"warnings": [],
"fileCount": 1,
"errorCount": 0,
"warningCount": 1,
"ran": ["markdownlint", "frontmatter", "links"]
}The audit's top-level ran array reports the selected source families: enabled document linters plus links unless link validation is off. OKF's document and project-tree checks share the public okf family, so ran reports selection, not the number of internal validators or a pass verdict.
Broken links are warnings by default. The project's validation.links setting — Settings ▸ This project ▸ Content rules — decides both whether they appear and at what severity: warning (the default), error to gate CI on them with --errors-only, or off to drop them from the plane entirely. Content-rule findings keep their own severities, so --errors-only covers both planes at once. Skill documents are the one exception: their link findings are always excluded, whatever this setting says, because a skill's links name files it creates only at runtime.
AI agents
Agents get the same signal you see, across three surfaces. See the MCP reference for the full tool list.
The lint tool
Lints a single document, or audits the project when document is omitted (path scopes it to a folder or file). fix: true — which requires document — auto-fixes fixable rules in place, attributed and live in the preview, the same result as the editor's Fix action.
A single-document call returns a readable summary. Configuration problems appear under a Lint incomplete warning block, and the closing hint tells the agent whether fix: true would help:
docs/guide.md: 2 warnings
⚠ line 1 frontmatter/required: Frontmatter property "owner" is required
⚠ line 2 frontmatter/enum: Frontmatter property "status" must be one of: draft, review, published (got "shipped")
Lint incomplete — 1 warning (findings may be partial):
⚠ frontmatter schema .ok/schemas/missing.schema.json: cannot read (ENOENT: no such file or directory, …)
None are auto-fixable — these need content edits via `edit`/`write`.
Checks run: frontmatter.With no linter enabled the report closes with No checks ran. instead, the same string ok lint prints.
The structured content is close to ok lint --json, with three differences:
ok lint --json | MCP lint | |
|---|---|---|
| Project path | contentDir | cwd |
fixedCount counts | files rewritten | problems resolved |
| Cap fields | — | omittedFileCount, per-file omittedDiagnosticCount, omittedWarningCount |
Everything else matches: files[].diagnostics with 0-based range, severity, source, code, message, plus errorCount, warningCount, fileCount on an audit, configuration or degradation warnings, and ran for the selected source families. Audit output is capped project-wide at 10 warnings, plus 10 files × 10 diagnostics per file — the text channel marks the remainder with "… and N more", the structured channel with the three cap fields above. Counts always reflect the full scan, and re-running with a narrower path recovers the detail. With fix: true the summary reports what was applied and what remains: Fixed 1 problem in docs/guide.md. followed by the unfixable findings.
The audit tool
The agent-side ok audit — content-rule problems and broken links in one read-only call, grouped by file, same 10 × 10 + 10-warning cap with omittedWarningCount when warnings are dropped, no fix shape. Its text and structured results both report ran. If a selected validator degrades, its family remains in ran and the warnings channel explains why; a partial degradation may still have contributed findings.
Write responses
Every write response carries validation findings for the document it touched, on two channels. Both nest under document in the structured content, and both are advisory: a finding never blocks the write.
"document": {
"brokenLinks": [
{ "href": "./guides/setup", "resolvedTo": "guides/setup", "reason": "no-such-doc" }
],
"warnings": [
{
"kind": "lint-violation",
"source": "frontmatter",
"code": "enum",
"message": "Frontmatter property \"status\" must be one of: draft, review, published (got \"shipped\")",
"severity": "warning",
"line": 2,
"column": 1
}
]
}warnings carries up to 10 findings across the whole validation plane — lint violations and broken links alike, honoring the project's validation.links setting. Positions are 1-based (line/column), ready to echo back to a human, and a links finding adds linkTarget. The field is present only when the write produced findings.
brokenLinks is the dedicated link channel, and unlike warnings it is always present — an empty array is the positive "every outbound link resolves" confirmation, which saves a separate links({ kind: "dead" }) round-trip. Each entry names the href exactly as authored, so an agent can grep for it. reason is no-such-doc (resolved to a docName that doesn't exist), no-such-file (a linked asset or source file missing from disk), or unresolvable (an empty href, or a relative path escaping the content root); resolvedTo is null for unresolvable.
See also
- markdownlint: the rules, your
.markdownlint.*config, and the rule browser - Frontmatter schemas: JSON Schema validation of document frontmatter, and the schema editor
- Editor: source mode, WYSIWYG, and the document panel
- Configuration reference:
config.ymland what's shared via git - MCP reference: the tools agents use to read and write your project
Folders and templates
Describe a folder with its own properties, and offer reusable templates that give new documents their starting content and properties.
markdownlint
The markdownlint linter for content rules — VS Code-parity defaults, your project's native `.markdownlint.*` config, and a no-code browser for all 53 rules.