.curt

An open format for AI conversations — spec v1 · curt-format@1.2.0 · 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": "curtis-conversation",
	"version": 1,
	"generator": "your-app@1.2.3",
	"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, 'my-app@1.0.0');
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
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
porterconverter + verify oracleR + Wvendored oracle
curtis-webexport endpointWhand-rolled
curtis-cloudshared libraryR + Wvendored mirror
curtis-ai-techembedded converter + docsR + Winline copy
tractumconsensus emitterWconsensus-receipt draft

None depend on the curt-format package yet — every one vendored or hand-rolled the rules. The ladder from five copies to one dependency is written down in the audit.

NOT YET — STATED PLAINLY

The npm package is documentation, not infrastructure. Every consumer today vendors or hand-rolls the rules, so a spec revision still lands in five places by hand.

The 1.1.0 BOM tolerance is unimplemented outside the reference reader, and three 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.