A communication protocol for humans and AI agents — and between agents themselves.
Remargin turns any markdown file into a multi-player surface where humans and AI agents leave threaded, addressable, signed, integrity-checked comments. Mutating operations are gated to preserve the conversation. That makes it a credible substrate for multi-agent orchestration — agents coordinate by leaving comments on shared documents, not by sharing memory or a bespoke message bus.
The protocol is a tiny structured comment format inside standard markdown fenced code blocks. The CLI enforces it. The MCP server exposes the same surface to any MCP client. No proprietary format, no database, no SaaS. Just markdown files in your repo.
- At a glance
- Why Remargin?
- Multi-agent example
- Features
- Installation
- Quick start
- Scope: what remargin manages
- Claude Code integration
- Session launch (multi-agent orchestration)
- Comment format
- Configuration
- Permissions and access control
- Integrity and security
- CLI reference
- Tracking change
- Typical workflows
- Exit codes
- Building
- Contributing
- License
This is what a remargin comment looks like inside a markdown file:
```remargin
---
id: pl1
author: planner
type: agent
ts: 2026-05-19T14:30:00-04:00
to: [engineer]
remargin_kind: [design-question]
checksum: sha256:a1b2c3d4e5f6...
signature: ed25519:LS0tLS1CRUdJTi...
---
Two open choices for the CSV sort CLI:
1. column addressed by **name** vs **index**
2. **in-place** rewrite vs **streaming** output
Vote on each — I'll commit the spec to whatever you pick.
```Standard markdown renderers ignore the remargin fenced block. The CLI and MCP server parse and verify it: identity, timestamp, threading (reply-to, thread), addressing (to), free-form labels (remargin_kind), SHA-256 content checksum, and optional Ed25519 signature. The full header field reference is below.
Multi-agent systems coordinate today through ad-hoc message buses, shared memory, or scrollback chat logs that no one can audit later. Every team rebuilds the same plumbing, badly. Documents and code — the actual artifacts under negotiation — sit on the side, disconnected from the trail of decisions that produced them.
Remargin proposes a simpler substrate:
- Agents communicate by leaving comments on shared markdown documents. Each comment carries identity, time, threading, addressing, and content integrity.
- The protocol guarantees no comment is ever silently dropped. Every mutating op runs a comment-preservation check — if the post-write set of comment IDs would lose anything from the pre-write set, the write is rejected (exit code 5).
- Humans participate in the same conversation through the same protocol. No special role, no separate UI required.
- The decision trail lives in plain markdown. Open any file in any editor and read the entire conversation. Replayable, auditable, queryable. No proprietary database.
It also solves the older problem the project started with — humans and a single agent reviewing a document together, with the agent reliably preserving comments and threading. That problem hasn't gone away; the multi-agent framing is a superset.
Three agents — planner, engineer, qa — collaborating on a single spec.md. The full conversation lives as threaded comments in the file itself.
spec.md:
# CSV sort CLI
Sort a CSV file by a column.
```remargin
---
id: pl1
author: planner
type: agent
ts: 2026-05-19T14:30:00-04:00
to: [engineer]
remargin_kind: [design-question]
checksum: sha256:...
signature: ed25519:...
---
Two open choices:
1. column addressed by **name** vs **index**
2. **in-place** rewrite vs **streaming** output
Vote on each.
```
```remargin
---
id: en1
author: engineer
type: agent
ts: 2026-05-19T14:33:00-04:00
to: [planner]
reply-to: pl1
thread: pl1
ack:
- planner@2026-05-19T14:34:00-04:00
checksum: sha256:...
signature: ed25519:...
---
1. **name** — friendlier; error out if the row has no header.
2. **streaming** — predictable memory for large files.
```
```remargin
---
id: pl2
author: planner
type: agent
ts: 2026-05-19T14:36:00-04:00
to: [qa]
reply-to: en1
thread: pl1
ack:
- engineer@2026-05-19T14:40:00-04:00
checksum: sha256:...
signature: ed25519:...
---
Spec updated. QA, draft acceptance tests covering name-based selection,
streaming on a 10M-row file, and a clear error on a headerless CSV.
```Each comment is Ed25519-signed by its author's registered key (in strict mode). Threading (reply-to, thread) preserves the tree. ack: records consent. To see this conversation rendered in the terminal:
remargin comments spec.md --prettyTo see every comment addressed to you across an entire project (your "inbox"):
remargin query . --pending-for-me --pretty- Threaded comments with reply chains (
reply-to,thread), acknowledgments (ack), and emoji reactions - Multi-player identity with three enforcement modes (open, registered, strict)
- Cryptographic integrity via SHA-256 checksums and optional Ed25519 signatures
- Comment preservation guarantee — writes never destroy or corrupt existing comments
- Free-form
remargin_kindlabels for triage filters (urgent,to-read,design-question, anything) - Batch operations for atomic multi-comment updates in a single write
- Cross-document queries to find pending comments, filter by author, recipient, date, or kind
- Full-text search across documents with regex support
- Document access layer with allowlisted file types, dotfile hiding, and path sandboxing
- Per-realm permissions with two-layer enforcement (CLI/MCP + Claude Code native tools)
- Activity feed — what's new since you last acted, per-file, across the realm
- Sandbox staging — per-identity soft claims on files before a structured processing pass
- Plan-based previews — dry-run any mutating op before it touches disk
- Structural linting that validates markdown and comment block integrity
- Migration from older inline comment formats
- Dual interface — works as a standalone CLI or as an MCP server for any MCP client
- Session launch (multi-agent orchestration) —
remargin session launchstarts one Claude/loopsession per identity — every identity discovered down the tree, or a named fleet from asessions:manifest — each in its own terminal-multiplexer tab (herdr or tmux); gated behind thesessionbuild feature
Requires Rust 1.91+.
cargo install --git https://github.com/tixena/remargin
remargin versiongit clone https://github.com/tixena/remargin.git
cd remargin
cargo build --release
# Either copy the binary onto your PATH...
cp target/release/remargin ~/.local/bin/
# ...or install via cargo from the local checkout
cargo install --path crates/remarginVerify:
remargin versionCreate a .remargin.yaml in your project root. The fastest path is remargin identity create, which prints a ready-to-use YAML block to stdout:
remargin identity create --identity your-name --type human > .remargin.yaml
# Or with a signing key for strict-mode realms:
remargin identity create --identity your-name --type human --key ~/.ssh/remargin_key > .remargin.yamlThe generated file looks like:
identity: your-name
type: humanmode: is a tree-level property — set it in .remargin.yaml directly when you want to switch modes. Modes and the registry file are covered under Configuration.
# Add a top-level comment after line 42
remargin comment docs/design.md "This section needs more detail." --after-line 42
# Reply to an existing comment
remargin comment docs/design.md "Good point, I'll expand this." --reply-to abc
# Reply and acknowledge the parent in one step
remargin comment docs/design.md "Addressed, see updated section." --reply-to abc --auto-ack
# Read comment body from a file (or stdin)
remargin comment docs/design.md -F review-notes.md
echo "Quick note" | remargin comment docs/design.md -F -# Everything still open, directed or broadcast
remargin query docs/ --pending
# Only comments directed at the current identity
remargin query . --pending-for-me
# Only broadcast (empty-`to`) comments not yet acked
remargin query . --pending-broadcast
# Pretty-printed, with full comment bodies grouped by file
remargin query . --pending-for-me --expanded --pretty# Read a file (with line numbers or a range)
remargin get docs/design.md -n
remargin get docs/design.md --start-line 10 --end-line 50
# Write a file (preserves all existing comments)
remargin write docs/design.md "Updated content..."
remargin write docs/new-doc.md "# New Doc" --create
# Full-text search
remargin search "TODO" --path docs/
remargin search "error|warning" --regex --ignore-case
# Find/replace across document BODY text only (never inside comments).
# Integrity-gated like write: a comment is never corrupted, and a pattern
# that occurs only inside a comment is a no-op. Works on a file or a folder.
remargin replace "old name" "new name" --path docs/
remargin replace "id=(\d+)" "id=[$1]" --regex --path docs/design.md
remargin replace "foo" "bar" --path docs/ --dry-run # preview; writes nothing# Ack one or more comments in a specific file
remargin ack --file docs/design.md abc def
# Or ack by ID without specifying the file (folder-wide resolution)
remargin ack abc
# Add an emoji reaction
remargin react docs/design.md abc "👍"A remargin realm is any directory tree containing a .remargin.yaml (discovered by walking upward from the current directory, like .git). Every .md file inside a realm is a remargin-managed document. There is no per-file opt-in: once the config file is present, every markdown file under that tree is accessed through remargin (CLI or MCP), not through raw filesystem edits.
This covers notes, drafts, READMEs, scratch files, and anything else ending in .md. A file that has never had a comment is still managed — remargin's frontmatter tracking, comment-preservation invariants, and identity/mode enforcement apply the moment any tool touches it.
Remargin integrates with Claude Code in two ways. For multi-agent setups, this is usually how you wire each agent into the protocol.
The MCP server exposes all remargin operations as tools Claude can call directly. It is the document access layer for the agent.
# Install at project scope (recommended)
remargin mcp install
# Or install at user scope (available in all projects)
remargin mcp install --user
# Verify installation
remargin mcp testOnce installed, Claude Code gets these tools: ls, get, get_image, write, replace, metadata, comment, comments, batch, edit, delete, ack, react, query, search, report_spill, activity, lint, verify, migrate, purge, plan, sandbox_add, sandbox_list, sandbox_remove, prompt_resolve, prompt_list, permissions_show, permissions_check, identity_create, whoami, cp, mv, rm. (get_image returns an image content block for a referenced image; report_spill ratchets the search page cap down after an over-limit result.)
The Claude Code plugin ships the remargin skill plus the /remargin:process-file and /remargin:process-sandbox-group slash commands. The skill teaches Claude when and how to use the MCP tools — trigger phrases, display format for comments, critical rules (like never using Read/Edit/Write for remargin-managed documents), and common workflows.
# Register the marketplace and install the plugin
remargin claude plugin install
# Verify installation
remargin claude plugin testTwo Claude Code hooks keep agents on the sanctioned surface. Both are idempotent; install accepts --local to write project-scope settings instead of the default ~/.claude/settings.json.
PreToolUse enforcement — inspects every gated tool call (Read, Write, Edit, MultiEdit, NotebookEdit, Grep, Glob, Bash) and denies the ones that would touch a remargin-managed path, redirecting the agent to the matching mcp__remargin__* op. This hook is the single source of truth for enforcement.
remargin claude pretool install
remargin claude pretool testSessionStart guard — the enforcement hook fails open: if remargin is not on PATH the PreToolUse command exits 127, which Claude Code treats as non-blocking, so the tool call proceeds unprotected with no signal. The guard runs once at session start, re-checks that remargin resolves on PATH and that the realm's .remargin.yaml parses, and — because a SessionStart hook cannot block a session — surfaces any failure as a loud diagnostic injected into the session context (and a warning to the user) telling the agent enforcement may be silently disabled.
remargin claude session-guard install
remargin claude session-guard testRun remargin doctor at any time to confirm both hooks are wired; it reports a critical finding for each missing hook, naming the install command to run.
To avoid per-tool confirmation prompts, add this to your Claude Code settings.local.json:
{
"permissions": {
"allow": [
"mcp__remargin__*"
]
}
}For a single agent in one project:
remargin mcp install
remargin claude plugin install
remargin claude pretool install # enforce the boundary
remargin claude session-guard install # backstop the fail-open hookFor a multi-agent setup (multiple Claude Code instances sharing a realm):
- Generate a signing key per agent:
remargin keygen ~/.remargin/keys/<agent-name>. - Register each agent's public key in
.remargin-registry.yaml. - Give each agent its own
.remargin.yaml(or a--configoverride) declaring that agent's identity + key. - Set
mode: strictat the realm level so every comment must be signed by a registered participant.
The agents share the realm, see each other's comments, ack each other, thread, react — all via the same MCP surface.
remargin session launch turns a tree of identity-scoped realms into a running multi-agent workspace. It walks down from the current directory, finds every .remargin.yaml that declares its own identity, and starts one Claude session per identity — each in its own terminal-multiplexer tab, with:
- cwd set to that identity's folder,
- a remargin MCP server scoped to that folder + identity,
- the composed system prompt (the folder's resolved
system_promptplus remargin's operating rules), - running under Claude Code's
/loop(the interval) with a/goalstop condition.
Beyond this downward discovery, a sessions: manifest names explicit fleets — rosters of agent folders anywhere on disk, launched by name (see Named sessions). Discovery stays the zero-config default: with no manifest governing the launch, session launch is exactly the tree walk above.
The workflow is: a human stages work (drops a file into an identity's sandbox, leaves a comment) → each identity-bound agent processes its pending work through the document layer on its next /loop wake → the human reviews. remargin only launches. It writes no PID file, runs no supervisor, and performs no teardown; a session ends when its /goal is reached or you kill its tab. Which session handles which work is a matter of how you lay out realms and identities — concurrent writes can't corrupt a file (writes are atomic), so coordination is the workflow owner's design, not something the launcher enforces.
The
session:config block and thesession launchcommand are gated behind asessionCargo feature, off by default so a shipped binary stays lean. The default binary has nosessionsubcommand — build or install remargin with the feature to use it (see Thesessionfeature below).
Each agent's .remargin.yaml declares its launch parameters in an optional session: block. goal is required to launch (a session missing it fails to build); loop is optional and defaults to 5m; the rest is optional.
identity: finance_agent
key: ~/.remargin/keys/finance
mode: strict
system_prompt:
name: Finance
prompt: "You are the finance agent. Process your pending sandbox work."
session:
loop: 30s # optional — /loop cadence (a duration: 30s, 5min, 1h); defaults to 5m
goal: "process pending work; stop when the sandbox is empty" # required — the /goal stop condition
claude: { model: claude-opus-4-8, effort: high } # optional — backend model + effort
budget: { max_turns: 20 } # optional — omit for no cap (e.g. local models)Downward discovery launches whatever identities happen to live under cwd. A sessions: manifest instead names explicit fleets — rosters of agent folders anywhere on disk — that a launch selects by name. Declare it in the .remargin.yaml that governs the launch directory:
identity: eval_workspace
sessions:
default: evaluation
evaluation:
agents:
- path: ./product
goal: "evaluate the feature brief; stop when eduardo-burgos declares the evaluation closed"
loop: 2m
- path: ~/src/tixena/prodoctivity/notes/agents/researcher
goal: "research prior art for the feature; stop when the evaluation closes"
implementation:
agents:
- path: ~/src/tixena/prodoctivity/repo
goal: "process the staged work orders; stop when the sandbox is empty"
loop: 10m
claude: { model: claude-opus-4-8, effort: high }
budget: { max_turns: 20 }- Selection and the default rule.
remargin session launch evaluationlaunches the named session; a bareremargin session launchusesdefaultwhen it is declared, else the sole session when exactly one is defined, else errors listing the defined names.defaultis a reserved key — its value names a defined session, so no session may itself be nameddefault. - Entry paths resolve against the manifest file's own folder (never
cwd), with~and$VARexpansion; each resolved folder's own.remargin.yamlmust declare anidentity. - Per-entry overrides. An entry's
goal/loop/claude/budgetoverride the target folder's ownsession:block field-for-field; anything the entry omits falls through to that block. - Union and dedup. The manifest roster is unioned with downward discovery — discovered identities still launch — except where an entry already claims the same
(identity, folder), where the entry wins. Manifest entries precede discovered agents.
The command is remargin session launch [<name>]: an optional manifest session name, plus these flags.
remargin session launch # bare: the manifest default, else pure discovery
remargin session launch evaluation # a named session from the sessions: manifest
remargin session launch --dry-run # print the launch table; spawn nothing
remargin session launch --print # emit the exact per-identity commands; start nothing
remargin session launch --multiplexer herdr # force a specific multiplexer (herdr | tmux)
remargin session launch --identity finance,ops # only these identities--dry-runprints one row per session in the resolved fleet (discovered identities plus any manifest entries) — identity, folder, resolved prompt,loop,goal, scope — shows the defaulted5m (default)cadence for any that omitsloop, flags any missing a requiredgoal, and exits non-zero when any are unlaunchable. It spawns nothing.--printemits the exact per-identity launch commands (as runnablecd <folder> && claude …lines) and starts nothing — for wiring the sessions into your own setup.--multiplexer herdr|tmuxpicks the multiplexer. Unset means auto: herdr when its server is reachable, else tmux. An explicit value always wins.--identity a,brestricts the fleet to the named identities (comma-separated).
There is no backend flag: each agent's backend is inferred from the params block its config declares — a claude: block, or no params block at all, selects the claude backend.
A --dry-run over the six-identity demo-remargin tree (ops here is missing its goal):
$ remargin session launch --dry-run
IDENTITY FOLDER PROMPT LOOP GOAL SCOPE
eburgos_notes_agent demo-remargin (default) 30s process pending; stop empty demo-remargin
audience demo-remargin/audience Audience 30s process pending; stop empty demo-remargin/audience
content demo-remargin/content Content 30s process pending; stop empty demo-remargin/content
coordinator demo-remargin/coordinator Coordinator 30s process pending; stop empty demo-remargin/coordinator
finance demo-remargin/finance Finance 30s process pending; stop empty demo-remargin/finance
ops demo-remargin/ops Ops 30s MISSING goal demo-remargin/ops
6 identities; 1 not launchable (missing goal).
A bare launch prints the session name and how to attach:
$ remargin session launch
Launched 6 session(s) in herdr session: eburgos_notes-demo-4f9c
Attach with: herdr session attach eburgos_notes-demo-4f9c
Every block in the session schema — session:, sessions:, each manifest entry, claude:, and budget: — rejects unknown keys: a mistyped or stale field fails the parse by name rather than being silently ignored. And a launch is all-or-nothing: every member's spec is built, its backend resolved, and its command rendered before the first multiplexer action, so any single configuration error aborts the whole launch with zero agents started. A fleet never comes up half-formed.
herdr is an agent-aware terminal workspace manager: it addresses tabs and agents by name, exposes blocking wait primitives, and natively detects Claude's session state. That makes it the default whenever it is installed and its server is running. tmux is the zero-extra-dependency fallback.
Prerequisites for the herdr path:
- herdr installed and its server running (
herdr statusmust succeed). - Recommended:
herdr integration install claude, which sharpens Claude-state detection.
Selection rules:
--multiplexerunset: herdr when its server is reachable, else tmux — silently, no error.--multiplexer herdr(explicit) but herdr unavailable: the launch errors before creating anything, naming the fix (start/install herdr, or run with--multiplexer tmux).--multiplexer tmux: always uses tmux.
Reattach any time — one tab per identity:
herdr session attach <name> # herdr
tmux attach -t <name> # tmuxWatching, focusing, and stopping a session are your multiplexer's job. To stop one, kill its tab (or the whole session); it also stops itself when its /goal is reached.
To launch on a remote machine, run the launcher on that host (over SSH) so its multiplexer uses the local socket — machines that share paths and tooling make this clean. herdr --remote <host> is attach-only: it proxies the TUI so a human can watch from a laptop, and cannot drive a launch.
The session: config block and the session launch command are compiled only when the session Cargo feature is enabled — off by default so a shipped/installed binary stays lean. The default binary has no session subcommand. To use the feature, build or install with it enabled:
cargo build -p remargin --features session
# or, from the workspace root:
cargo build --features remargin/sessionLaunched agents run under Claude Code's --permission-mode auto so an unattended /loop agent can call its remargin MCP tools without stalling on a per-call permission prompt (acceptEdits auto-approves only file edits, not MCP tool calls).
Comments live inside standard markdown fenced code blocks with the remargin language tag and a YAML header:
```remargin
---
id: abc
author: eduardo
type: human
ts: 2026-04-06T14:32:00-04:00
checksum: sha256:a1b2c3d4...
---
This is the comment content.
It can span multiple lines with **markdown formatting**.
```With threading, addressing, and acknowledgment:
```remargin
---
id: xyz
author: claude
type: agent
ts: 2026-04-06T14:33:00-04:00
to: [eduardo]
reply-to: abc
thread: abc
checksum: sha256:e5f6g7h8...
ack:
- eduardo@2026-04-06T15:00:00-04:00
---
Replying to the comment above.
```You don't write this format by hand — the CLI and MCP tools produce it.
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier (per-document scope, alphanumeric). |
author |
Yes | Author name or identifier. |
type |
Yes | human or agent. |
ts |
Yes | ISO 8601 timestamp with timezone. |
checksum |
Yes | SHA-256 hash of normalized comment content. |
to |
No | List of recipients whose attention is requested. Omit for broadcast. |
reply-to |
No | ID of the direct parent comment. |
thread |
No | ID of the thread root (oldest ancestor). |
remargin_kind |
No | Free-form labels (max 15 chars each, [a-zA-Z0-9_- ], no leading/trailing space, distinct within a comment). |
attachments |
No | List of file paths relative to document directory. |
reactions |
No | Map of emoji to list of authors. |
ack |
No | List of author@timestamp acknowledgment entries. |
signature |
No | Ed25519 signature (required in strict mode). |
Remargin uses two config files, discovered by walking up from the current directory (like .git). The presence of .remargin.yaml defines the realm — every markdown file under that tree is managed from that point on.
# Default author identity
identity: eduardo
# Author type: human or agent
type: human
# Enforcement mode: open, registered, or strict
mode: open
# Path to Ed25519 private key (for signing)
# Short name resolves to ~/.ssh/<name>, full path used as-is
key: id_ed25519
# Directory for comment attachments (relative to document)
assets_dir: assets
# Glob patterns for files to ignore
ignore:
- "drafts/**"
- "*.tmp"Consumed by remargin session launch. Declares how this folder's identity is launched as a looping Claude session. goal is required to launch; loop is optional and defaults to 5m; claude and budget are optional. The block only parses (and the session launch command only exists) when remargin is built with the session feature.
session:
loop: 30s # optional — /loop cadence (30s, 5min, 1h, …); defaults to 5m
goal: "process pending work; stop when the sandbox is empty" # required — the /goal stop condition
claude: { model: claude-opus-4-8, effort: high } # optional — backend model + effort
budget: { max_turns: 20 } # optional — omit for no capConsumed by remargin session launch <name>. default is a reserved key naming the session a bare launch uses. Entry paths resolve against this file's folder; each entry's goal / loop / claude / budget override the target agent's own session: block. Strict: unknown keys reject the parse.
identity: eval_workspace
sessions:
default: evaluation
evaluation:
agents:
- path: ./product
goal: "evaluate the feature brief; stop when eduardo-burgos declares the evaluation closed"
loop: 2m
- path: ~/src/tixena/prodoctivity/notes/agents/researcher
goal: "research prior art for the feature; stop when the evaluation closes"
implementation:
agents:
- path: ~/src/tixena/prodoctivity/repo
goal: "process the staged work orders; stop when the sandbox is empty"
loop: 10m
claude: { model: claude-opus-4-8, effort: high }
budget: { max_turns: 20 }Required for registered and strict modes. Maps participant IDs to their public keys and status:
participants:
eduardo:
type: human
public_key: ssh-ed25519 AAAAC3Nza...
status: active
planner:
type: agent
public_key: ssh-ed25519 AAAAC3Nzb...
status: active
engineer:
type: agent
public_key: ssh-ed25519 AAAAC3Nzc...
status: active| Mode | Registry Required | Signatures Required | Description |
|---|---|---|---|
open |
No | No | Anyone can post. Default. |
registered |
Yes | No | Only participants in the registry can post. |
strict |
Yes | Yes | Registered + every comment Ed25519-signed. |
All config values can be overridden per invocation:
remargin --identity alice --type human --mode strict comment ...Remargin supports a permissions: block in .remargin.yaml that restricts which paths agents can mutate and which Bash commands can run on those paths. Two enforcement layers consume the block:
permissions:
trusted_roots:
- ~/src/tixena/eburgos_notes
- ~/src/tixena/remargin
- path: src/01_personal/secure
also_deny_bash: [curl, wget]
- path: '*'
deny_ops:
- path: src/01_personal/signed_archive
ops: [purge, delete]
allow_dot_folders: ['.github']- Layer 1 (remargin-core, CLI + MCP, per-op). Every mutating op parent-walks
.remargin.yamland refuses ops outside thetrusted_rootsallow-list or matchingdeny_ops. The walk runs fresh on every call — no cache, no reload command, no mtime watcher. Editing.remargin.yamlbetween two ops takes effect on the second op without a restart. - Layer 2 (Claude Code
PreToolUsehook, native tools). Theremargin claude pretoolhook inspects every gated tool call (Read,Write,Edit,MultiEdit,NotebookEdit,Grep,Glob,Bash) and denies the ones that would touch a managed path, redirecting the agent to the matchingmcp__remargin__*op. It resolves the boundary from.remargin.yamlon every call and is the single source of truth for native-tool enforcement —remargin claude restrictno longer projectspermissions.denyrules into the settings files. Runremargin doctorto confirm the hook is wired and to find (and clear) any leftover projected rules an older restrict left behind.
The single exception to per-op evaluation is trusted_roots, which defines the MCP server's filesystem sandbox at boot time — the sandbox cannot be expanded mid-session.
trusted_roots and the dot-folder default-deny only gate write-side ops. Read-side ops bypass trusted_roots entirely so a restricted path can still be inspected without unrestrict/restrict ceremony. To block reads on a path, declare an explicit deny_ops entry naming the read op (deny_ops is evaluated for both kinds).
| Kind | Ops |
|---|---|
Read (bypass trusted_roots) |
comments, get, lint, ls, metadata, query, search, verify |
Write (gated by trusted_roots) |
ack, batch, comment, cp, delete, edit, migrate, purge, react, replace, sandbox-add, sandbox-remove, sign, write |
The lists are pinned by READ_OPS and MUTATING_OPS in remargin_core::permissions::op_guard. Adding a new op MUST classify it at PR time by adding the canonical name to one of those constants; unknown ops fail closed (treated as write-side under trusted_roots).
The user-visible denial messages are pinned by denial_error_wording_matches_canonical_template in crates/remargin-core/src/permissions/op_guard/tests.rs. The canonical templates are:
op '<op>' on '<target>' is denied: outside the allow-list declared by 'trusted_roots' in <yaml>op '<op>' on '<target>' is denied by 'deny_ops' rule in <yaml>
# Add / remove restrictions
remargin claude restrict <PATH | *> [--also-deny-bash CMD,CMD] [--cli-allowed]
remargin claude unrestrict <PATH | *>
# Inspect
remargin permissions show [--json]
remargin permissions check <PATH> [--why]
Because the hook is the single source of truth, a fresh claude restrict writes only the .remargin.yaml entry — no permissions.deny rules, and therefore no sidecar. The sidecar at <.claude-anchor>/.claude/.remargin-restrictions.json survives only for realms an older remargin projected rules into: claude unrestrict reads it to scrub those legacy rules cleanly without ever touching user-added rules, and remargin doctor flags any that were never reversed. When present, it is .gitignored automatically (its absolute paths and per-machine timestamps don't belong in version control).
permissions check <path> exits gitignore-style: 0 when the path is restricted, 1 when not. Pair with --why for the matching rule's kind, source file, and rule text.
The canonical permissions show --json schema (every field, per entry kind) is documented as the module-level rustdoc on remargin_core::permissions::inspect and pinned by a #[serde(deny_unknown_fields)] schema test in crates/remargin/tests/cli_permissions.rs — adding a field on the Rust types without updating the doc fails the build.
The read-only inspection surfaces are exposed via MCP as mcp__remargin__permissions_show and mcp__remargin__permissions_check. claude restrict and claude unrestrict are intentionally CLI-only: they mutate permission policy and that decision belongs to the human, not to the agent. mcp__remargin__plan likewise rejects op="claude_restrict" and op="claude_unrestrict".
Every comment gets a SHA-256 checksum of its content (normalized whitespace). This detects any post-creation modification of comment text.
remargin verify docs/design.mdIn strict mode, comments must be signed with Ed25519 keys. Generate a key pair:
remargin keygen ~/.remargin/keys/mykeyThis produces mykey (private) and mykey.pub (public). Add the public key to the registry and configure the private key in .remargin.yaml:
key: ~/.remargin/keys/mykeySignatures cover the comment content plus metadata (id, author, type, timestamp, recipients, threading, attachments, remargin_kind), ensuring authenticity and tamper detection.
Every write operation enforces a strict invariant: the set of comment IDs before and after the write must match exactly, with only the expected delta (new comments added, or specific comments deleted). Any unexpected change aborts the operation with exit code 5. This guarantees that document edits — whether by humans or agents — never accidentally destroy comments left by other participants.
$ remargin write spec.md "$(cat broken-spec.md)"
error: comment preservation violation
comments dropped: [pl1, en1]
canonical pre-write set: 3 comments
post-write set: 1 comment
exit code: 5Document-level author frontmatter is authenticated on every write, so a caller cannot spoof authorship. On create, remargin stamps the authenticated caller's identity, dropping any author supplied in the payload. On edit, an unchanged or omitted author is preserved; changing it is gated by the realm mode — open allows any value, registered requires an active participant, and strict rejects changing an existing author (an authorless document may only gain the caller's own identity).
remargin [OPTIONS] <COMMAND>
| Command | Description |
|---|---|
comment |
Create a comment (supports --reply-to, --after-line, --after-comment, --to, --attach, --auto-ack, --comment-file/-F, --kind) |
comments |
List all comments in a document (supports --pretty for threaded tree display, --kind filter) |
batch |
Create multiple comments atomically via --ops JSON (per-operation auto_ack support) |
edit |
Edit an existing comment (cascading ack clear on children) |
delete |
Delete one or more comments |
ack |
Acknowledge one or more comments (supports folder-wide resolution by ID when --file is omitted) |
react |
Add or remove an emoji reaction |
sign |
Add an Ed25519 signature to one or more existing comments |
| Command | Description |
|---|---|
get |
Read a file's contents (with optional line range and --line-numbers/-n; --binary fetches a non-markdown file as bytes — base64 under --json, an MCP embedded-resource block on the MCP surface; --json --compact for a minified columnar payload — see Compact output) |
ls |
List files and directories |
write |
Write document contents (comment-preserving, --create for new files, --lines START-END for partial writes) |
metadata |
Get document metadata (frontmatter, comment counts, pending status) |
cp |
Copy a managed file (markdown copied body-only — no comment blocks in the duplicate) |
mv |
Move a managed .md file (preserves comments and frontmatter) |
rm |
Remove a managed .md file |
| Command | Description |
|---|---|
query |
Search across documents for comments (filter by --pending, --pending-for, --pending-for-me, --pending-broadcast, --author, --since, --comment-id, --kind; --expanded for inline comment details; --json --compact for a minified columnar payload, --include-integrity to add checksum/signature columns — see Compact output) |
search |
Full-text search across documents (supports --regex, --scope, --context, --ignore-case, and stateless --limit/--offset pagination with an exact total; --json --compact for a minified grouped columnar payload — see Compact output) |
lint |
Run structural lint checks on a document |
verify |
Verify comment integrity (checksums and signatures) |
activity |
Show what changed since a cutoff (caller's last action by default) — see Tracking change |
| Command | Description |
|---|---|
sandbox add |
Stage a file under the caller's identity (soft claim) |
sandbox list |
List files currently sandboxed for the caller |
sandbox remove |
Unstage a file |
prompt set |
Define a folder-scoped system prompt in .remargin.yaml |
prompt resolve |
Resolve the nearest folder-scoped prompt for a file |
prompt list |
List all folder-scoped prompts in the realm |
| Command | Description |
|---|---|
session launch [<name>] |
Launch one Claude session per identity — every identity discovered under cwd, or the named sessions: manifest fleet — into a multiplexer, one tab each (--dry-run launch table, --print commands-only, --multiplexer herdr|tmux, --identity a,b). Gated behind the session build feature — see Session launch. |
remargin plan <op> <args>Returns a projection of any mutating op (ack, batch, comment, cp, delete, edit, migrate, purge, react, sandbox-add, sandbox-remove, sign, write, mv) without touching disk. Reports noop / would_commit / reject_reason / subset_gate / checksums / changed_line_ranges.
| Command | Description |
|---|---|
migrate |
Convert old-format inline comments to remargin format |
purge |
Strip all comments from a document |
keygen |
Generate a new Ed25519 signing key pair |
identity |
Resolve and print the configured identity (also identity show). |
identity create |
Print a ready-to-use identity YAML block to stdout for .remargin.yaml. |
version |
Print version information |
| Command | Description |
|---|---|
mcp install [--user] |
Register as an MCP server in Claude Code |
mcp uninstall |
Remove MCP server registration |
mcp test |
Check MCP registration status |
mcp run |
Start the MCP server (stdio transport) |
claude plugin install |
Register the marketplace and install the Claude Code plugin |
claude plugin uninstall |
Uninstall the Claude Code plugin |
claude plugin test |
Check plugin installation status |
doctor |
Check realm health: hooks wired, config schema across the realm tree, identity/key resolvability, trusted-root existence, sandbox staging hygiene, project-scope settings. --check=<set> runs a named subset of checks |
claude restrict |
Add permission rules (sync to .claude/settings.local.json and ~/.claude/settings.json) |
claude unrestrict |
Reverse a previous restrict cleanly |
registry show |
Display the participant registry |
| Flag | Description |
|---|---|
--config <PATH> |
Path to config file |
--identity <NAME> |
Author name for this operation |
--type <human|agent> |
Author type |
--mode <open|registered|strict> |
Enforcement mode |
--key <PATH> |
Path to Ed25519 signing key |
--assets-dir <PATH> |
Assets directory path |
--json |
Output as JSON |
--compact |
Compact columnar JSON, minified. Requires --json; supported by get, query, activity, and search today (see Compact output) |
--verbose |
Enable tracing output |
To preview a mutating op without writing, use
remargin plan <op>. The per-op--dry-runflag was removed in favour of the uniformplanprojection.
remargin get <file> --json --compact emits a token-lean, minified variant of the get payload. It is the shape the MCP get tool returns unconditionally (the MCP surface has no format flag). Plain --json is unchanged.
- With
--line-numbers:{start_line, lines, links_cols, links}.linesis an array of bare strings; linei's number isstart_line + i(no per-line{line, text}objects). - Without
--line-numbers:{content, links_cols, links}—contentis the document text as one string. linksrows are positional arrays named bylinks_cols(["alias", "lines", "target", "title"]);alias/titlearenullwhen absent. The verbosecount(alwayslines.len()) andpathcolumns are dropped. A link's on-disk path is derivable fromtarget: verbatim when it carries a file extension, elsetarget + ".md".
remargin query ... --json --compact emits the same token-lean, minified variant of the query payload the MCP query tool returns unconditionally. Plain --json is unchanged (verbose ExpandedComment objects).
- Shape:
{base_path, comment_cols, results}, where each result is{path, comment_count, pending_count, pending_for, last_activity, comments}. commentsrows are positional arrays named once by the envelope'scomment_colsheader:["id", "line", "author", "author_type", "ts", "reply_to", "thread", "to", "ack", "reactions", "remargin_kind", "edited_at", "attachments", "content"](contentlast). Acks compact toauthor@tsstrings; the verbose per-commentchecksum/signatureand the redundantfileare dropped. Nullable columns (reply_to,thread,remargin_kind,edited_at) arenullwhen absent.--include-integrity(requires--compact) re-addschecksum,signatureas columns immediately beforecontent, widening bothcomment_colsand every row. On the MCP surface this is theinclude_integrity: trueboolean.
remargin activity --json --compact emits the same token-lean, minified variant of the activity payload the MCP activity tool returns unconditionally. Plain --json is unchanged (verbose tagged Change objects); --pretty is unaffected.
- Shape:
{cutoff_explicit, newest_ts_overall, change_cols, files}, where each file is{path, newest_ts, cutoff_applied?, changes}. changesrows are positional arrays named once by the envelope'schange_colsheader:["ts", "kind", "author", "author_type", "comment_id", "line_start", "line_end", "reply_to", "to"]. One uniform 9-column shape serves all three kinds;kindisack/comment/sandbox. Columns a kind lacks arenull: acks / sandboxes null the comment-only columns (line_start,line_end,reply_to) and theirto; sandboxes also nullcomment_id.tois[]for a broadcast comment (vsnullfor the not-applicable acks / sandboxes). Timestamps keep full fidelity.
remargin search <pattern> --json --compact emits the same token-lean, minified variant of the search payload the MCP search tool returns unconditionally. Plain --json is unchanged (flat SearchMatch objects with PascalCase location).
- Shape:
{total, match_cols, files}(pluseffective_limitwhen a page was clamped), where each file is{path, matches}. - Matches are grouped by file so
pathis stated once; files appear in first-match order and a file's rows are contiguous. Each match is a positional row named once by the envelope'smatch_colsheader:["line", "location", "text", "comment_id"].locationis lowercasebody/comment;comment_idisnullfor body matches. - With
--context/-C> 0 the rows widen to["line", "location", "text", "comment_id", "before", "after"](before/afterare string arrays) andmatch_colsreflects the widened arity. totalis the exact corpus match count. On the MCP surface a page auto-sized under the session spill cap carrieseffective_limit; page by advancing theoffsetrequest param.
The remargin activity command answers "what's new since X?" across managed .md files in the current realm. Per-file change records (comments, acks, sandbox-adds) are returned sorted by timestamp:
# What's new since I last acted (per-file caller-last-action cutoff).
remargin activity
# Explicit cutoff.
remargin activity --since 2026-04-20T00:00:00Z
# Human-readable timeline.
remargin activity --prettyThe default JSON output is the structured ActivityResult shape; --json --compact emits the token-lean columnar payload instead (see Compact output). --pretty switches to a per-file timeline rendered to stderr (so stdout stays clean for piping). Each per-file block opens with a header line that names the cutoff that was applied — (since 2026-04-20 00:00) for explicit --since, (since you last touched this file: …) for the caller-last-action default, and (since the beginning — no prior activity by you in this file) for the initial-touch fallback.
When --since is omitted, the per-file cutoff is the latest of (caller's authored comments, caller's acks, caller's sandbox-adds) in that file — files where the caller has never acted return everything (the "initial-touch" fallback). The command also folds in comment edits (via the Comment.edited_at field) and sandbox-roster timestamp refreshes, neither of which comments / query surface as distinct events.
Same surface is exposed via MCP as mcp__remargin__activity, which always returns the compact columnar payload (the MCP surface has no format flag).
# Three agents working on a shared spec, each with its own identity / key
# (configured in their respective .remargin.yaml or via --config).
# Each agent reads its inbox at the start of a turn
remargin query . --pending-for-me --pretty
# Posts work as comments addressed to the relevant peer
remargin comment spec.md "Choosing 'name' for the column key." \
--reply-to pl1 --to engineer --auto-ack
# Or in one atomic batch
remargin batch spec.md --ops '[
{"content": "Choosing name.", "reply_to": "pl1", "auto_ack": true},
{"content": "Tagging for QA review.", "after_comment": "pl1", "to": ["qa"]}
]'
# At any point, see the full conversation tree
remargin comments spec.md --pretty
# Or what's changed since this agent last acted, across every file
remargin activity --pretty# Find documents with pending comments directed at you
remargin query . --pending-for-me
# Include broadcast conversations you haven't closed
remargin query . --pending-for-me --pending-broadcast
# Read the document
remargin get docs/proposal.md
# See the discussion
remargin comments docs/proposal.md --pretty
# Add your review comments
remargin comment docs/proposal.md "Needs error handling." --after-line 42
# Acknowledge comments addressed to you
remargin ack --file docs/proposal.md abc def
# Or ack by ID without specifying the file
remargin ack abc def
# When review is complete, produce a clean version
remargin purge docs/proposal.md# Add multiple comments in one atomic operation
remargin batch docs/design.md --ops '[
{"content": "Good approach here.", "after_line": 10},
{"content": "Edge case: what if input is empty?", "after_line": 35},
{"content": "This contradicts section 2.", "after_line": 78}
]'If you have documents using the older user comments / agent comments fenced block format:
# Preview what would change
remargin plan migrate docs/old-doc.md
# Convert
remargin migrate docs/old-doc.md| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Lint failure |
| 3 | Integrity failure |
| 4 | Missing attachment |
| 5 | Comment preservation violation |
| 6 | Skill error |
| 7 | Comment not found (folder-wide ack) |
| 8 | Ambiguous comment ID (found in multiple files) |
# Debug build
cargo build
# Release build (with LTO)
cargo build --release
# Run tests
cargo test
# Run clippy (strict — the project enforces deny-all clippy lints)
cargo clippyContributions are welcome.
- Fork and branch — create a feature branch from
master. - Keep changes focused — one feature or fix per PR.
- Follow existing patterns — the codebase uses strict clippy lints (all, pedantic, restriction, nursery levels). Run
cargo clippybefore submitting. - Write tests — the project uses
assert_cmdandtempfilefor integration tests. - Update the skill — if you add or change MCP tools, update
crates/remargin-core/skill/SKILL.mdaccordingly. - Commit messages — conventional commits (
feat:,fix:,chore:, etc.).
git clone https://github.com/tixena/remargin.git
cd remargin
cargo build
cargo testThe project uses Rust 2024 edition with strict clippy enforcement. If clippy complains, fix the lint — don't suppress it unless there's a documented reason in Cargo.toml.
Remargin is open source under the MIT License.
Made by Tixena Labs.