An open standard for portable document annotations — highlights, comments, and suggestions — that any editor can support.
annox splits the problem the way LSP does: a portable data format that tools can read and write directly, and an optional protocol that lets editors talk to an annotation server instead of each editor integrating with each backend.
Warning
Status: spec version 0.1, a complete first draft. Expect incompatible changes before 1.0. See spec/ and the changelog.
A review goes through four steps. Whoever acts, a person in an editor or an agent at the command line, each action writes one event file to .annox/.
Here it is in Neovim: replying to a comment, commenting on a selection, accepting a suggestion, and suggesting edits as you type.
To try it, examples/ has a short proposal, already reviewed by two authors, with open, resolved, accepted, rejected, and orphaned annotations. Open examples/proposal.md in an editor with annox, or run annox report --verbose in that directory.
Editors talk to one server, and the CLI and agents work on the files directly:
flowchart LR
subgraph editors [Editors]
direction TB
nvim[Neovim plugin]
vscode[VS Code extension]
other[Any LSP editor]
end
subgraph tools [Scripts and agents]
direction TB
cli[annox CLI]
mcp[annox mcp]
end
editors -->|"LSP + annox/* (§6)"| lsp[annox lsp]
lsp -->|events| store[(".annox/ (§5)")]
tools -->|events| store
store <-->|commit, pull| git[(git remote)]
lsp <-->|"WebSocket (§7)"| hub[annox hub]
hub <--> peers[Collaborators' servers]
A Rust reference implementation lives in crates/. It covers every section:
| Spec | Implemented in | Tested by |
|---|---|---|
| §2 Data model | annox-core (event, replay) |
replay vectors, reverse-order replay |
| §3 Anchoring | annox-core (text, anchor) |
anchoring vectors, worked example |
| §4 Suggestions | annox-core (suggestion) and the server |
suggestion vectors, accept/revert end-to-end |
| §5 Storage | annox-core (storage, ops) |
storage vectors, moves and publishing end-to-end |
| §6 Protocol | annox-lsp (annox lsp) |
in-process LSP tests, headless Neovim |
| §7 Sync | annox-sync (annox hub, replica) |
hub over WebSockets, two servers syncing |
Crates:
annox-coreis the library. It covers normalization, anchoring (§3), suggestions (§4), event replay (§2), and storage (§5). Its conformance tests run every vector inspec/tests/.annox-synchas the sync hub and replica (§7).annox-lspbuilds theannoxbinary.annox lspruns the language server (§6) over stdio, andannox hubruns a sync hub.
Install the latest annox binary on Linux or macOS (Apple silicon) into ~/.local/bin. Add --skill to also install the agent skill for Claude Code and other agents, and see --help for the version and directories. It verifies the download against the release's SHA256SUMS, and puts the man pages in ~/.local/share/man, for man annox and man annox-<command>.
curl -fsSL https://gaardhus.github.io/annox/install.sh | sh
curl -fsSL https://gaardhus.github.io/annox/install.sh | sh -s -- --skillOn Windows, install.ps1 does the same, and also adds ~\.local\bin to your user PATH (unless you pass -NoModifyPath). See -Help for its options.
powershell -ExecutionPolicy ByPass -c "irm https://gaardhus.github.io/annox/install.ps1 | iex"
powershell -ExecutionPolicy ByPass -c "& ([scriptblock]::Create((irm https://gaardhus.github.io/annox/install.ps1))) -Skill"Later, annox update installs the newest release over the binary you run it from, and also updates the skill if it's installed. Pass --version vX.Y.Z to pick a release.
To build from source with cargo build --release, see Contributing.
The server implements:
- For plain LSP editors (§6.5): annotations as diagnostics, threads on hover, and code actions to accept or reject suggestions and to resolve or reopen threads.
- For annox-aware plugins (§6.6): every
annox/*extension method. That covers creating, replying, editing, publishing local drafts, setting status, accepting and bulk-accepting, re-targeting, re-attaching, deleting and restoring, resolving conflicts (including reverting an accepted edit), moving documents, and history. It pushesannox/didChangeAnnotationsafter every change.
Edits always go through workspace/applyEdit, and the status event is written only once the editor confirms. The server picks up outside changes to .annox/, such as a git pull. It uses the editor's file watching where available, and otherwise checks every second. It offers to create a workspace when you add the first annotation outside one.
The annox binary also reads and writes annotations directly, printing JSON (annox report prints a one-line-per-file summary, or per author with --by author, or a per-status breakdown with --verbose, and annox history one line per event, unless given --json). It's meant for scripts and AI agents: text is targeted by quoting it, not by offsets, and an ambiguous quote is an error that lists where it occurs.
annox init [--local]
annox list [FILE] [--all | --closed] [--kind K] [--status S] [--mine | --others] [--broken] [--unread]
annox list paper.md --quote "the bound is tight" # everything on that text, closed annotations too
annox show ID
annox history ID [--json]
annox log [FILE] [--json] [--follow] # every event, by anyone; -f prints new ones as they're written
annox report [FILE] [--json | --verbose] [--by author,document] [--mine | --others] [--unread] ...
annox comment paper.md --quote "the bound is tight" --body "Cite Lemma 4?"
annox highlight paper.md --quote "the bound is tight"
annox suggest paper.md --quote "teh" --replace "the" --body "typo"
annox reply ID --body "Fixed."
annox status ID resolved
annox accept ID
annox revert ID [--accept] [--body TEXT]
annox read ID... | --all
annox commit [-m TEXT] [--dry-run]annox list --unread lists the threads others changed since you last read them, and every entry says whether it's unread. annox read marks threads read, and writing to a thread does too. What each author id has read is kept in the git-ignored .annox/local/read.json, so an agent with its own id (below) has its own unread list, and its reading doesn't mark anything read for you. list --as ID reads as another id.
Write commands use --author/--name, or ANNOX_AUTHOR/ANNOX_AUTHOR_NAME, and otherwise your configured identity. Give an agent its own id, for example ANNOX_AUTHOR=urn:agent:claude, so its annotations are distinguishable from yours. A running annox lsp picks up the changes, so they appear in the editor. Run annox help for every command.
annox init creates .annox/ at the project root, which is meant to be committed so collaborators see the annotations. Every change is a new file there, so they add up between commits. annox report counts them, and annox commit commits just those files, with a message summarizing them, and leaves anything else you've staged alone. It doesn't push. --dry-run prints what it would commit. The editor plugins have the same command, which lets you edit the message first. To keep them to yourself, use annox init --local. It adds * to .annox/.gitignore, so git ignores the whole folder. To share them later, delete that line.
harnesses/skills/annox/ is a skill that teaches an agent the CLI and how to use it well: suggest instead of editing text you own, and answer comments in their threads. Install it with install.sh --skill (see above). That puts it in ~/.claude/skills for Claude Code, and in ~/.agents/skills, which Codex, Gemini CLI, Copilot CLI, Cursor, OpenCode, and pi read. Pass --skill-dir DIR to install it only in DIR. Or link it into your skills from a checkout:
ln -s "$PWD/harnesses/skills/annox" ~/.claude/skills/annox
ln -s "$PWD/harnesses/skills/annox" ~/.agents/skills/annoxFor agents that can't run shell commands, annox mcp serves the same commands as MCP tools over stdio:
claude mcp add annox -- annox mcp --author urn:agent:claude --name ClaudeIt works on the directory it's started in, or the one given with --root.
annox watch prints, one JSON line each, the threads others changed for you, as they change: already unread ones first, then each new comment or reply once it has been unchanged for --settle milliseconds (default 2000). With --mention @claude, only threads that mention @claude or that the agent wrote in. Your own changes and threads you've marked read don't count, so an agent watching as itself never wakes itself up.
ANNOX_AUTHOR=urn:agent:claude annox watch --mention @claudeFor a feed of everything that happens, by anyone, your own changes included, use annox log --follow (add --json for one JSON line per event). It doesn't use read state, so don't point an agent at it: it would see, and answer, its own replies.
Anything that can read lines can drive an agent with annox watch. In Claude Code, ask the agent to run that command with its Monitor tool and handle each thread as it arrives.
annox mcp --channel does the same through a Claude Code channel: it pushes the threads into the running session, which handles them without being prompted. Channels are a research preview, so a custom one has to be loaded explicitly:
claude mcp add annox -- annox mcp --author urn:agent:claude --name Claude --channel --mention @claude
claude --dangerously-load-development-channels server:annoxThen comment @claude … in your editor and save. The agent reads files on disk, so it can't quote text you haven't saved yet. Its replies and suggestions appear in your editor as it writes them. Whoever can add annotations to the workspace can address the agent this way, collaborators through git or a sync hub included, so only use it in workspaces whose collaborators you trust with your session.
In pi, the annox extension does the same: start pi with --annox-watch=@pi, or run /annox-watch in a session.
To share annotations live instead of only through git, run a hub and point the workspace at it:
annox hub --data ~/annox-hub --listen 127.0.0.1:7878 --token-file ~/.annox-hub-tokenIn .annox/annox.json, which is safe to commit:
{ "format": 1, "sync": { "url": "wss://hub.example.org/w/my-paper" } }Each user adds the hub to their credentials file, which is never committed:
{ "hubs": { "wss://hub.example.org/w/my-paper": { "token": "…" } } }The server syncs only with hubs listed there. Remote hubs must use wss://, and plain ws:// is accepted only to localhost. The hub itself speaks plain WebSocket, so put it behind a TLS-terminating proxy to serve wss://. Sync and git work together. Events received from the hub are kept in the git-ignored .annox/synced/, so git status only lists the events you wrote, and pulling a collaborator's commit never collides with events that sync already delivered. Each person commits their own events. Collaborators' cursors appear in the editor as presence (§7.8). The hub doesn't implement read-only access yet.
- Neovim:
editors/nvim/is a plugin covering the full workflow: highlights, comments and suggestions, threads, accept and reject, suggestion mode, local drafts, re-targeting, conflict resolution, and history. - VS Code:
editors/vscode/is an extension with the same workflow, showing annotations as native comment threads beside those of other providers. Each release attaches it as a.vsix. - Any other LSP editor: point it at
annox lspfor the plain-LSP features. It acceptsinitializationOptionsof the form{ "annox": { "author": { "id": "…", "name": "…" }, "diagnostics": true } }(§6.2). Both are optional.
annox reads configuration from four places:
| Where | Scope | Settings |
|---|---|---|
.annox/annox.json |
The workspace, committed | format and sync.url, the hub to sync with (see Live sync) |
~/.config/annox/config.json |
You, on every workspace | author, your identity |
~/.config/annox/credentials.json |
You, never committed | hubs, a token per hub URL (see Live sync) |
| Editor settings | One editor | The LSP initializationOptions above, or the Neovim plugin's options |
The user files live under $XDG_CONFIG_HOME/annox/ when it's set. ANNOX_CONFIG_FILE and ANNOX_CREDENTIALS_FILE point at a file elsewhere. A missing file is the same as an empty one.
config.json sets the identity your annotations are written as:
{ "author": { "id": "mailto:ada@example.org", "name": "Ada Lovelace" } }The identity is taken from the first of these that is set:
- The command:
--author/--namefor the CLI andannox mcp, or the editor'sauthoroption forannox lsp. ANNOX_AUTHORandANNOX_AUTHOR_NAME.authorinconfig.json.- Git's
user.emailanduser.name, asmailto:plus the email.
A name alone only renames the identity from the next step down. An id without a name stands on its own, so an agent's ANNOX_AUTHOR=urn:agent:claude never picks up your name. If config.json is invalid, the CLI refuses to write, and annox lsp logs a warning and uses git's identity.
Contributions are welcome. CONTRIBUTING.md covers building and testing, changing the spec, and commit conventions. For anything bigger than a small fix, please open an issue first.
