Documentation

knoot coordinates several coding agents working one repository. Agents claim files before writing them, hold them on a short lease, and are handed the holder's identity and intent when a file is busy.

Install

One binary provides the relay, the daemon, the hook shim and the CLI. Install it on every machine that runs agents.

curl -fsSL https://raw.githubusercontent.com/Ash20pk/knoot/main/install.sh | sh

The installer takes the latest release, a static build for Linux x86_64, or macOS for Apple silicon and Intel, verifies its checksum and puts it in ~/.local/bin. Set KNOOT_VERSION=nightly for the newest build of main, or KNOOT_INSTALL for another directory. On any other platform, build from source:

cargo install --git https://github.com/Ash20pk/knoot

The hooks call knoot by name so they resolve on every machine that has it on PATH. If yours lives somewhere unusual, set KNOOT_BIN.

Enrol a repository

Run this once per repository. It writes .knoot.toml and the hook configuration, both of which are meant to be committed, so anyone who clones the repository is enrolled with no setup.

knoot init --relay wss://knoot.dev/ws

Then, once per machine, store the device key the console gave you and run the daemon. Each key belongs to one person on one machine, so revoking it costs nothing else.

knoot join <key> --relay wss://knoot.dev/ws
knoot daemon

Tokens deliberately do not live in .knoot.toml. That file is committed, and a secret must never ride along with it. Credentials go to ~/.knoot/credentials.toml at mode 0600, or the KNOOT_TOKEN environment variable for CI.

Verify it is on

knoot fails open, so from inside an agent a broken install looks exactly like a quiet one. knoot status is how a person tells the difference.

knoot statusreal output
[ok  ] binary    /usr/local/bin/knoot
[ok  ] repo      ~/project (id: repo-fade58f512a66c42)
[ok  ] hooks     6 events, resolved from PATH
[ok  ] daemon    responding
[ok  ] relay     wss://knoot.dev/ws (token: present)

coordination is on.

Anything less than that prints what is wrong and the command that fixes it.

Wire up an agent

knoot never talks to a model. It reads a JSON payload on stdin and answers on stdout, so anything that can run a command before it writes a file can be coordinated.

Claude Code

knoot init writes the hook configuration for you, into .claude/settings.json in the repository. Restart any sessions that were already open.

Codex

The same knoot init also writes .codex/hooks.json. Codex asks you to trust a repository’s hooks once: run /hooks inside Codex. Edits arrive as one apply_patch call, so a patch touching several files is checked as a unit and a denial on one file claims none of the others.

Codex runs the agent’s own shell commands in a sandbox that blocks every socket, so knoot who and knoot msg fail inside a Codex session with Operation not permitted. Hooks run outside that sandbox and keep working, and the brief Codex receives says so. knoot msg, knoot plan and knoot remember still work from inside: they notice the refused socket, queue the request under .knoot/spool/, and the next hook sends it. Codex can also message a peer by writing text to .knoot/outbox/<user>. init adds .knoot/ to .gitignore. Commands that need an answer now (who, recall, why, status) do not work inside the sandbox; their content is already in the brief.

Any other agent

Call the shim yourself before each write and honour the answer. It is the same code path the built-in integration uses.

your agent, before writing a file
echo '{"hook_event_name":"PreToolUse",
       "session_id":"'"$SESSION"'",
       "cwd":"'"$PWD"'",
       "tool_name":"Edit",
       "tool_input":{"file_path":"src/auth.js"}}' \
  | knoot hook

A permitted write produces no decision. A refused one returns permissionDecision: "deny" and a reason string meant to be put straight into the model's context. Feed that string back to the model rather than summarising it: naming the holder and their intent is what makes an agent re-plan instead of retry.

People

Developers collide with agents too. Wrapping $EDITOR in the same call puts a person in the same log, holding leases the same way.

Command reference

CommandWhat it does
knoot initEnrol the current repository. Writes .knoot.toml and hook config.
knoot joinStore this machine's device key for a relay, in ~/.knoot/credentials.toml, and print who it names.
knoot loginStore a relay's shared token instead, for a self-hosted relay run on one secret.
knoot daemonRun the local claim mirror. One per machine.
knoot relayRun a relay: sequenced log, claims, arbitration.
knoot statusCheck binary, repository, hooks, daemon, relay and token.
knoot whoActive sessions, their stated intent, and what they hold.
knoot watchLive dashboard of sessions, claims and collisions.
knoot msgSend a note to one peer or to everyone.
knoot inboxRead and clear pending notes for this user.
knoot hookThe shim. Reads a hook payload on stdin.
knoot whoreal output
priya        main   "Add refreshSession() to the auth module"      51s ago
             └─ src/tokens.js (lease 9m)
sam          main   -                                             51s ago
ash          main   "Refactor session handling to use short-lived tokens" 64s ago
             └─ src/auth.js (lease 8m)

Messages and notifications

Blocking alone is not coordination. Being denied a path registers interest in it. When the holder releases it, by finishing, ending, or letting the lease lapse, every waiter is told, along with what the holder was doing.

Delivery rides the turn-end hook, so notice arrives the moment an agent tries to stop rather than whenever a person next types. A per-user cap means a chatty peer can never keep a session spinning.

Agents also write to each other directly.

knoot msg priya "auth.js is yours, exports are stable"
knoot msg all "goal is green, stop editing"

Identity comes from KNOOT_USER, falling back to $USER. Set it per session to give agents readable names in conflict briefs.

The hosted relay

Sign in to the console and create a team. Each person mints a device key per machine, and invites bring colleagues in under their own address. Keys are stored as hashes, so an existing one can never be shown again, only replaced.

The console shows the live event log for every repository your team has connected, which is also the fastest way to confirm coordination is really on. Its Memory view lists what your team has written down, by whom and when; on knoot.dev the text itself is sealed, see Encryption.

Encryption

On knoot.dev, the facts your team publishes are sealed end to end with MLS (Messaging Layer Security, RFC 9420), the IETF standard for encrypting to a group of devices. A fact is encrypted on the laptop that writes it, with a key only your team's machines hold, and the relay stores and forwards ciphertext it cannot open. Neither the relay nor anyone operating it can read your memory, and neither can a copy of its database.

What the relay can and cannot see

  • Never sent at all: file contents, diffs, commands, transcripts. This holds with or without encryption.
  • Sealed: the facts, plans and derived knowledge your agents publish to memory.
  • Readable to the relay: file paths in claims and writes, the one-line intent of each session, who is connected, and messages between people. Coordinating agents is reading these, so they cannot be hidden from the thing that coordinates.

How a machine gets the key

Each room — the access group over your repositories, general by default — is an MLS group, and each machine that has run knoot join is a member. The server never holds the group key, so it cannot hand it out: a new machine is admitted by a daemon that is already in the room, which happens on its own the next time a teammate's daemon is running. Until then the new machine coordinates normally and simply cannot read the room's memory yet.

Removing a person or a machine moves the key forward, so what is written afterwards is unreadable to it.

The same property that keeps the relay out has one cost: if every machine in a room lost its knoot state at once — ~/.knoot/mls on every laptop — nobody would hold the key, and that room's memory could not be read again, backups of the relay included. A team that has anyone's laptop still enrolled never meets this.

Checking it

knoot status on a machine prints the provider (provider mls), and the status page shows whether the relay can read the memory it stores. A relay you run yourself is readable by default; set KNOOT_KEY_PROVIDER=mls on it to seal memory the same way.

Your own relay

The relay is one process and one SQLite file, and nothing about it is specific to knoot.dev. The quickest way is the container behind a proxy that handles TLS; deploy/selfhost/ in the repository is a working setup with Caddy:

docker run -d -p 127.0.0.1:7420:7420 -v knoot:/data -e KNOOT_RELAY_TOKEN=$(openssl rand -hex 24) ghcr.io/ash20pk/knoot:latest

Or the binary itself, anywhere:

KNOOT_RELAY_TOKEN=$(openssl rand -hex 24) knoot relay --listen 127.0.0.1:7420

Without KNOOT_RELAY_TOKEN a relay starts open, which is right for 127.0.0.1 and wrong for anything else. Use wss:// off-machine: a bearer token over plaintext is a token anyone on the path can take.

Register your team with a POST to /api/register, which returns the owner's key; knoot join it, then knoot member add each colleague, who gets a key of their own. Registration is open so anyone can start a team on a public relay — on your own, close it at the proxy once your team exists. Add KNOOT_KEY_PROVIDER=mls to seal memory end to end, as knoot.dev does.

A relay that refuses you still fails open. A rejected token means coordination is off, not that anyone is blocked: the daemon says so once on stderr, and every edit is allowed. An operator's mistake cannot become an outage for the team.

The deploy/ directory provisions a relay on a fresh Ubuntu host: TLS terminated by Caddy in front of a loopback-bound relay, systemd keeping it up, and a token generated on the box rather than in the repository.

Backups

The event log is the product, so it is protected in two layers that fail differently.

  • Nightly snapshots on the box. A sqlite3 .backup, never cp, which half-copies a write-ahead database into one that restores as corrupt. Gzipped, seven kept. This covers a bad delete or a corrupted page.
  • Continuous replication off the box which covers losing the host: ten seconds of loss rather than a day. It needs storage credentials, so it turns itself on only once they exist and says so loudly while it is off.

Hook protocol

Every payload is a single JSON object on stdin. The fields knoot reads:

FieldMeaning
hook_event_nameOne of SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd.
session_idStable identifier for this agent session. Distinguishes two sessions run by the same person.
cwdAbsolute path inside the enrolled repository.
tool_nameEdit, Write or Bash. Others are ignored.
tool_input.file_pathThe file about to be written.
tool_input.commandFor Bash: the command line, parsed for write targets.
promptOn UserPromptSubmit: summarised into the intent sentence peers are shown.

A refusal comes back as hookSpecificOutput.permissionDecision: "deny" with a permissionDecisionReason string. SessionStart instead returns additionalContext, listing the other active sessions and what changed under you since your last turn.

SessionStartadditionalContext, real output
knoot: 2 other active session(s) on this repo right now:
- priya on branch main: "Add refreshSession() to the auth module"
  [working in: src/tokens.js]
- ash on branch main: "Refactor session handling to use short-lived tokens"
  [working in: src/auth.js]
Avoid editing files they are working in; you will be blocked with
details if you try, and told automatically when a file you were
blocked on is released.

Shell writes are gated too, or the scheme would be optional. Redirects, heredoc targets, tee, sed -i, cp, mv, rm and dd of= are parsed out and checked. Quoting is respected and heredoc bodies are skipped.

Event types

One sequenced log per repository. These are the types you will see in the console and in knoot watch.

EventMeaning
session_startedAn agent joined, with its branch.
intent_declaredThe sentence peers are shown when they are blocked.
claim_acquiredA file was taken on a lease.
claim_deniedSomeone asked for a held file and was given a brief.
file_writtenA permitted write landed.
claim_releasedThe holder let a file go.
path_freedA lease lapsed; waiters were notified.
messageAn agent wrote to a peer or to everyone.
ungated_writeA write knoot could not gate landed on someone's claim. Detection, not prevention.
session_endedAn agent left; its claims were dropped.

Known limits

  • Interpreters are detected, not blocked. python3 -c "open(...)" writes first and is recorded second.
  • Attribution under concurrent writes is inferred. The working tree is shared, so a peer's edit lands inside our audit window too. Authorship comes from their file_written event; if that has not arrived, an ungated write can be attributed to the wrong session.
  • Sessions sharing a name share a mailbox. Mail is keyed by user, so two sessions running as the same KNOOT_USER both receive its notes.
  • Fail-open is ambiguous by design. An allowed edit and an unreachable daemon look identical from the agent's side. Check knoot status.