.curt

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
2026-10-14 design-review 001.curt
.CURT — THE FILE, OPENED
WHAT EVERY CONFORMING READER OWES YOU
magic"curtis-conversation" — exact
version1, the number — a string "1" is a rejection
fidelitybase64 images and tool calls ride in content, verbatim
toleranceone leading UTF-8 BOM ignored; writers never emit it
round-tripunknown fields preserved on re-save ✓
licenseMIT — spec, reference code, schema, fixtures

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 — the envelope, opened .curt — the envelope, opened DOTCURT.ORG · SPEC V1 · MIT 2026-10-14 design-review 001.curt { "curt": "curtis-conversation", "version": 1, "generator": "[email protected]", "conversation": { "id": "conv_001", "title": "Thursday's thread", "createdAt": 1760000000000, "updatedAt": 1760000600000, "messages": [ { "id": "m2", "role": "user", "content": "hello", "model": "provider/model-id", "timestamp": 1760000000000 } ] } } WHAT THE FIELDS PROMISE magic — exact match the number 1,never the string provenance — readersmust ignore epoch milliseconds unknown fields surviveround-trip role — user · assistant ·system · tool full fidelity: base64rides verbatim
Fig. 1 — the envelope, annotated
{
	"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:

fieldrequiredmeaning
curtyesmagic — must be exactly "curtis-conversation"
versionyesformat version; this spec is 1
generatornoprovenance, "writer-name@version" — readers must ignore
extensionsnonames from the registry — never a validation input
conversationyesid, title, createdAt, updatedAt, messages
roleyesuser · assistant · system · tool
modelnoprovider 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

  1. 01
    The body parses as JSON — after ignoring at most one leading UTF-8 BOM, because Windows editors prepend one anyway.
  2. 02
    curt is exactly "curtis-conversation" and version is exactly the number 1.
  3. 03
    The conversation carries id and title as strings, timestamps as numbers, and a messages array 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.

namestatuspurposeemitted by
images-inlinestableimage payloads ride inside contentplugin · porter · web · cloud · ai-tech
usage-metadatastableper-message tokens, cost, providerporter · web · ai-tech
tool-linkagedrafttool calls linked to their resultsporter · ai-tech · curt-convert
agent-contextdraftagent and swarm provenanceplugin · porter · cloud · ai-tech
consensus-receiptdraftsigned k-of-n consensus bound to the transcript evidencetractum

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.

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.

consumerroler · wcarries
curtis pluginimport · export · backupR + Wvendored copy
curt-openwithoffline viewer — double-click any .curtRhand-rolled port
porterconverter + verify oracleR + Wvendored oracle
curtis-webexport endpointWhand-rolled
curtis-cloudshared libraryR + Wvendored mirror
curtis-ai-techembedded converter + docsR + Winline copy
tractumsigned consensus receiptsR + Wown validator
curt-vcon.curt ⇄ vCon bridgeR + Wnpm curt-format
curt-convertClaude Code JSONL → .curtWnpm curt-format
curt-openwebuiOpen WebUI exporter (local)Wnpm 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.

NOT YET — STATED PLAINLY

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.