OpenKnowledge
Content rules

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.

Two 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. 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.

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 problems

The 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.

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
}

Conventions to know:

  • JSON ranges are 0-based and end-exclusive (LSP-aligned); the text report displays 1-based positions.
  • fixes appears only on auto-fixable findings — its presence is how tooling knows --fix would resolve the problem.
  • fixed is true on each file --fix rewrote, and the top-level fixedCount counts files, not problems.
  • The top-level warnings array carries the configuration problems.

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.

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 problems

Unlike 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
}

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.

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 ride along as lines, 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")
  ⚠ 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`.

The structured content is close to ok lint --json, with three differences:

ok lint --jsonMCP lint
Project pathcontentDircwd
fixedCount countsfiles rewrittenproblems resolved
Cap fieldsomittedFileCount, per-file omittedDiagnosticCount

Everything else matches: files[].diagnostics with 0-based range, severity, source, code, message, plus errorCount, warningCount, fileCount on an audit, and the configuration-problem warnings. Audit output is capped at 10 files × 10 diagnostics per file — the text channel marks the remainder with "… and N more", the structured channel with the two 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 cap, no fix shape.

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