Guide · Linux

A durable record of shared work, on your own machine.

Kovee is a daemon and a small set of clients. You build it, start it, and it keeps an immutable, typed record of what people and agents contributed to a shared space — with the context given to an assistant recorded exactly, and every committed change on one event ledger you can page through.

Linux · Unix sockets SQLite, WAL Single user, same UID Pre-release
Pre-release

Kovee is under active development and has not been audited. It runs, and everything on this page was executed against a live koveed, but the assurance profile is developer — same-UID, no confinement, no team mode. Read security & limits before you put anything you care about in it.

What kovee is

One daemon, koveed, holding a SQLite database and listening on two Unix domain sockets — one for clients, one for assistant workers. Everything else talks to it: the kovee CLI, an MCP server your agent harness can launch, and a Python SDK an assistant runs inside.

The records are ordinary and inspectable:

Concepts takes each of these apart. The full operation set is in the reference.

What it is not

Kovee's design describes considerably more than kovee ships. This table is the short version; each "not shipped" row is checked against the operation registry on every build of this site.

You might expectToday
Fork a branch, merge by adoption Not shipped. Branching is a record model: every space has a main branch and every contribution carries an origin branch and sequence. There is no branch_* operation in the registry — nothing forks and nothing merges.
An attention contract that wakes an assistant Not shipped. There is no attention_* operation; crates/kovee-attention is a two-line stub. An assistant is invoked explicitly, by a client that says so.
Needs, offers, formation proposals, commitments Not shipped. No need_*, offer_* or commitment_* operation exists.
Team mode — several people, a server, sign-in Does not exist. No PostgreSQL, no OIDC, no realtime, no NATS. Kovee binds Unix sockets and authenticates its peer by SO_PEERCRED same-UID. There is no network listener at all.
A sandbox that contains an agent Not claimed. The assurance profile is developer: no UID separation, no attested process identity, no asymmetric endpoint identity. A same-UID process can do anything you can.
A gateway that stops an agent reaching a model directly Not claimed. The broker's own calls are disclosed, permitted and metered. Nothing stops a different process on your machine from calling a provider itself. See provider bypass.
A chat app, a UI, a hosted account None of those. There is no web interface in this repository. The clients are a CLI, an MCP server, and a Python SDK.

Requirements

You needWhy
Linux Both sockets are Unix domain sockets, and the daemon authenticates its peer per connection by reading a ucred through SO_PEERCRED — a Linux interface. Nothing here has been built or tested on another platform.
A Rust toolchain The tree pins one in rust-toolchain.toml; rustup will fetch it. This guide was run on cargo 1.95.0 / rustc 1.95.0.
A C compiler SQLite is compiled in through rusqlite's bundled feature, so there is no system SQLite to install — but there is C to build.
Python 3 (optional) Only for the assistant SDK and for mcp/check.py. The SDK is stdlib-only.
A short runtime directory A Unix socket path has a hard length limit. See troubleshooting.

Build it

your machine
git clone https://github.com/zarbafian/kovee
cd kovee
cargo build --workspace --locked

That produces three binaries under target/debug/: koveed (the daemon), kovee (the CLI) and kovee-mcp (the MCP server). Put target/debug on your PATH, or call them by path — this guide writes them as koveed and kovee.

To validate the tree, run ./run-checks.sh: formatting, clippy under -D warnings, the whole test suite, the MCP tool-bundle check, the check that keeps this website honest, and two independent re-derivers of the golden vectors — one in Python, one in TypeScript, sharing no code with the Rust. CI runs most of those; the MCP and docs checks are local-only today.

Quickstart

Six verbs, in order, from an empty store. Every block below is the real output of the command above it, captured from one run on 2026-07-28; only the identifiers will differ on your machine, because they are minted per installation.

1. Start the daemon

koveed takes one optional flag, --data-dir. Anything else is a hard error. Here it is pointed at a scratch directory with two environment variables so nothing lands in your real profile:

terminal 1
export KOVEE_DATA_DIR=/tmp/kvqs/data
export KOVEE_RUNTIME_DIR=/tmp/kvqs/run
mkdir -p "$KOVEE_DATA_DIR" "$KOVEE_RUNTIME_DIR"
koveed &

koveed: personal profile; store /tmp/kvqs/data/kovee.db; listening on /tmp/kvqs/run/kovee.sock (external) and /tmp/kvqs/run/kovee-worker.sock (worker)

Without those variables the store goes to $XDG_DATA_HOME/kovee (falling back to ~/.local/share/kovee) and the sockets to $XDG_RUNTIME_DIR/kovee (falling back to /tmp/kovee-<uid>). The directory is created 0700 and each socket file 0600.

2. Say hello

Every client starts here. hello negotiates a protocol version and reports which feature bundles this installation actually serves.

terminal 2
kovee hello

{
  "features": [
    "core_v1",
    "shared_space_v1",
    "developer_assistant_v1"
  ],
  "implementation": "koveed",
  "implementation_version": "0.0.1",
  "installation_id": "inst-bffac71619de79b979c4a9c9263fa393",
  "limits_digest": "3ee3bd3169b8f32fad61b1e3a8bec77a9065c8052e8cfbda8df80e1e0441ada9",
  "selected_version": "0.1",
  "server_time": "2026-07-28T03:00:40Z"
}

Three bundles, not four. A bundle is atomic — a client may assume all of it or none of it — so the fourth bundle, governed_work_binding_v1, is deliberately not advertised while it is built out at 9 of its fourteen operations. Its operations do dispatch over the socket; they are simply not something a client should discover and depend on yet. Capability discovery is this list, never a version number.

3. Initialise

kovee init is three operations in one verb: hello, realm_show, and a project_create under a fixed idempotency key, so running it again returns the same project instead of minting a second one.

terminal 2
kovee init

installation: inst-bffac71619de79b979c4a9c9263fa393
realm:        realm-personal
project:      proj-b9e7413a81a4596bd34ac8c374b8c44b (name "personal")

The personal profile has exactly one realm, realm-personal, minted at first start. Keep that project id — every later command needs it.

4. Create a space

terminal 2
kovee space create --project proj-b9e7413a81a4596bd34ac8c374b8c44b \
  --title "Flaky test triage"

{
  "created_at": "2026-07-28T03:00:40Z",
  "created_by": "prin-owner",
  "default_classification_ref": "class-default",
  "main_branch_id": "branch-9b988617a2fb9bfe1cce041219052847",
  "next_space_sequence": 1,
  "policy_set_ref": "policy-default",
  "project_id": "proj-b9e7413a81a4596bd34ac8c374b8c44b",
  "realm_id": "realm-personal",
  "revision": 1,
  "space_id": "space-88b7a591ca55b98e0a2c59507bf380b8",
  "status": "open",
  "title": "Flaky test triage",
  "visibility": "project"
}
event_cursor: kc1.7b2265706f…2c2276223a317d.73e93652bb…7f136ef63e   (on stderr, elided)

Note three things. The space came with a main branch already — branches are a record model, not something you create. Its visibility defaulted to project; the other accepted value is restricted, and widening it later is a separate prepared path rather than an edit. And the result printed to stdout while the event_cursor went to stderr, which is what lets you pipe the JSON somewhere without losing the cursor.

5. Append contributions

A contribution is typed. --kind takes one value from a closed enum and defaults to utterance; here a question and then the evidence that answers it.

terminal 2
kovee space contribute --project proj-b9e7413a81a4596bd34ac8c374b8c44b \
  --space space-88b7a591ca55b98e0a2c59507bf380b8 \
  --kind question --text "Why does k1_crash_matrix flake only on CI?"

{
  "author_actor_ref": "prin-owner",
  "body_parts": [
    {
      "media_type": "text/plain",
      "text": "Why does k1_crash_matrix flake only on CI?"
    }
  ],
  "classification_ref": "class-default",
  "content_digest": "2aecf827087cb2267118e469914ba56f0795fc88c44f8c7c431eb520dcdee51c",
  "contribution_id": "contrib-08ef618663d43b7416a0c6b5c5ee0f12",
  "created_at": "2026-07-28T03:00:40Z",
  "kind": "question",
  "origin_branch_id": "branch-9b988617a2fb9bfe1cce041219052847",
  "origin_branch_sequence": 1,
  "project_id": "proj-b9e7413a81a4596bd34ac8c374b8c44b",
  "realm_id": "realm-personal",
  "retention_policy_ref": "ret-default",
  "revision": 1,
  "schema_ref": "schema:contribution-body-v1",
  "source_refs": [],
  "space_id": "space-88b7a591ca55b98e0a2c59507bf380b8",
  "space_sequence": 1,
  "subject_refs": []
}
terminal 2
kovee space contribute --project proj-b9e7413a81a4596bd34ac8c374b8c44b \
  --space space-88b7a591ca55b98e0a2c59507bf380b8 \
  --kind evidence --text "Ten reruns: three failures, all under the WAL checkpoint path."

{ …
  "content_digest": "f5a3c0d75fbc91ea29f58b02201c363ad5ccb1e9d6beab79cae65d33617c886c",
  "contribution_id": "contrib-a48910a8f8117f458031ae6ca880e7e9",
  "kind": "evidence",
  "origin_branch_sequence": 2,
  "space_sequence": 2,
  … }

A wrong kind is refused rather than coerced, and the daemon says which rule it broke:

terminal 2 — real failure
kovee space contribute --project $P --space $S --kind banana --text hi

kovee: problem urn:kovee:error:invalid: invalid operation arguments
  kind is not in the closed ContributionKind enum
exit status 1

6. Read the space, then the ledger

terminal 2
kovee space show --project proj-b9e7413a81a4596bd34ac8c374b8c44b \
  space-88b7a591ca55b98e0a2c59507bf380b8

{ …
  "next_space_sequence": 3,
  "revision": 3,
  "status": "open",
  "title": "Flaky test triage",
  "visibility": "project" }

The space is at revision 3 after two appends: creating it was revision 1, and each contribution moved it. Now the ledger:

terminal 2
kovee events --project proj-b9e7413a81a4596bd34ac8c374b8c44b --limit 4

   1  2026-07-28T03:00:40Z  dev.kovee.project.created.v1  proj-b9e7413a81a4596bd34ac8c374b8c44b
   2  2026-07-28T03:00:40Z  dev.kovee.space.created.v1  space-88b7a591ca55b98e0a2c59507bf380b8
   3  2026-07-28T03:00:40Z  dev.kovee.space.contribution-appended.v1  contrib-08ef618663d43b7416a0c6b5c5ee0f12
   4  2026-07-28T03:00:40Z  dev.kovee.space.contribution-appended.v1  contrib-a48910a8f8117f458031ae6ca880e7e9
next_cursor: kc1.7b2265706f…2c2276223a317d.eee9e1a3c8…931a38343b7e   (on stderr, elided)

Paging is by cursor, not by offset. Feed the cursor back with --after and you get everything committed since — the same events from 3 onwards here, because the cursor above was taken after event 2:

terminal 2
kovee events --project proj-b9e7413a81a4596bd34ac8c374b8c44b \
  --after "kc1.7b2265706f…2c2276223a317d.73e93652bb…7f136ef63e" --limit 5

   3  2026-07-28T03:00:40Z  dev.kovee.space.contribution-appended.v1  contrib-08ef618663d43b7416a0c6b5c5ee0f12
   4  2026-07-28T03:00:40Z  dev.kovee.space.contribution-appended.v1  contrib-a48910a8f8117f458031ae6ca880e7e9

That is the whole shipped CLI. Everything else is reached over the socket.

Reading the output

FieldWhat it tells you
revisionOptimistic-concurrency counter for that record. Send it back as expected_revision and a mutation that raced you is refused as stale rather than silently applied.
space_sequence, origin_branch_sequenceDense per-space and per-branch positions. Dense means no gaps — a rolled-back transaction consumes no number.
content_digestThe address of that contribution's body. Relations and assemblies bind to it, so they name an exact revision rather than "the latest".
event_cursor / next_cursorAn opaque, authenticated position on the project ledger: kc1.<hex payload>.<hex MAC>. It is minted per installation and per source, so a cursor from elsewhere is refused as invalid. Possession grants nothing; authorization is rechecked on every read.
Event typesReverse-domain and versioned, e.g. dev.kovee.space.contribution-appended.v1. Reads may filter by type prefix.
limits_digestA digest over the protocol limits this installation enforces, so a client can tell two installations apart without probing them.

Running it twice

Every mutation carries an idempotency key. The CLI mints a random one per invocation, except for init, which uses a fixed key — that is why running init twice prints the same project id rather than making a second project. Both mutating verbs accept --idempotency-key <key>, which is not in the usage block but is real:

terminal 2
kovee space create --project $P --title "Once" --idempotency-key mine-1 | grep space_id
kovee space create --project $P --title "Once" --idempotency-key mine-1 | grep space_id

  "space_id": "space-94f631c6e948f614832cbb57dd2f10e6",
  "space_id": "space-94f631c6e948f614832cbb57dd2f10e6",
# the second call returned the first result — one space, not two

Reusing the key with different arguments is an error, not a silent overwrite. The daemon compares a canonical digest of the request — one that deliberately excludes the request id and trace headers, so a genuine retry still matches — and refuses a mismatch:

terminal 2 — real failure
kovee space create --project $P --title "Twice" --idempotency-key mine-1

kovee: problem urn:kovee:error:idempotency-mismatch: same scoped idempotency key, different canonical request
  reusing an idempotency key with changed arguments is refused
exit status 1

This is the same mechanism that makes a crashed client safe to retry — see the command transaction.

The other three surfaces

The CLI covers six verbs of 96 operations. The rest are reached by writing a JSON command line to the socket yourself, or through the MCP server or the SDK. All four surfaces speak the same protocol: one JSON object per line in, one reply line out, connection closed.

terminal 2 — the wire, by hand
python3 - "$P" <<'PY'
import json, os, socket, sys
cmd = {"version": "0.1", "op": "space_list", "realm_id": "realm-personal",
       "project_id": sys.argv[1], "args": {"limit": 3}}
s = socket.socket(socket.AF_UNIX)
s.connect(os.path.join(os.environ["KOVEE_RUNTIME_DIR"], "kovee.sock"))
s.sendall((json.dumps(cmd) + "\n").encode())
s.shutdown(socket.SHUT_WR)
print(json.dumps(json.loads(s.makefile().readline()), indent=2))
PY

{
  "outcome": "ok",
  "result": {
    "boundary_event_cursor": "kc1.7b2265706f…2c2276223a317d.ab68d0a5c8…36371e9d45",
    "items": [
      {
        "created_at": "2026-07-28T02:57:21Z",
        "created_by": "prin-owner",
        …
        "space_id": "space-442e84b453eb9aa04ccd7a32ba556c5f",
        "status": "open",
        "title": "Flaky test triage",
        "visibility": "project"
      },
      …

A reply is either {"outcome":"ok", "result": …} or {"outcome":"problem", "problem": …}. Problems are typed — urn:kovee:error:invalid, urn:kovee:error:idempotency-mismatch and so on — with a human detail line, which is what the CLI prints. Note the boundary_event_cursor: a list read hands back the ledger position its answer was true at, so a client can follow the ledger from there rather than polling the list.

MCP server

kovee-mcp is a stdio JSON-RPC MCP server exposing 14 tools to an agent harness. It parses no command-line arguments at all: the harness launches the binary and speaks MCP on its stdin and stdout. The one configuration line in the tree is

your harness
claude mcp add kovee -- kovee-mcp

It needs a running koveed, finds the socket the same way the CLI does, and scopes itself to one project: set KOVEE_PROJECT, or leave it unset and let it use the single project if there is exactly one.

Each tool is derived from a registry operation, not hand-written — the tool kovee_contribution_append is the operation contribution_append minus the fields the channel already fixes. Read-only tools are marked safe to allow; mutations and the one credential-bearing read are marked gated, for your harness to prompt on. The tool table lists all fourteen with the operation each one binds; concepts explains how they are derived.

Assistant SDK

An assistant is a Python class with a run method. The SDK (sdk/python/kovee_sdk.py, stdlib only, no package to install — put the directory on sys.path) does the rest: it creates the context assembly, creates the invocation on the client socket, claims the attempt on the worker socket, runs you, and completes.

assistant — the shape of one
from kovee_sdk import Assistant, run_one_shot

class DeterministicReviewer(Assistant):
    def run(self, ctx):
        question = ctx.trigger_contribution
        synthesis = ctx.contribute(
            kind="synthesis",
            parts=[{"media_type": "text/plain", "text": "…"}],
            operation_key="synthesis-v1",
        )
        ctx.relate("addresses", synthesis.ref, question["contribution_id"],
                   operation_key="addresses-v1")
        ctx.result_ref = synthesis.ref

run_one_shot(DeterministicReviewer(), project_id=…, space_id=…, branch_id=…,
             question_ref=…, invocation_key="review-1",
             deadline="2027-01-01T00:00:00Z")

That is condensed from assistants/deterministic_reviewer.py, which is not an illustration: the acceptance test runs that exact file against a real daemon, kills the daemon mid-flow, and asserts the ledger ends with exactly one synthesis and exactly one relation. The mandatory operation_key on every mutating call is what makes that true — it becomes the durable idempotency key, so a retry after a crash replays instead of duplicating.

Limit

run_one_shot is a direct invocation: your code decides to run the assistant and names the contribution it should look at. Nothing in kovee wakes an assistant on its own — the attention machinery that would is not shipped.

Troubleshooting

The CLI cannot reach the daemon

real output
kovee: cannot reach koveed at /tmp/kvdoc/nope/kovee.sock (No such file or directory (os error 2)); is the daemon running?

The CLI and the daemon resolve the socket by the same rules, so this almost always means the two disagree about KOVEE_RUNTIME_DIR. Check what the daemon printed when it started — it names both socket paths.

The daemon will not bind

real output
koveed: bind socket: socket io: path must be shorter than SUN_LEN

A Unix socket path is limited to about a hundred bytes by the kernel, and a deep KOVEE_RUNTIME_DIR blows past it. Use a short one.

A cursor is refused

real output
kovee: problem urn:kovee:error:invalid: invalid cursor
  not a cursor this installation minted for this source

Cursors are authenticated against an installation secret and bound to the source they were minted for, so one from another store — or a truncated copy-paste — is indistinguishable from a forgery and is refused as invalid. In zsh, note that 2>&1 >/dev/null does not mean what it means in bash: with MULTIOS on you capture the event lines too. Capture the cursor with grep -o 'kc1\.[^ ]*' instead.

An unknown verb, or a missing flag

There is no --help. Any unrecognised verb or missing required flag prints the usage block on stderr and exits 2; a rejected operation exits 1.

Where next