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:
{
"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"
}
}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:
metabox— envelope version, always"1"for now.type— what kind of record this is (annotation,epoch,dependency, ...). Defaults to"annotation"when absent.subject— the artifact this record is about, usually a path likesrc/parser.rs.issuer— who or what wrote it, as a URI (mailto:,https:, orurn:).issuer_type— optional;human,ai,tool, orunknown.created_at— RFC 3339 timestamp.id— a BLAKE3 hash of the record itself, so identical records always get identical IDs.body— the type-specific payload.
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:
annotation— a quality signal (concern, praise, blocker, comment, ...). The one you'll write most often.epoch— a compaction snapshot. Synthesizes a chunk of history into one summary record.dependency— declares that one subject depends on others, so layered tools can propagate signals across edges.license— a license declaration for a subject.security-advisory— a known vulnerability or weakness.perf-measurement— a performance measurement against a baseline.
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.