CLI reference

ctxgrd 2.1.0 – every command, its flags, the exit-code contract, and the output formats.

Rule codes are not enumerated here. ctxgrd rules resolves them against your own ctxgrd.toml, including external rules, and cannot go stale; the rule tour is generated from the same source. A hand-copied list in this file drifted for forty releases before anyone noticed (BUG-044), which is the argument for not keeping one.

Exit-code contract

The same contract applies to every command and every --format value.

CodeMeaning
0Clean – no diagnostics, no errors.
1Diagnostics present (lint violations, drift detected, done-gate not met).
2Config or kernel error – ctxgrd.toml unreadable, malformed, or a dependency cycle.

Scripts and agents branch on exit code; they do not need to parse text output. stdout is always a clean, parseable stream. Progress notes and hints go to stderr.

Global flags

These flags apply before any subcommand and to the default lint command when invoked bare.

FlagDefaultDescription
--root <PATH>nearest ancestor with a ctxgrd.tomlProject root. Passing it explicitly means exactly that directory and disables the upward search.
--format <FORMAT>richOutput format for lint. Values: rich, simple, json.
--harness <HARNESS>Emit a turn-end decision for the named agent harness. Only valid value: claude. Incompatible with --recursive.
-r, --recursiveoffLint every ctxgrd.toml found under the resolved root, each as its own project.
-V, --versionPrint version and exit.

Finding the project root

With no --root, ctxgrd walks up from the working directory to the nearest ctxgrd.toml — the same rule git, cargo, npm and go use, and the same one the sibling linters wrkgrd and trtlgrd already used. Running ctxgrd from docs/adrs/ lints the whole repository, exactly as running it from the root does.

Three consequences worth knowing:

  • --root <dir> never searches. It means that directory and nothing above it, which is how you lint a subtree deliberately.
  • Nearest wins. A subdirectory carrying its own ctxgrd.toml is a separate lint root with its own namespaces, not a fragment of the parent’s. -r descends from whichever root was resolved.
  • init is the exception. It scaffolds in the working directory and never walks up — it writes ctxgrd.toml, and --force overwrites, so searching upward would let ctxgrd init --force in a subdirectory destroy the repository’s real config.

When no ctxgrd.toml exists anywhere above, ctxgrd still lints id-claimed documents under the zero-config core rules — and now says so, with a cfg.zero-config warning naming the reduced rule set and the namespaces it applied to. The run still exits 0; the warning exists so a reduced run cannot be mistaken for a fully configured clean one.

Diagnostic paths are relative to the resolved root, not to your working directory. Running from a subdirectory, ctxgrd prints the root it resolved so the paths stay reconstructible:

$ cd docs/adrs && ctxgrd lint
root: /repo (diagnostic paths are relative to this, not to your working directory)
docs/adrs/001-x.md:3:1: error: ...

The line goes to stderr and is omitted when the root is your working directory. --format json carries the same value as a top-level root key. Editor quickfix from a subdirectory still needs an absolute path — see BUG-056.

Commands

lint (default)

Lint the document tree against ctxgrd.toml. Invoked when no subcommand is given.

ctxgrd lint
ctxgrd lint --format json
ctxgrd --root /path/to/repo lint --format json
ctxgrd -r                           # monorepo: lint all ctxgrd.toml files

Formats

ValueDescription
richColumn-aligned human table with coloured severity markers (default).
simplePlain text, one diagnostic per line – suitable for grep.
json{"exit_code": N, "root": "...", "diagnostics": [...], "kernel_messages": [...]} – stable schema. root is the resolved lint root every diagnostic file is relative to.

The --harness claude flag selects the Claude Code Stop-hook format (ADR-062) instead of a --format value; see ctxgrd hooks claude for wiring details.

Exit codes: standard contract (0 / 1 / 2).


status

Report the work queue: per-document readiness over the depends_on graph — what can be picked up now, what is waiting on what, and what is finished.

ctxgrd status
ctxgrd status --format json
ctxgrd status --lineage PRD-3 --exit-code
ctxgrd status --format mermaid
FlagDescription
--format <FORMAT>text (default), json, mermaid, dot.
--lineage <ID>Scope to one feature: the transitive dependents of <ID> plus <ID> itself.
--exit-codeProject the done-signal onto the process exit: 0 when no document in scope is blocked by a non-terminal dependency, 1 otherwise. Report is still printed; no file is modified.
--granularity <doc>Which graph mermaid/dot draw. doc – one node per document, coloured by readiness – is the only accepted value and is also the default; the flag is a no-op kept for compatibility. namespace is rejected (exit 2): the namespace-stage graph it named no longer exists.
--no-titlesDrop the document titles, leaving the bare <ID> <status> rows the queue printed before 2.2.0. Applies to text and json; the diagram formats label nodes by id and status either way.

Text output, in order: a census line (N documents · R ready · B blocked · S settled), an optional ready: list, an optional blocked: list, an optional settled on open work: block naming any terminal document that still depends on open work, and a tip: line.

Since 2.2.0 each row also names its document:

ready:
  ADR-002      draft    Retention
  BUG-001      open     Drift
  HANDOFF-001  pending  Carry on

blocked:
  SPEC-001  draft  Reconciliation  ← ADR-002

The title column is fitted to 60 characters, with a trailing where a longer one was cut; --format json always carries it whole. An ID identifies a document without describing it, so the queue was unreadable to anyone not already holding the corpus in their head – use --no-titles only where the report is injected into a context on every run and the cost matters.

The mermaid and dot formats emit DAG diagram source only; they do not render an image.

Exit codes: 0 clean, or nothing in scope blocked (when --exit-code); 1 diagnostics, or something in scope blocked (when --exit-code); 2 config error, dependency cycle, or a rejected --granularity value.

See Polling a feature done-signal in an agent loop for agent-loop patterns, including what --exit-code does and does not guarantee.


rules

Introspect the rule set that would actually run against this tree.

ctxgrd rules
ctxgrd rules --format json
ctxgrd rules --namespace ADR
ctxgrd rules core.cross-ref         # detail view for one rule
FlagDescription
--namespace <NS>Filter output to one namespace.
--format <FORMAT>rich (default), simple, json. json includes full descriptions.
[RULE_CODE]Optional positional: print a detail block for one rule code.

Each row carries a binding: configured when ctxgrd.toml declares the namespace, zero-config when nothing does and the zero-config fallback supplied it. The distinction matters because a namespace your config never declared is still linted — with the zero-config set instead of your full set — so zero-config is the prompt to run ctxgrd init or ctxgrd pack add. The human table shows the column only when at least one row is zero-config; --format json always carries the field.

Namespaces your config declares but has no documents for yet are reported too, so a project that wrote its config before its first document still gets an answer. That is why the row count can exceed the rule count in lint’s ok: line, which counts only namespaces that hold documents.

Exit codes: standard contract.


pack

Inspect and apply rule packs – reusable namespace bundles.

ctxgrd pack list
ctxgrd pack show <name>
ctxgrd pack add <name>
ctxgrd pack outdated
ctxgrd pack migrate
ctxgrd pack migrate --dry-run
ctxgrd pack migrate --dry-run --format json
SubcommandExit on cleanExit on action needed
list0
show <name>02 (unknown pack)
add <name>02 (unknown pack or config error)
outdated0 (no pack has moved – blocks with no baseline may still be listed)1 (drift present)
migrate0 (nothing left to reconcile)1 (blocks left for manual resolution)

pack add appends pack blocks to ctxgrd.toml with a provenance comment (# pack: <name>@<version> sha:<fingerprint>). It never clobbers an existing namespace block.

See Keeping adopted packs current for migration workflows.


init

Write a starter ctxgrd.toml.

ctxgrd init
ctxgrd init --namespaces ADR,PRD,SPEC
ctxgrd init --pack project-docs
ctxgrd init --stdout                # print to stdout, do not write a file
ctxgrd init --force                 # overwrite an existing ctxgrd.toml
FlagDescription
--namespaces <LIST>Comma-separated namespace names. Default: ADR.
--pack <NAME>Apply one or more packs after writing the base config. Repeatable.
--stdoutPrint to stdout instead of writing the file.
--forceOverwrite an existing ctxgrd.toml.

Exit codes: 0 on success; 2 if ctxgrd.toml already exists and --force is absent.


new

Scaffold a new document, or a new external rule script.

ctxgrd new ADR "Adopt PostgreSQL"
ctxgrd new PRD "Billing overhaul" --id 12
ctxgrd new rule design.token-check "token references must resolve"
ctxgrd new ADR "Use Redis" --stdout    # print to stdout, do not write a file
Argument / flagDescription
<NAMESPACE>Namespace (e.g. ADR, PRD), or the literal rule to scaffold an external rule.
<TITLE>Document title, or rule code like design.foo when namespace is rule.
[DESCRIPTION]One-line rule description – only used when namespace is rule.
--id <N>Explicit document number. Default: max(existing) + 1.
--out <DIR>Target directory override.
--stdoutPrint scaffolded content to stdout.

Exit codes: 0 on success; 2 on config error or unknown namespace.


hooks

Manage git hooks that gate commits on ctxgrd.

ctxgrd hooks install
ctxgrd hooks claude
SubcommandDescription
installInstall a composable, tracked pre-commit gate (.githooks/pre-commit.d/10-ctxgrd) that runs ctxgrd before each commit, and set core.hooksPath .githooks. Composes with a sibling *grd tool’s gate (e.g. wrkgrd) rather than claiming the single hook slot; respects an existing custom core.hooksPath.
claudePrint the Claude Code Stop-hook wiring and detect whether it is already installed. Print-and-detect only – never writes settings.json.

Exit codes: 0 on success; 2 on config error.


refs

List every location pointing at a document ID: the document itself (if file-backed), depends_on references from other documents, body cross-ref tokens, and reference-scanner hits.

ctxgrd refs ADR-001
ctxgrd refs PRD-12 --format json
FlagDescription
--format <FORMAT>rich (default), simple (one <file>:<line>:<col> per line), json.

Output is deterministic so callers can diff across runs.

Exit codes: standard contract.


list

List ingested documents grouped by namespace.

ctxgrd list
ctxgrd list --namespace ADR
ctxgrd list --format json
ctxgrd list --format markdown
FlagDescription
--namespace <NS>Filter to one namespace.
--format <FORMAT>rich (default), markdown (H2 + pipe table per namespace), json.

Exit codes: standard contract.


pin

Manage commit pins on documents (see ../namespaces.md for pin configuration).

ctxgrd pin --bless ADR-001
ctxgrd pin --bless ADR-001 --force
FlagDescription
--bless <ID>Re-pin the named document’s pin.commit to the current HEAD.
--forceBless even when scoped paths have uncommitted changes.

Exit codes: 0 on success; 2 on config error or when uncommitted changes are present without --force.


lsp

Start the Language Server Protocol server over stdio.

ctxgrd lsp

The LSP server streams diagnostics to editors as documents change. See ../editors.md for editor integration.

Exit codes: 0 on clean shutdown; 2 on startup error.


serve

Serve a read-only, graph-aware web view of the governed docs on loopback.

ctxgrd serve
ctxgrd serve --port 8080
FlagDefaultDescription
--port <PORT>0TCP port to bind on 127.0.0.1. The default lets the OS pick a free one.
--root <PATH>.Project root.

Renders the namespace index, per-document pages with server-side markdown, clickable depends_on edges, and the status work queue. Read-only and loopback only – it never writes and never binds a public interface.

Prints one line to stdout so an agent can discover the bound port; logs go to stderr:

{"url":"http://127.0.0.1:53412"}

That is why --port 0 is the default: parse the line rather than guessing a port, and nothing collides.

Exit codes: 0 on clean shutdown; 2 on config error or bind failure.


changelog

Generate CHANGELOG.md from the document graph rather than by hand.

ctxgrd changelog            # print to stdout
ctxgrd changelog --write    # regenerate CHANGELOG.md in place
ctxgrd changelog --check    # exit 1 if the file is stale
FlagDefaultDescription
--writeoffRegenerate CHANGELOG.md in place.
--checkoffRegenerate to memory and diff against disk; exit 1 if they differ. Writes nothing.
--format <FORMAT>richrich, simple, or json (versions → sections → entries).
--root <PATH>.Project root.

Each whitelisted document is attributed to the first release whose tag tree marks it terminal, so released sections are immutable – they are read from tags, not from the working copy. Entries below the cutover marker are hand-authored history and are never regenerated.

--check is the cargo fmt --check contract, for CI. Note that a wrong entry is corrected with a changelog: frontmatter field on the source document; deleting the line from CHANGELOG.md only makes --check stale forever.

See Generating a changelog for the configuration.

Exit codes: 0 when clean or written; 1 when --check finds drift; 2 on config error.


docs

Print an end-user guide bundled with the binary.

ctxgrd docs rules
ctxgrd docs namespaces
ctxgrd docs sources
ctxgrd docs packs

Rule codes

Rules are identified by <prefix>.<name>. The prefix is the origin: core.* is built into the binary and available to any namespace that enables it, and every other prefix comes from a pack (agents.*, guide.*, design.*, todo.*, checklist.*, and so on) and applies only to the namespaces that pack defines.

This guide deliberately does not list them. Two commands answer it exactly, both resolved against the binary you have installed:

ctxgrd rules                      # every rule resolved against your ctxgrd.toml
ctxgrd rules --format json        # the same, machine-readable
ctxgrd pack show <name>           # what one pack binds, before you add it

ctxgrd rules includes external rules from rules/, which no static list could know about, and it moves when the registry moves. The published rule tour is generated from the same output if you want to browse without installing.

Some rules are opt-in and fire only when named in ctxgrd.tomlcore.acceptance-complete is the common one; see Polling a feature done-signal in an agent loop.


Output format notes

--format json (lint)

{
  "exit_code": 0 | 1 | 2,
  "diagnostics": [
    {
      "code": "<rule-code>",
      "file": "<path>",
      "line": <N>,
      "severity": "error" | "warning",
      "message": "<text>"
    }
  ],
  "kernel_messages": ["<text>"]
}

stdout is a single JSON object. The exit code embedded in the object matches the process exit code.

--format json (--recursive)

{
  "recursive": true,
  "exit_code": 0 | 1 | 2,
  "roots": [
    { "root": "<path>", "exit_code": N, "diagnostics": [...], "kernel_messages": [...] }
  ]
}

status --format json

{
  "documents": [
    {
      "id": "<ID>",
      "namespace": "<NS>",
      "title": "<title>",
      "status": "<status>" | null,
      "ready": true | false,
      "blocked_by": ["<ID>", ...]
    }
  ]
}

ready is true when the document is itself non-terminal and nothing blocks it. blocked_by lists only the non-terminal dependencies currently holding the document back – it empties once each one reaches a terminal status, even if the document itself has not.

title (2.2.0) is the document’s title: – or name: – frontmatter value, whole and never truncated; a document declaring neither yields "", not null. It is the one key that can be absent: --no-titles omits it.

With --lineage <ID>, the object also carries "lineage": "<ID>" and, only when the lineage shares a document with another root, "shared": ["<ID>", ...] naming the other root(s). Pipe to jq or any JSON processor – stdout is a clean stream.


Configuration

ctxgrd reads ctxgrd.toml from the nearest ancestor directory that has one, or from --root when you pass it (see Finding the project root). Run ctxgrd init to create one. See ../namespaces.md for the full config schema and ../packs.md for pack adoption.