An open format for AI conversations — spec v1 · [email protected] · MIT
One file, one conversation, kept whole.
A .curt file is UTF-8 JSON — the whole thread in one
file. Images and tool calls ride verbatim, unknown fields survive
foreign readers, and a corrupted file is rejected whole, never
half-imported. Born in Curtis AI Chat, owned by no one.
npm install curt-format
- conformance27 vectors · passing in CI
- audit2026-10-14 · addendum 11-01 · public
- implementations11 — read, write, or both
The envelope, and what rides inside.
A .curt file is UTF-8 JSON, tab-indented by convention.
The generator field lets a writer sign its work —
readers must ignore it: presence, absence, or value never affects
parsing. Declared capabilities ride in extensions.
{
"curt": "curtis-conversation",
"version": 1,
"generator": "[email protected]",
"extensions": ["images-inline", "usage-metadata"],
"conversation": {
"id": "conv_001",
"title": "Thursday's thread",
"createdAt": 1760000000000,
"updatedAt": 1760000600000,
"messages": [
{ "id": "m1", "role": "user",
"content": "hello", "timestamp": 1760000000000 },
{ "id": "m2", "role": "assistant",
"content": "hi back", "model": "…",
"timestamp": 1760000001000 }
]
}
}
The envelope, field by field:
| field | required | meaning |
|---|---|---|
| curt | yes | magic — must be exactly "curtis-conversation" |
| version | yes | format version; this spec is 1 |
| generator | no | provenance, "writer-name@version" — readers must ignore |
| extensions | no | names from the registry — never a validation input |
| conversation | yes | id, title, createdAt, updatedAt, messages |
| role | yes | user · assistant · system · tool |
| model | no | provider model id, typically on assistant messages |
Writers may attach extra fields anywhere — tokens, images, tool-call linkage — and readers must preserve them on round-trip where possible and never fail on them. Full field tables live in the spec; a non-normative JSON Schema ships for tooling. The reference implementation is TypeScript, zero dependencies:
import { serializeCurt, parseCurt, curtFilename } from 'curt-format';
const body = serializeCurt(conversation, '[email protected]');
const back = parseCurt(body); // CurtConversation | null — null means invalid
const name = curtFilename(back!); // 2025-10-09 my-thread 001.curt
Strict by design.
A READER ACCEPTS A FILE WHEN, AND ONLY WHEN
-
01
The body parses as JSON — after ignoring at most one leading UTF-8 BOM, because Windows editors prepend one anyway.
-
02
curtis exactly"curtis-conversation"andversionis exactly the number1. -
03
The conversation carries
idandtitleas strings, timestamps as numbers, and amessagesarray in which every entry passes message validation.
Anything else is a rejection — whole-file, not a best-effort parse.
Many conversations travel as a zip of .curt files.
Version 1 never changes meaning; later revisions only clarify, so
version-1 files stay readable forever.
Grows by extension, not by version.
Writers declare named capabilities in a top-level
extensions array. A reader that doesn't know one
ignores it and never rejects over it. An entry moves
draft → stable when two independent
implementations round-trip it. The registry is additive by rule:
new entries never change how existing v1 files parse.
| name | status | purpose | emitted by |
|---|---|---|---|
| images-inline | stable | image payloads ride inside content | plugin · porter · web · cloud · ai-tech |
| usage-metadata | stable | per-message tokens, cost, provider | porter · web · ai-tech |
| tool-linkage | draft | tool calls linked to their results | porter · ai-tech · curt-convert |
| agent-context | draft | agent and swarm provenance | plugin · porter · cloud · ai-tech |
| consensus-receipt | draft | signed k-of-n consensus bound to the transcript evidence | tractum |
Full field definitions per entry: EXTENSIONS.md.
Graded, not claimed.
Implementations run executable vectors from conformance/ and claim the levels they pass — self-reported, but reproducible from the vectors. The reference implementation runs them in CI and passes L1–L4.
- L1 · CORE must Magic and version exact-match, whole-file rejection, one leading BOM tolerated, unknown fields preserved on round-trip, any JSON-number timestamp accepted.
-
L2 · NAMED
should
curtFilenameimplements the<date> <slug> <suffix>.curtalgorithm exactly. -
L3 · BULK
may
The zip-of-
.curtconvention: entry names fromcurtFilename, readers never depend on names, corrupted entries reported rather than silently dropped. - L4+ · EXTENSION may Per-extension vectors — an extension's presence or malformation never causes rejection.
Ecosystem status — who is in, who passes what, where conformance has drifted — is tracked in CAPABILITIES.md, audited 2026-10-14.
Who reads and writes it today.
| consumer | role | r · w | carries |
|---|---|---|---|
| curtis plugin | import · export · backup | R + W | vendored copy |
| curt-openwith | offline viewer — double-click any .curt | R | hand-rolled port |
| porter | converter + verify oracle | R + W | vendored oracle |
| curtis-web | export endpoint | W | hand-rolled |
| curtis-cloud | shared library | R + W | vendored mirror |
| curtis-ai-tech | embedded converter + docs | R + W | inline copy |
| tractum | signed consensus receipts | R + W | own validator |
| curt-vcon | .curt ⇄ vCon bridge | R + W | npm curt-format |
| curt-convert | Claude Code JSONL → .curt | W | npm curt-format |
| curt-openwebui | Open WebUI exporter (local) | W | npm curt-format |
Every implementation here is the author's own — the first outside
consumer is still the open item. The three newest
(curt-vcon, curt-convert,
curt-openwebui) depend on the
curt-format package directly; the five original
application readers still vendor or hand-roll the rules, so a
spec revision lands in five places by hand. The ladder from ten
copies to one dependency is written down in the audit.
Argued at length, and on the record.
Why a conversation format needs to exist at all — the export trap, the portability right that fails without interoperable formats, the landscape of vendor exports, training formats, transport protocols, and containers — is written up as a preprint.
.curt: A Conversations-First, LLM-Native File Format with Executable Conformance for AI Chat Portability. Jordan Newell, 2026. preprint · DOI 10.5281/zenodo.23864677
Full text lives in the preprint. Citability: the DOI is live — 10.5281/zenodo.23864677 (CC-BY); arXiv (cs.CY) pending an endorsement, and the paper mirrors there when it lands. The claim, cross-checked against the 2025–26 literature: scholarship reached generative AI — model internals, generated data, agentic portability — but never the conversation archive, and the one open export spec stops at a generic transcript. The slot is open. The paper is the argument; this format, with its executable conformance vectors, is the answer.
The five original application readers still vendor or
hand-roll the rules instead of depending on the
curt-format package, so a spec revision still lands
in five places by hand.
The 1.1.0 BOM tolerance is unimplemented in the five original
application readers, and their filename conventions compete with
the spec's curtFilename.
Bindings beyond TypeScript. The predicates are small — Rust, Python, Go, Swift: pick one and keep it in lockstep with SPEC.md. PRs to dot_curt are welcome.