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 | shThe 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/knootThe 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/wsThen, 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/wsknoot daemonTokens 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.
[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.
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
| Command | What it does |
|---|---|
knoot init | Enrol the current repository. Writes .knoot.toml and hook config. |
knoot join | Store this machine's device key for a relay, in ~/.knoot/credentials.toml, and print who it names. |
knoot login | Store a relay's shared token instead, for a self-hosted relay run on one secret. |
knoot daemon | Run the local claim mirror. One per machine. |
knoot relay | Run a relay: sequenced log, claims, arbitration. |
knoot status | Check binary, repository, hooks, daemon, relay and token. |
knoot who | Active sessions, their stated intent, and what they hold. |
knoot watch | Live dashboard of sessions, claims and collisions. |
knoot msg | Send a note to one peer or to everyone. |
knoot inbox | Read and clear pending notes for this user. |
knoot hook | The shim. Reads a hook payload on stdin. |
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:latestOr the binary itself, anywhere:
KNOOT_RELAY_TOKEN=$(openssl rand -hex 24) knoot relay --listen 127.0.0.1:7420Without 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, nevercp, 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:
| Field | Meaning |
|---|---|
hook_event_name | One of SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd. |
session_id | Stable identifier for this agent session. Distinguishes two sessions run by the same person. |
cwd | Absolute path inside the enrolled repository. |
tool_name | Edit, Write or Bash. Others are ignored. |
tool_input.file_path | The file about to be written. |
tool_input.command | For Bash: the command line, parsed for write targets. |
prompt | On 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.
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.
| Event | Meaning |
|---|---|
session_started | An agent joined, with its branch. |
intent_declared | The sentence peers are shown when they are blocked. |
claim_acquired | A file was taken on a lease. |
claim_denied | Someone asked for a held file and was given a brief. |
file_written | A permitted write landed. |
claim_released | The holder let a file go. |
path_freed | A lease lapsed; waiters were notified. |
message | An agent wrote to a peer or to everyone. |
ungated_write | A write knoot could not gate landed on someone's claim. Detection, not prevention. |
session_ended | An 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_writtenevent; 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_USERboth 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.