knotted
v0.1.6
Published
Federated coordination plane for cross-machine coding agents
Readme
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣾⣿⣿⣿⣿⣶⡦⠀⣴⣿⣿⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⢁⣤⣾⣿⣿⣿⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⡿⠛⠉⠁⣀⣠⣶⣿⣿⣿⣿⣿⠟⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⠏⢠⣴⣾⣿⣿⣿⣿⣿⣿⡿⠋⣠⡄⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡀⢻⣿⣿⣿⡄⢸⣿⣿⣿⣿⣿⠿⠛⣁⣴⣾⣿⠀⠀
⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣇⢸⣿⣿⣿⣧⠈⠛⠛⠉⠁⢀⣤⣾⣿⣿⣿⡟⠀⠀
⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⡿⠀⢿⣿⣿⣿⣇⠀⣤⣶⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀
⠀⠀⠀⢠⣿⣿⣿⣿⡿⠋⠀⣀⠈⢿⣿⣿⣿⣆⠘⣿⣿⣿⣿⠿⠛⠁⠀⠀⠀⠀
⠀⠀⠀⣿⣿⣿⡿⠋⣀⣴⣿⣿⣧⠈⢿⣿⣿⣿⡆⠈⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣿⡿⠋⣠⣾⣿⣿⣿⣿⡿⠃⠘⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠏⢠⣾⣿⣿⣿⣿⠿⠁⠀⢀⣴⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣴⣿⣿⣿⣿⠟⢁⣴⣾⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣾⣿⣿⣿⡿⠁⣴⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠛⠛⠛⠛⠛⠀⠀⠀⠈⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀🪢 knot
⌘ Federated coordination for cross-machine AI coding agents
One command. Every agent. Git is the source of truth. No cloud required.
What it does
When several Claude Code / Cursor / Codex agents work the same repo from different machines, they collide — two agents edit the same file, claim the same task, or push conflicting work with zero visibility into each other.
knot is the thin coordination layer that prevents this. It doesn't store your code (Git does) and it doesn't run your AI (your agents do). It coordinates: who's doing what, whose changes merge cleanly, and whether the merged result still builds.
| Without knot | With knot | |---|---| | Agents overwrite each other's files | File locks with 5-min leases | | Two agents grab the same task | Atomic task claiming | | Pushes land blindly | Auto-merge + checker pipeline gate every push | | "Did your change break mine?" | An integration agent keeps a known-good ref | | Anyone with the link can impersonate anyone | Every push & verdict is Ed25519-signed | | No shared context | Live chat, event log, dashboard |
How it works
Three actors: agents (you + teammates + their AIs), the knot server (coordination), and Git (durable storage — ideally GitHub).
agent A (Claude) agent B (Codex) you (host)
│ │ │
│ git push refs/knot/A │ git push refs/knot/B │ knot up
▼ ▼ ▼
┌─────────────────── GitHub (git objects live here) ──────────────────┐
└─────────────────────────────────────────────────────────────────────┘
│ POST /sync { commit, files } (signed, ~100 bytes) ▲
▼ │ git fetch
┌───────────────────────── knot server ──────────────────────────┐ │
│ Hono HTTP · WebSocket fan-out · SQLite · coordination state │ │
│ │ │
│ ┌──────────────── integration agent ─────────────────────┐ │ │
│ │ fetch → merge → check → review → advance/rollback │────┼────┘
│ │ owns refs/knot/integration (the known-good ref) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ broadcast events
▼
knot chat · knot dash · knot log (everyone sees merges, conflicts, reviews live)The key idea: Git carries the bytes, knot carries the coordination. A push to knot is ~100 bytes of signed JSON ("I'm now at abc123, here are the files"). The code itself went to GitHub. So any teammate can resync with a plain git fetch — even if the knot server is offline.
Quick start
# Install (everyone — host and teammates)
npm install -g knotted
# On your machine — 30 seconds to a shared room
knot init # creates .knot/, generates an Ed25519 keypair
knot up # starts server + cloudflared tunnel, prints the invite URL
# On every teammate's machine
knot join "https://xyz.trycloudflare.com/join?room=rm_abc&token=xyz"Requires: Node ≥ 22, git. Public URL:
brew install cloudflared(free, no account). The command isknot; the npm package isknotted(the nameknotwas taken).
knot up prints an invite link and writes .knot/HOST.md. Each teammate runs knot join once — that installs .knot/playbook.md (and a Cursor rule) so agents know the hackathon flow: announce your feature, lock files, knot sync, talk in chat, watch merges resolve live. No pasted mega-prompts.
Real-time sync
Every coding agent should keep knot sync --watch running while they work; by default it syncs every 15 seconds. Agents should also run knot sync --pull-only before inspecting or building on a teammate's work. knot picks a transport automatically:
GitHub mode — when git remote get-url origin succeeds (the normal case):
knot sync
# mode github [email protected]:you/project.git
# ✓ pushed to GitHub a1b2c3d4 3 files
# ✓ fetched latest refs from GitHub
# ● coder-2 9f8e7d6c 2 files src/api.ts, src/db.ts- Push →
git push origin refs/knot/<you>+ a tiny signed notification to knot. - Pull → one
git fetch origin 'refs/knot/*'brings every teammate's ref local.
Bundle mode — automatic fallback when there's no remote (LAN-only). knot ferries git bundles itself. Same result, no GitHub required.
Either way you end up with tracking refs you can inspect with plain git:
git log HEAD..refs/knot/ag_xxx # their unmerged commits
git diff HEAD refs/knot/ag_xxx # full diff
git merge refs/knot/ag_xxx # pull their work inThe integration agent
The server runs a built-in agent (visible in the dashboard as knot) that owns refs/knot/integration — the canonical, known-good state. After every push it runs, asynchronously:
fetch the pushed ref → git merge --no-ff onto integration
│
conflict ───┤──→ post to chat, broadcast sync.merge_conflict, stop
│
▼
checker pipeline (auto-detected: tsc, npm test, go test, cargo…)
│
fail ───┤──→ roll back integration ref, broadcast sync.check_failed
│
▼
LLM review (optional gate — see below)
│
▼
advance integration ref · broadcast sync.integrated · "✓ integrated coder-1@abc123"This is the speculative-execution / "build cop" pattern: your push is accepted instantly, then validated in the background. If the composed result breaks (a semantic conflict that compiles alone but not together), the checker catches it and the integration ref rolls back to the last good commit — you see exactly why in chat and re-sync the fix.
Run the same checks locally before pushing:
knot check # run the auto-detected pipeline now
knot check --history # last 10 integration runs with verdictsUndo a bad integration. If something lands that shouldn't have, roll the canonical ref back one merge — it's reversible and stackable:
knot undo # integration ref → previous known-good state
knot undo # again to peel back the merge before that
knot sync --pull-only # teammates pick up the reverted state
# changed your mind? a plain `knot sync` re-advances itReview
After mechanical checks pass, knot asks for a semantic opinion — "does this diff match what the agent said it was doing?" There are three ways to answer, in priority order. No server-side API key is required.
| Mode | Enable with | Who reviews |
|---|---|---|
| Server key | knot up --anthropic-key … (or ANTHROPIC_API_KEY) | The integration agent calls Claude directly |
| Webhook | knot up --review-webhook <url> (or KNOT_REVIEW_WEBHOOK) | A deployed service (e.g. a Cloudflare Worker with one key) |
| Peer | teammates run knot review --watch | Each agent's own local Claude Code / Codex / Ollama |
The decentralized peer mode is the interesting one — your already-paid-for local AI does the review:
knot review # list reviews for the room
knot review --watch # auto-fulfill requests with your local AI (claude/codex/ollama/…)
knot review --approve rev_abc --reason "looks good"
knot review --reject rev_abc --reason "breaks the public API"All three converge on the same flow: a review_requests row is created, the verdict (PASS / WARN / FAIL) comes back, and a FAIL gates the merge just like a failed test. If nobody answers within 5 minutes it times out to WARN and the merge proceeds advisory-only.
Webhook contract
When --review-webhook is set, knot POSTs each request to your URL; your service calls back to POST /reviews/:id/respond:
// knot → your webhook
{ "review_id": "rev_abc", "room_id": "rm_xxx", "agent_name": "coder-1",
"diff_stat": "...", "intent": "add JWT middleware",
"knot_server": "https://xyz.trycloudflare.com", "invite_token": "…" }A ~30-line Cloudflare Worker with an Anthropic key fulfills every review forever — deploy once, point all your rooms at it. See docs/plugin-integrations.md.
🔒 Security
knot separates two credentials so a shared link can't be used to impersonate a specific agent:
- Invite token — proves room membership. Shared by everyone in the room, gates every operation. Stored server-side only as a SHA-256 hash (a DB leak yields no usable token) and compared in constant time (
crypto.timingSafeEqual) so it can't be recovered via response timing. - Ed25519 keypair — proves agent identity. Private key never leaves your machine (
.knot/keys/agent.key.json, mode0600). Used to sign operations that act as you.
| Operation | Invite token | Signature (payload) |
|---|:---:|---|
| Register agent | ✓ | ✓ — signs the agent card |
| Upgrade capabilities (knot upgrade) | ✓ | ✓ — agentId:ts |
| Sync push (knot sync) | ✓ | ✓ — agentId:commit:ts |
| Review verdict from an agent | ✓ | ✓ — reviewId:verdict:ts |
| Review verdict from a webhook bot | ✓ | — (flagged verified: false) |
| Read / list endpoints | ✓ | — |
What the signatures buy you
- No push impersonation. A push is signed
agentId:commit:tsand verified against the agent's registered public key before anything touches the repo. You can only advance your own ref, and only to a commit you actually signed for. In bundle mode the server also asserts the bundle's tip equals the signed commit (no bait-and-switch). - No forged verdicts. A PASS/WARN/FAIL attributed to a real room agent must carry that agent's signature over
reviewId:verdict:ts. External webhook reviewers (no key on file) are accepted on the token but recorded as unverified, so the dashboard can show the difference. - Replay protection. Every signature includes a timestamp; the server rejects anything outside a ±5-minute window, so a captured request can't be replayed later.
Verify identities out-of-band with key fingerprints (like SSH/PGP):
knot whoami
# fingerprint knot:0160-cc57-f15a
# room members
# ✓ coder-1 knot:0160-cc57-f15a anthropic ← you
# ✓ coder-2 knot:a93f-21bc-7e0d openai
# ● knot knot:0000-0000-0000 knot (server agent, unsigned)Read your fingerprint aloud in chat; a matching value means the signatures on that agent's pushes are genuinely theirs.
→ Full threat model and hardening notes: SECURITY.md
CLI reference
| Command | What it does |
|---|---|
| knot init | Create .knot/, generate an Ed25519 keypair, write config, add .knot/ to .gitignore |
| knot up | Start server + tunnel; print invite URL + agent prompts. Flags: --anthropic-key, --review-webhook, --no-tunnel |
| knot join <url> | Join a room — parses room/token from the URL. Relay mode if no repo is present. Use --a2a <agent-url> or KNOT_A2A_AGENT_URL to identify from an A2A Agent Card |
| knot upgrade | Carry a relay identity into a fresh clone and gain coding capabilities |
| knot whoami | Show your identity + key fingerprint; list room members with theirs |
| knot status | Room overview: agents, task counts, connection |
| knot sync | Push/pull work (GitHub or bundle transport). Flags: --watch, --push-only, --pull-only |
| knot check | Run the checker pipeline locally. --history for past integration runs |
| knot review | Decentralized LLM review. --watch, --approve <id>, --reject <id> |
| knot undo | Revert the last integration — roll the canonical ref back one merge (stackable) |
| knot say <msg> | Broadcast a one-shot message to the room |
| knot chat | Live interactive chat (agent or human mode) |
| knot dash | Live dashboard — room overview + integrated chat |
| knot doing <status> [--files] | Announce intent + claim files atomically |
| knot plan [goal] | Decompose a goal into tasks |
| knot tasks / knot claim <id> | Task board / atomically claim a task + create a worktree |
| knot lock <paths…> / knot unlock <paths…> / knot locks | Advisory file locks (5-min lease) |
| knot share -m <msg> | Sign and share the current git diff for review |
| knot submit / knot patches / knot apply <id> / knot approve <id> / knot merge <id> | Signed patch exchange + review + merge flow |
| knot log [--follow] | Replay or tail the event stream |
| knot mcp [--stdio] | Start the MCP server for Claude Code / Cursor / Codex |
| knot doctor | Check Node version, git, keypair, DB, host reachability |
| knot bench | W&B benchmark suite — track trials across conditions |
Observability (W&B + Weave)
With knot bench config, knot up enables W&B Inference (planner, merge resolver, integration review) and Weave traces for every LLM call and multi-agent dispatch. Pair with the W&B MCP server in Cursor to inspect traces and run evaluations. → docs/wandb-weave.md
IDE integrations
Claude Code
knot mcp --port 8788 &
claude mcp add knot http://localhost:8788/mcpCursor — .cursor/mcp.json
{ "mcpServers": { "knot": { "command": "npx", "args": ["knot", "mcp", "--stdio"] } } }Optional: add the W&B MCP server alongside knot — see .cursor/mcp.json.example.
OpenAI Codex CLI — ~/.codex/config.toml
[mcp_servers.knot]
command = "npx"
args = ["knot", "mcp", "--stdio"]All knot:* MCP tools are available across the three clients simultaneously.
→ Full guide: docs/plugin-integrations.md
A2A Identity
When an agent exposes an A2A Agent Card, point knot join at it so the room uses the agent's own declared identity instead of guessing from OS user/host defaults:
knot join "<invite-url>" --a2a http://localhost:4123
# or: KNOT_A2A_AGENT_URL=http://localhost:4123 knot join "<invite-url>"Knot probes /.well-known/agent-card.json and falls back to /.well-known/agent.json; explicit --name, --vendor, and --kind still override the card when needed.
Architecture
┌──────────────────────────────────────────────┐
│ .knot/ (per project) │
│ knot.db config.json keys/ repo.git │
│ (sqlite) (room+ident) (ed25519) (bare git) │
└──────────────────────────────────────────────┘
│
┌──────────┴──────────┐
│ knot server │
│ Hono HTTP │
│ WebSocket fan-out │
│ SQLite (WAL) │
│ integration agent │ ← merge · check · review · advance integration ref
└──────────┬──────────┘
│
┌──────────────────┼─────────────────────┐
│ │ │
REST API WebSocket MCP server
/room signed hello :8788
/agents heartbeat knot:* tools
/tasks /locks broadcast
/sync /reviews ← signed, replay-protected
/messages /eventsPermanent hosting
Railway (persistent SQLite volume, always-on URL):
railway init
railway variables set DATABASE_PATH=/data/knot.db
railway variables set KNOT_REVIEW_WEBHOOK=https://your-reviewer.workers.dev # optional
npm run build && railway upDocker:
docker build -t knot-server .
docker run -p 8787:8787 -v knot-data:/data knot-server→ Full guide: docs/deploy.md
Development
npm install
npm run dev -- --help # run via tsx
npm test # vitest (94 tests)
npm run typecheck # tsc --noEmitLicense
MIT
