How ctxgrd decides what to lint
ctxgrd is quiet by default – and this is on purpose. Understanding why requires understanding the single design decision that sets it apart from most frontmatter-walking linters: a file must claim intent before ctxgrd will touch it.
The problem with frontmatter as a signal
A naive linter infers “this is a document I should check” from the presence of a YAML frontmatter block. That inference works in a greenfield repo where ctxgrd is the only tool that uses frontmatter. It breaks everywhere else.
A Hugo site puts frontmatter on every content page, _index.md, and blog post.
A design-token system uses a DESIGN.md with version: and palette: keys. An
Obsidian vault embeds YAML for its own indexing. Notion exports carry frontmatter
on every note. None of these files are records that ctxgrd was configured to
lint – but a frontmatter-driven linter would fire on all of them, producing
diagnostics that the user has to enumerate into an [ignore] exclusion list just
to make the noise stop.
The deeper problem with negative declarations is that they compound. You exclude
**/CHANGELOG.md today; next quarter your team adopts a new tool that writes
frontmatter to **/DESIGN.md; the quarter after that, something else. The list
grows with every new tool in the repo. And a broad [ignore] entry silently
swallows real documents that land under the same path – so you may be leaving
genuine ADRs or runbooks unlinted with no warning.
Claims of intent
ctxgrd resolves this with a positive declaration instead of a negative one. Rather than asking “what should I skip?”, it asks “what has the user explicitly said belongs to ctxgrd?”
A file is a candidate for linting iff it makes one of two claims of intent:
- Id-claim. The frontmatter contains
id: <NAMESPACE>-<NUMBER>and<NAMESPACE>matches a configured namespace. Example:id: ADR-007. - Path-claim. The file’s path matches one of the globs in
[<NAMESPACE>].paths. Example: a file atdocs/adrs/007-foo.mdwhen[ADR].paths = ["docs/adrs/**"]is configured.
Files that satisfy neither signal are silently skipped – no diagnostic, no hint, no noise. It does not matter whether they have frontmatter. ctxgrd has not been asked to lint them, so it does not.
This is why running ctxgrd on a fresh repo with no configuration produces
exactly zero output. It is not broken; it is waiting for intent.
Why “first touch is silent” is load-bearing
The silence on first touch is not an oversight to work around – it is the property that makes ctxgrd safe to install in any repo without a cleanup sprint.
When ctxgrd fires on files that were never meant to be ctxgrd documents, the
user is forced to choose between noisy output they cannot action and an
[ignore] list that grows indefinitely and may suppress real documents. Neither
is acceptable. Silent-by-default means ctxgrd can coexist with Hugo, design
tokens, and Notion exports from day one, and diagnostics, when they do appear,
are unconditionally actionable – they can only fire on files the user has
explicitly claimed.
The help text for core.id and core.frontmatter reflects this: it no longer
carries the “or add to [ignore].patterns if this file isn’t a ctxgrd document”
escape clause that older versions needed. Under intent-based classification, a
diagnostic implies the file is a ctxgrd document by intent, so the escape is
never needed.
Two ways to claim, two purposes
Id-claims and path-claims serve different needs and complement each other.
An id-claim works file by file. It is the natural fit for repos where
documents are scattered across directories or already follow a naming convention
that ctxgrd recognises. It also works without any ctxgrd.toml change – just
add id: ADR-001 to a file and it enters the linting scope immediately.
A path-claim works directory by directory. It is the right shape when a team
stores all documents of a type together: docs/adrs/, docs/runbooks/, or any
glob that covers the location. With path-claims, a new document dropped into the
right directory is immediately linted without requiring any frontmatter id. The
ctxgrd init command pre-fills [<NS>].paths automatically when it detects
conventional directories – so most users get path-claims without writing a line
of config.
When a file matches both an id-claim and a path-claim, the id-claim wins for
namespace classification. When a file matches two different namespaces’ path
globs and has no id to resolve the ambiguity, ctxgrd emits a cfg.path-conflict
configuration error naming both namespaces and the file path, rather than
silently picking one. Overlap in path globs is almost always a mistake; the
error surfaces it immediately.
Markdown only, and why
ctxgrd lints only .md files with YAML frontmatter and <NAMESPACE>-<NUMBER>
IDs. It does not lint Turtle files, YAML, JSON, source code, or any other format.
This boundary exists for two reasons. The first is focus: structured markdown records – ADRs, PRDs, runbooks, post-mortems – share a common shape and a common lifecycle (status fields, dependency links, required headings). A linter built around that shape produces tight, actionable diagnostics. Expanding to other formats would dilute that focus and require generalising the rule model.
The second reason is determinism. Same input, same output, same exit code – the invariant that makes ctxgrd safe in CI and in agent loops. Non-markdown formats would introduce format-specific parse ambiguities and edge cases that are difficult to contain. The supported escape hatch for teams that do want to lint other formats is an external source script: a small program that reads the non-markdown format and emits document envelopes that ctxgrd’s rule engine can process. The core remains markdown-only; the extension surface is explicit and contained.
Exit codes and machine readability
ctxgrd is driven as often by agents as by people. Every command follows the same exit-code contract:
| Code | Meaning |
|---|---|
0 | No diagnostics – clean. |
1 | Diagnostics reported. |
2 | Kernel or configuration error. |
This means a CI step or an agent loop can branch on outcome without parsing text.
--format json streams diagnostics as structured objects on stdout. Diagnostic
hints and progress messages go to stderr, keeping stdout a clean pipe target.
The determinism of intent-based classification is what makes these exit codes
trustworthy. Because ctxgrd only lints files you have explicitly claimed, a 0
means “every document you declared clean is genuinely clean” – not “nothing
fired because ctxgrd quietly skipped half your repo.”
One file, one namespace
A file is claimed by exactly one namespace. That single-owner rule keeps rules deterministic: each namespace has its own rule list, required headings, and allowed status values, and mixing two namespaces’ rules against a single file would produce undefined behavior.
Cross-file rules – for example, a rule asserting that a CLAUDE.md links to the entry guide – fire from the namespace that owns one side and read the other file from disk. They do not introduce a second namespace claim on the far file.
The done-signal question
One consequence of pooling documents by type is that ctxgrd status --exit-code
on the whole project asks “is the entire repo done?” – which is almost never the
right question when driving a single feature. The --lineage flag narrows the
scope to one feature’s dependency graph. See
Polling a feature done-signal in an agent loop
for the mechanics.
Further reading
- Getting started with ctxgrd – configure your first namespace and run the linter.
- Configuring namespaces –
[<NS>].paths,[ignore], and the full claim model in depth. - Rule packs – built-in namespace bundles that ship pre-configured path claims.