CLI reference

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). A dependency cycle is a lint violation (core.dep-cycle), so it exits 1.
2Config or kernel error – ctxgrd.toml unreadable or malformed.

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. One of claude, codex, gemini or antigravity. Incompatible with --recursive.
--pack <NAME>–Lint only the namespaces this project’s ctxgrd.toml stamped # pack: <name>. Repeatable and comma-separable. A name that matches nothing exits 2.
--namespace <NS>–Lint only the named namespaces. Repeatable and comma-separable; combines with --pack. A name the config does not declare exits 2.
-r, --recursiveoffLint every ctxgrd.toml found under the resolved root, each as its own project.
-V, --version–Print 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.

When there is no ctxgrd.toml and no document claims intent, nothing is linted. ctxgrd then reports error[cfg.missing] and exits 2.

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": "...", "summary": {...}, "diagnostics": [...], "kernel_messages": [...], "hint": "..."} – stable schema; see Output format notes. 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, optional claimed: and abandoned: lists, an optional settled on open work: block naming any terminal document that still depends on open work, and a tip: line.

A document a claim holds (see claim) is never listed as ready. claimed: lists live claims and abandoned: lists stale ones, each row with its holder and idle time. When either list is present, the census line ends with · C claimed · A abandoned.

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 or a rejected --granularity value. A dependency cycle exits 1; ctxgrd lint reports it as core.dep-cycle.

Messages from external sources, such as src.warning and src.runtime-error, go to stderr. They never reach the status report on stdout.

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 install <path|git-url>[@ref]
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)
install <path|git-url>[@ref]02 (source unreadable or name clash)
outdated0 (no pack has moved – blocks with no baseline may still be listed)1 (a pack has moved)
migrate0 (nothing left to reconcile)1 (blocks left for manual resolution)

pack install copies a pack from a folder or a git URL into ~/.ctxgrd/packs/<name> and records its source and version. Run it again to update the pack. --name <NAME> sets the pack name (default: the folder or repository name). --force replaces a folder of that name that pack install did not create. --format json emits the install record.

pack outdated reports two kinds of drift: blocks in ctxgrd.toml whose pack has changed since they were stamped, and installed packs whose source has moved. A source it cannot reach is listed but not judged.

When a pack in ~/.ctxgrd/packs has the same name as a built-in pack, the pack commands and init read the global copy and print a warning on stderr. Delete or rename the folder to use the built-in pack.

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

Some packs depend on another (workflow depends on handoff, so its [SPEC]/[TASK]/[PROMPT] pipeline gets a [HANDOFF] to carry work between sessions). pack add applies the dependency first, reporting each pack it touched:

Added pack 'handoff' — 1 namespace, 0 rule families.
Added pack 'workflow' — 3 namespaces, 2 rule families.

pack show <name> prints a Depends: <packs> line whenever the pack declares one. --format json carries the same list under a top-level depends key.

See Keeping adopted packs current for migration workflows, and Writing session handoffs for the handoff pack itself.


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
ctxgrd init --domain legal          # apply every pack that declares the legal domain
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.
--domain <NAME>Apply every discovered pack whose pack.toml declares # domain: <name>. Default: software. Any other domain drops the software starter namespaces unless you also pass --namespaces. A domain no discovered pack declares exits 2 (init.domain); install its packs first with pack install.
--format <FORMAT>rich (default), simple, json. json emits {"status": "created" | "exists", "path": "..."}.
--forceOverwrite an existing ctxgrd.toml.

Exit codes: 0 on success. When ctxgrd.toml already exists and --force is absent, init leaves it unchanged, reports status exists and exits 0.


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
ctxgrd hooks codex
ctxgrd hooks gemini
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. Refuses a pre-commit hook no *grd tool wrote (writes nothing, exit 2) unless --force.
claudePrint the Claude Code Stop-hook wiring and detect whether it is already installed. Print-and-detect only – never writes settings.json.
codexPrint the Codex Stop-hook wiring for .codex/hooks.json (project) or $CODEX_HOME/hooks.json (global), and detect whether it is installed. Codex runs the hook only in a trusted project, and only after you approve the entry with /hooks in a Codex session. Never writes.
geminiPrint the Gemini CLI AfterAgent-hook wiring for .gemini/settings.json (project) or ~/.gemini/settings.json (global), and detect whether it is installed. Gemini CLI runs project hooks only in a trusted folder. Never writes.

Exit codes: 0 on success; 2 on config error, or when install refuses a foreign pre-commit hook.


harness

Package ctxgrd for an agent harness.

ctxgrd harness export claude
ctxgrd harness export gemini --domain legal --out ./ctxgrd-gemini
SubcommandDescription
export <harness>Write a local plugin folder for claude, codex, gemini or antigravity: its manifest, the turn-end hook (ctxgrd lint --harness <harness>), a ctxgrd skill and, with --domain <name>, every discovered pack declaring that domain. Claude Code and Codex bundles also hold a local marketplace file. Prints the commands that install the bundle. --out <dir> (default ./ctxgrd-<harness>) is refused when it is not empty, unless --force, which deletes nothing. --format json emits {harness, path, files, packs, install}.

Exit codes: 0 on success; 2 when --out is not empty, the domain is unknown, or a file cannot be written.


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.


namespaces

List the document types ctxgrd.toml declares. Each row shows how the namespace claims files, its paths, its owner, when to write one (its summary), and how many documents it holds. Run it before you create or name a document.

ctxgrd namespaces
ctxgrd namespaces --namespace ADR
ctxgrd namespaces --for docs/adrs/042-use-rust.md
ctxgrd namespaces --format json
FlagDescription
--namespace <NS>Show only that namespace’s row.
--for <PATH>Show the row of the namespace whose paths claim the file. No row when none does.
--format <FORMAT>rich (default) or json: a bare array whose rows always carry namespace, claim (path or id), paths, owner, summary, new and documents. A missing owner or summary is null.

A namespace with no summary still prints, with — in the table. One hint on stderr says how many lack one; ctxgrd lint reports each as cfg.namespace-undescribed.

Exit codes: 0, also when no row matches; 2 on a config error.


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.


claim

Claim a document, so a parallel session does not run it too. The namespace must bind core.claim-complete or core.claim-stale; the handoff pack binds both on [HANDOFF].

ctxgrd claim HANDOFF-12
ctxgrd claim HANDOFF-12 --session 7f3c2a90-5b1e-4c8e-9d2f-1a6b3c4d5e6f
ctxgrd claim HANDOFF-12 --take-over
FlagDescription
--session <ID>The caller’s session, written as claimed_session. Default: $CLAUDE_CODE_SESSION_ID. The caller holds a claim whose claimed_session equals it.
--take-overTake over a claim idle longer than [NS."core.claim-stale"] days (default 7). Never applies to a live claim.
--format <FORMAT>rich (default) or json.

Under a lock directory beside the file, claim writes the claim status (claim_status, default in_progress), claimed_by (<host>-<pid>-<unix time>), claimed_at (RFC 3339, UTC) and claimed_session. It changes no other byte of the file. Of two sessions that claim at the same moment, one gets the claim and the other is told it is held.

JSON status is one of claimed, renewed (the caller already held it, and claimed_at moved to now), taken-over, held, stale or settled.

Exit codes: 0 claimed, renewed or taken over; 1 held by another live session, stale without --take-over, or already finished, with nothing changed; 2 unknown ID, a namespace without claim rules, or a lock that stays taken.


release

Give a claim up: remove claimed_by, claimed_at and claimed_session, and set the status.

ctxgrd release HANDOFF-12 --status consumed
ctxgrd release HANDOFF-12 --status pending --force
FlagDescription
--status <VALUE>Required. consumed when the work is done, pending when it stopped halfway. Checked against the namespace’s core.allowed-values status list.
--session <ID>The caller’s session. Default: $CLAUDE_CODE_SESSION_ID.
--forceRelease a claim another session holds.
--format <FORMAT>rich (default) or json.

JSON status is released, not-claimed or not-holder.

Exit codes: 0 released; 1 not claimed, or held by another session without --force, with nothing changed; 2 unknown ID, or a status the namespace does not allow.


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>nearest ancestor with a ctxgrd.tomlProject root. See Finding the 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>nearest ancestor with a ctxgrd.tomlProject root. See Finding the 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.toml – core.acceptance-complete and core.requirement-status are two; see Polling a feature done-signal in an agent loop.


Output format notes

--format json (lint)

{
  "exit_code": 0 | 1 | 2,
  "root": "<path>",
  "summary": {"errors": N, "warnings": N, "infos": N, "files": N, "namespaces_undeclared": N},
  "diagnostics": [
    {
      "code": "<rule-code>",
      "severity": "error" | "warning",
      "message": "<text>",
      "file": "<path>",
      "location": "<path>",
      "line": <N> | null,
      "col": <N> | null,
      "help": "<text>" | null
    }
  ],
  "kernel_messages": [{"severity": "warning", "code": "<code>", "message": "<text>"}],
  "hint": "<text>"
}

line and col are null when the finding has no position. location is where the finding points. hint is present only when the run has findings. Messages from external sources, such as src.warning, arrive in kernel_messages. 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>", ...],
      "claim": {
        "claimed_by": "<holder>" | null,
        "claimed_at": "<RFC 3339>" | null,
        "claimed_session": "<session>" | null,
        "idle_seconds": N | null,
        "abandoned": true | false
      }
    }
  ]
}

claim is present only on a document a claim holds.

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.