Guide
Install the client and a daemon, write your first keys, then put ctx to work. Every command and output here comes from a real run against a scratch daemon.
Install
ctx is one Rust binary, ctx, which is the daemon, the command line and the MCP server. SQLite is built in.
With Nix and Home Manager
The flake provides the package, an overlay and a Home Manager module. programs.ctx installs the client and its config; services.ctx.daemon runs the daemon as a systemd user service on Linux or a launchd agent on macOS, on the one machine that hosts it.
# flake.nixinputs.ctx.url = "github:harryaskham/ctx";# a Home Manager module{ inputs, ... }: { imports = [ inputs.ctx.homeManagerModules.ctx ]; programs.ctx = { enable = true; settings.client.daemon-url = "http://192.168.1.20:7641"; }; services.ctx.daemon = { enable = true; # only where the daemon runs bind = "192.168.1.20:7641"; };}
From source
# Rust 1.85 or newercargo install --path .# or, with Nix nix build .#ctx
Run a daemon
With no configuration at all, ctx daemon listens on 127.0.0.1:7641, creates an owner-only token at ~/.config/ctx/daemon-token and a database at ~/.local/share/ctx/ctx.sqlite. Clients on the same machine find both by default.
ctx daemon2026-10-17T17:14:20.412303Z INFO ctx::daemon: ctx daemon ready database=~/.local/share/ctx/ctx.sqlite tcp=Some(127.0.0.1:7641) unix=None head_rev=0# in another shellctx set project.lighthouse.status '{"state":"open","pr":31}' project.lighthouse.status: rev 1 (v1)
Share it between machines
Run one daemon for the whole fleet, bound to a private interface such as a LAN, VPN or tailnet address, and point every client at it. Each client needs a copy of the daemon's token file; a symlink managed by your secrets tool is fine.
-
Configure the daemon host
# ~/.config/ctx/config.yaml on atlasdaemon: bind: 192.168.1.20:7641 database: ~/.local/share/ctx/ctx.sqliteclient: daemon-url: http://192.168.1.20:7641 -
Give each client the URL and the token
# ~/.config/ctx/config.yaml on every other machineclient: daemon-url: http://192.168.1.20:7641 token-file: ~/.config/ctx/daemon-token # the default -
Check from a client
ctx config validatevalid: ~/.config/ctx/config.yamlctx statusctx statusprints the daemon URL, its version and head revision, and the store's size. A wrong token exits 6; an unreachable daemon exits 5.
A Unix socket (daemon.unix-socket, created owner-only) is the fastest local transport; clients use unix:///path/to/ctx.sock. Every setting is listed in the configuration reference.
First steps
Write a few keys, then list them and read them back as a prompt-ready outline. CTX_WRITER or --writer names who wrote each change.
ctx set project.lighthouse.status '{"state":"open","pr":31,"branch":"wren/flaky-ingest"}' project.lighthouse.status: rev 1 (v1)ctx update project.lighthouse.status '{"state":"review"}'project.lighthouse.status: rev 2 (v2)ctx append project.lighthouse.decisions '{"what":"Retry ingest twice, then fail the batch","by":"heron"}' --max-items 50 project.lighthouse.decisions: rev 3 (v1)ctx set project.lighthouse.conventions 'Tests: cargo nextest; never commit generated fixtures'project.lighthouse.conventions: rev 4 (v1)ctx ls -r projectproject.lighthouse.conventions just now heronproject.lighthouse.decisions just now heronproject.lighthouse.status just now wrenctx get project.lighthouse --format md## ctx: project.lighthouse (3 entries)- `project.lighthouse.conventions` (just now, heron): "Tests: cargo nextest; never commit generated fixtures" - `project.lighthouse.decisions` (just now, heron): [{"what":"Retry ingest twice, then fail the batch","by":"heron"}] - `project.lighthouse.status` (just now, wren): {"state":"review","pr":31,"branch":"wren/flaky-ingest"}
Without -r, ctx ls lists one level: each child with how many keys sit below it, when the newest of them changed and who changed it.
Recipe: one memory for a project
Put what an agent should know where its preload looks: project facts under project.lighthouse, as above; host facts under machine.atlas; an agent's own state under agent.wren; fleet-wide facts under global. Store conclusions, not transcripts, and update entries in place.
ctx set machine.atlas.quirks 'Nix builds capped at 8 jobs; /scratch is wiped nightly'machine.atlas.quirks: rev 5 (v1)ctx set agent.wren.task '{"bead":"bd-41c2","doing":"fix flaky ingest test"}' agent.wren.task: rev 6 (v1)ctx set global.freeze falseglobal.freeze: rev 7 (v1)
An agent starting in the lighthouse checkout on atlas now gets all of it, because the default preload reads global, project.{project}, machine.{machine} and agent.{agent}. ctx prompt shows exactly what it will see; Agents shows the whole output.
Recipe: claim it before you start
When several agents might pick up the same job, make the first step a write that only one can win. --if-rev 0 succeeds only if the key does not exist.
ctx set flow.release.lock '{"holder":"wren"}' --if-rev 0flow.release.lock: rev 8 (v1)# heron tries a moment laterctx set flow.release.lock '{"holder":"heron"}' --if-rev 0ctx: conflict on `flow.release.lock`: expected rev 0, current rev 8 next: re-read the key and retry with its current --if-rev
The exit code is 4 for a conflict, so scripts can branch on it. Release the claim by deleting the key; claims do not expire, so a holder that dies leaves its claim for someone to clear.
Recipe: a release state machine
Keep the state in one key and every transition conditional on the revision you read. Pair each transition with a capped log, and do both in one batch.
-
Read the state and its revision
ctx get flow.release.state --format ndjson{"key":"flow.release.state","value":"testing", "rev":9, "version":1, "writer":"ci", "updated_at":"2026-10-17T16:58:08.681Z"} -
Move it only from there
ctx set flow.release.state '"approved"' --if-rev 9flow.release.state: rev 10 (v2)Anyone else who read revision 9 now gets a conflict instead of overwriting the approval.
-
Transition, log and release together
ctx batch <<'JSON'[{"op":"set","key":"flow.release.state", {"op":"append","value":"shipping", "if_rev":10}, "key":"flow.release.log", {"op":"delete","value":{"to":"shipping", "by":"wren"}, "max_items":50}, "key":"flow.release.lock"}] JSONflow.release.state: rev 11 (v3)flow.release.log: rev 12 (v1)flow.release.lock: deleted 1 (rev 13)All three apply or none do. Whoever waits on
flow.release.statetakes the next step.
Recipe: wait, then react
A CI job can block until another system says it is done, with a deadline. --until looks at the current value first, so it returns at once if the state was reached before it started.
# in CI: wait for the deploy, at most 30 seconds here 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; a timeout exits 7
Or run a command for every change. The event arrives on stdin, and the key, revision, operation, value, previous value and writer in the environment; values are JSON, so strings keep their quotes.
ctx watch flow.deploy.state --exec 'echo "$CTX_KEY -> $CTX_VALUE (rev $CTX_REV, by $CTX_WRITER)"' flow.deploy.state -> "building" (rev 14, by ci) flow.deploy.state -> "deploying" (rev 15, by ci) flow.deploy.state -> "done" (rev 16, by ci)
A watch reconnects by itself and resumes from the last revision it saw. To wake an agent rather than run a command, use a subscription.
Recipe: from any program, over HTTP
Every operation is one JSON object posted to /v1/op, with the token as a bearer and the writer in x-ctx-writer. The response is the same envelope --json prints.
curl -s localhost:7641/v1/op -H "authorization: Bearer $TOKEN" -H "x-ctx-writer: deploy-bot" \ -d '{"op":"update","key":"project.lighthouse.status","patch":{"state":"merged","pr":null}}' {"status":"success","meta":{"schema_version":1,"command":"update"},"data":{"key":"project.lighthouse.status","op":"update","changed":true,"rev":24,"prev_rev":2,"version":3}} curl -sN "localhost:7641/v1/watch?pattern=project.lighthouse.**&since=20" -H "authorization: Bearer $TOKEN"{"type":"change","rev":24,"key":"project.lighthouse.status","op":"set","value":{"state":"merged","branch":"wren/flaky-ingest"},"prev_rev":2,"version":3,"writer":"deploy-bot","at":"2026-10-17T16:58:53.336Z"} {"type":"ready","head_rev":24}
The merge patch set state and removed pr. The HTTP reference lists every operation and field.
Recipe: back up and move
Export a subtree, or everything, as NDJSON, and import it into any daemon. Imports run in atomic chunks of 500 entries by default.
ctx export flow.release{"key":"flow.release.log","value":[{"to":"shipping","by":"wren"}],"rev":12,"writer":"wren","updated_at":"2026-10-17T16:58:14.826Z"} {"key":"flow.release.state","value":"shipping","rev":11,"writer":"wren","updated_at":"2026-10-17T16:58:14.826Z"} ctx export > ctx-backup.ndjson# with the client pointed at the new daemonctx import ctx-backup.ndjsonimported 10 (10 changed)
An import writes current values as new changes on the target; history stays with the original database, which is an ordinary SQLite file you can copy while the daemon is stopped.