●ctx0.1.0

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 Nixnix 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.

  1. 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
  2. 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
  3. Check from a client

    ctx config validatevalid: ~/.config/ctx/config.yamlctx status

    ctx status prints 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 50project.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 8next: 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.

  1. 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"}
  2. 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.

  3. Transition, log and release together

    ctx batch <<'JSON'[{"op":"set","key":"flow.release.state","value":"shipping","if_rev":10}, {"op":"append","key":"flow.release.log","value":{"to":"shipping","by":"wren"},"max_items":50}, {"op":"delete","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.state takes 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 herectx 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.