Concepts
ctx is a key-value store whose keys form a tree, whose every change is numbered and kept, and whose changes you can follow. Seven ideas cover all of it.
Keys make a tree
A key is a dotted path, project.lighthouse.status, and its value is any JSON: an object, an array, a string, a number, a boolean or null. There are no schemas and no types to declare. Prefixes are conventions that agents share, not rules the store enforces; the agent conventions list them.
"@".ctx get machine.atlas{ "@": { "role": "gpu builder", "owner": "maya" }, "quirks": "Nix builds capped at 8 jobs; /scratch is wiped nightly"}
On the command line a value is parsed as JSON when it parses, and stored as a string when it does not. --string forces a string.
ctx set agent.heron.note hello && ctx set agent.heron.count 3 && ctx set agent.heron.text --string 3 && ctx get agent.heronagent.heron.note: rev 18 (v1)agent.heron.count: rev 19 (v1)agent.heron.text: rev 20 (v1){ "count": 3, "note": "hello", "text": "3"}
One counter, every version kept
The store has one revision counter, like etcd. Every change to any key takes the next number, so revisions order everything that ever happened. Each entry records the revision of its last change, its version (writes since it was created), its writer and the time. Nothing expires: history keeps every version, and a delete is kept as a tombstone.
That makes the past readable. history lists a key's versions; any read takes --rev to see the store as it was.
ctx history project.lighthouse.status2 project.lighthouse.status just now wren {"state":"review","pr":31,"branch":"wren/flaky-ingest"} 1 project.lighthouse.status just now wren {"state":"open","pr":31,"branch":"wren/flaky-ingest"} ctx get project.lighthouse.status --rev 1 --compact{"state":"open","pr":31,"branch":"wren/flaky-ingest"}
Writing a value a key already holds is a no-op: no new revision, nothing for watchers. Agents can report status as often as they like.
ctx update project.lighthouse.status '{"state":"review"}'project.lighthouse.status: unchanged (rev 2)
Writes, and writing safely
| Operation | Does |
|---|---|
set | Replaces the value. |
update | Merges a JSON merge patch (RFC 7386): objects merge, null removes a field, anything else replaces. An absent key is patched as if it held null. |
append | Adds to an array, creating it if absent; max_items keeps only the newest. Appending to a non-array is an error. |
delete | Removes a key, or with recursive its whole subtree; each removed key takes a revision. |
batch | Applies several of the above in order, atomically: all or none. Each still takes its own revision. |
Every write takes an optional if_rev: the revision you expect the key to have. If anything changed it since you read it, the write is refused with a conflict and exit code 4. if_rev: 0 means the key must not exist, which makes a claim or a lock.
ctx set flow.release.state '"approved"' --if-rev 9flow.release.state: rev 10 (v2)# heron read the same rev 9ctx set flow.release.state '"rejected"' --if-rev 9ctx: conflict on `flow.release.state`: expected rev 9, current rev 10 next: re-read the key and retry with its current --if-rev
A batch carries one condition per write, so you can advance a state machine, log the step and release a lock in one move; see the release recipe.
Watching never misses a change
A watch follows a pattern from a revision. Patterns are an exact key, flow.release.state; one level of children, flow.release.*; or a whole subtree, flow.** (which includes flow itself), also written flow.>.
Three ways to use it:
- Stream:
ctx watch 'flow.**'prints each change as one line of JSON. - Wait:
--until VALUEchecks the current value first, then waits for it and exits 0, or exits 7 at--timeout.--onceexits after the first change. - React:
--exec CMDruns a command per change, with the event on stdin andCTX_KEY,CTX_REV,CTX_OP,CTX_VALUE,CTX_PREV_VALUEandCTX_WRITERin its environment.
ctx watch flow.deploy.state --until '"done"' --timeout 30s{"type":"change","rev":14,"key":"flow.deploy.state","op":"set","value":"building","version":1,"writer":"ci","at":"2026-10-17T16:58:24.513Z"} {"type":"change","rev":15,"key":"flow.deploy.state","op":"set","value":"deploying","prev_rev":14,"version":2,"writer":"ci","at":"2026-10-17T16:58:25.124Z"} {"type":"change","rev":16,"key":"flow.deploy.state","op":"set","value":"done","prev_rev":15,"version":3,"writer":"ci","at":"2026-10-17T16:58:25.732Z"} # exit 0, the moment "done" committed
Polling works too: ctx changes 'flow.**' --since 7 returns what changed after revision 7 and where to continue from. Agents get the same feed as messages through subscriptions.
Fresh enough to trust
An entry is a claim by its writer at a time. Reads can drop or flag old claims and fit a budget, which matters when the result goes into a prompt.
| Option | Effect |
|---|---|
--max-age 7d | Omits entries not updated within the age. |
--stale-after 7d | Keeps older entries but marks them STALE. |
--max-bytes 8000 | Drops the oldest entries until the output fits, and says how many it dropped. |
--since-rev 20 | Only entries changed after revision 20. |
--format md | Markdown with each entry's age and writer, ready for a system prompt. Also json, ndjson and raw. |
ctx get project.lighthouse --format md --stale-after 1s## ctx: project.lighthouse (3 entries, 3 stale) - `project.lighthouse.conventions` (STALE, 9m ago, heron): "Tests: cargo nextest; never commit generated fixtures" - `project.lighthouse.decisions` (STALE, 9m ago, heron): [{"what":"Retry ingest twice, then fail the batch","by":"heron"}] - `project.lighthouse.status` (STALE, 8m ago, deploy-bot): {"state":"merged","branch":"wren/flaky-ingest"} ctx get project.lighthouse --format md --max-bytes 300## ctx: project.lighthouse (1 entry, 2 older omitted) - `project.lighthouse.status` (8m ago, deploy-bot): {"state":"merged","branch":"wren/flaky-ingest"} ctx: 2 older entries omitted (--max-bytes)
Search by words or by meaning
Search looks through keys and values. Every write is indexed for keywords in the same transaction, so it is findable at once. With an embeddings endpoint configured, entries are also embedded in the background and become findable by meaning a moment later.
| Mode | Finds |
|---|---|
fts | Keywords, ranked by BM25 (SQLite FTS5). Every word must match; the last may be a prefix. The default without embeddings. |
semantic | Entries closest in meaning to the query, by cosine similarity of embeddings from any OpenAI-compatible endpoint. |
hybrid | Both lists fused by reciprocal rank; each hit says whether it matched by keyword, meaning or both. The default with embeddings. |
ctx search nightly scratchmachine.atlas.quirks 46s ago Nix builds capped at 8 jobs; /[scratch] is wiped [nightly]
If the embeddings endpoint fails, hybrid search keeps answering from keywords and ctx status shows the error.
One token, named writers
A ctx instance has one bearer token, and anyone holding it can read and write everything. There are no per-subtree permissions: an instance serves one purpose, and another purpose gets another instance. Every change records a writer, which the client declares: --writer, else CTX_WRITER, else CACO_AGENT_ID, else user@host. It identifies, it does not authenticate.
Values are readable by every token holder and kept in history forever. Never store secrets, tokens or private data in ctx.