The .qual format

A friendly tour of the .qual file format.

A .qual file is how Qualifier records structured observations about code. Concerns, suggestions, anything worth keeping is stored in plain UTF-8 encoded JSONL. Append-only, merge friendly, one record per line, sitting next to your source. This page is the orientation: enough to read one fluently and write one by hand. The full reference is the spec.

Files live next to your source. An annotation about src/parser.rs typically lives in src/.qual (one file per directory) or src/parser.rs.qual (one file per source file). Either works, and git treats them like any other text file. git blame, git log, git diff all do the right thing.

A two-record .qual file looks like this:

{"metabox":"1","type":"annotation","subject":"src/auth.rs","issuer":"mailto:alice@example.com","issuer_type":"human","created_at":"2026-02-24T10:00:00Z","id":"a1b2...","body":{"kind":"concern","summary":"SQL injection risk in login handler"}}
{"metabox":"1","type":"annotation","subject":"src/auth.rs","issuer":"urn:anthropic:claude","issuer_type":"ai","created_at":"2026-02-24T11:00:00Z","id":"e5f6...","body":{"kind":"comment","references":"a1b2...","summary":"Switched the handler to parameterized queries in 8f3c2a1"}}

A human reviewer flags a concern; an AI agent replies with a fix, threaded to the original via references.

Anatomy of a record

Those two records, expanded side by side:

Comment (Record 1)
{
  "metabox": "1",
  "type": "annotation",
  "subject": "src/auth.rs",
  "issuer": "mailto:alice@example.com",
  "issuer_type": "human",
  "created_at": "2026-02-24T10:00:00Z",
  "id": "a1b2...",
  "body": {
    "kind": "concern",
    "summary": "SQL injection risk in login handler"
  }
}
Response (Record 2)
{
  "metabox": "1",
  "type": "annotation",
  "subject": "src/auth.rs",
  "issuer": "urn:anthropic:claude",
  "issuer_type": "ai",
  "created_at": "2026-02-24T11:00:00Z",
  "id": "e5f6...",
  "body": {
    "kind": "comment",
    "references": "a1b2...",
    "summary": "Switched the handler to parameterized queries in 8f3c2a1"
  }
}

There are two halves. The envelope (everything outside body) is who-said-what-about-which-subject-when. The body is what they actually said. Every record in a .qual file has the same envelope shape — that's why the two records above look nearly identical at the top. The body varies by record type.

That split is intentional: tools that don't understand a particular body schema can still read the envelope and route the record sensibly.

The envelope

Every record carries the same eight fields. One sentence each:

That's the Metabox envelope, specced separately so other tools can adopt the same shape. If you want field-level depth (validation rules, URI schemes, canonical ordering), the spec has it.

What kinds of records?

Annotations are the primary record type, but .qual is a substrate. The format supports any structured record type that fits the envelope, and tools are required to preserve records they don't understand. That means a .qual file can carry ecosystem signals from many sources without the format itself needing to grow.

The types defined in the spec today:

The first three ship in the CLI today. The rest are spec-level: the format defines them so adopters can produce them, and any tool that round-trips a .qual file will preserve them whether or not it knows how to interpret them.

For full schemas, see section 3 of the spec.

Threads and resolution

Two body fields turn a flat list of records into a conversation.

references is a lightweight "re:" link. Bob sees Alice's concern, replies with a comment, and points body.references at Alice's record ID. Both records stay active; the link is purely for threading.

supersedes is stronger. A new record with body.supersedes set to a prior record's ID withdraws the prior record from the active set. That's how you "edit" something in an immutable, append-only file: write a new record that replaces the old one. The resolve annotation kind is the canonical way to close something out, retiring whatever it supersedes.

{
  "metabox": "1",
  "type": "annotation",
  "subject": "src/parser.rs",
  "issuer": "mailto:bob@example.com",
  "created_at": "2026-03-01T10:00:00Z",
  "id": "b2c3...",
  "body": {
    "kind": "comment",
    "references": "a1b2...",
    "summary": "Good catch, fixed in 8f3c2a1"
  }
}

Tools render threads with tree-drawing characters so the conversation reads naturally in a terminal.

Custom body fields

The annotation body has a small set of well-known fields (kind, summary, detail, references, supersedes, span, tags, suggested_fix), but the format doesn't constrain what else you put there. A team that wants numeric scoring can attach a score field to each annotation; a tool that imports SARIF can stash the original ruleId. Records that round-trip through tooling preserve unknown body fields verbatim.

This is one example of how an ecosystem can layer quality signals on top of the substrate. The spec sketches a numeric score field as an abstract example; the qualifier CLI itself doesn't compute or gate on scores.

Why JSONL?

Three reasons, all about the format being boring on purpose.

Append-only means clean merges. Two people writing annotations to the same file at the same time produce two new lines at the end. Git merges them trivially because there's no editing in place. You can push straight to main.

Content-addressed IDs are stable. Each record's id is a hash of its canonical form. Identical records produce identical IDs across machines, languages, and time. That's what makes references and supersedes work without a coordinating server.

Human-writable means anyone can produce it. A reviewer with a text editor, a CI script with jq, an AI agent with a function call. Same format, same file, no special tooling required to participate. The CLI is a convenience, not a gatekeeper.

Where next?