CLI reference
gnarl <command> [subcommand] [flags]
The command is gnarl. The things with a user interface — the desktop app, the
web Console, and Gnarly Edge on phones — are Gnarly. Same node either way:
the executable's name decides which it presents as.
Global flags
| Flag | Default | Description |
|---|---|---|
--output <fmt> | text | text, json, or ndjson. Global on purpose: an agent driving this CLI asks for JSON once and every command answers the same way. |
-V, --version | Print the version. Each binary names itself. | |
-h, --help | Help for any command or subcommand. |
Commands that talk to a running node also take --port <PORT> (default 8080)
and --no-tls. There is no config file — everything else is a flag on start,
listed in the configuration reference.
Running a node
gnarl start [--port 8080] [--data-dir <path>] [--desktop] [--headless]
gnarl status # node ID, peers, claims
gnarl explain # ownership, readiness, routing policy
gnarl peers # known peers with health and RTT
gnarl stats # indexing, storage, proof, query counters
gnarl contribution # claims, bytes, queries served
gnarl inspect # explain + stats + peers in one view
gnarl reset # wipe the data directory and regenerate identity
start runs in the foreground; Ctrl+C stops it.
reset regenerates your identityA node's identity is its keypair. Resetting means peers that trusted the old node ID do not follow it to the new one.
Joining a mesh
A fresh node is private and announces nothing. Joining is deliberate, and it is how two machines share indexes and agent memory.
gnarl mesh setup --mesh-name my-mesh --with-invite
gnarl mesh invite --mesh-name my-mesh --admit-peer <their-node-id>
gnarl mesh join --invite 'lucenia1.…'
setup creates the profile and prints the gnarl start … line to run. invite
emits a token pinned to one node ID. join decodes a token and prints the exact
start command. Confirm with gnarl status — you want peers > 0.
--mesh-name separates traffic; it does not authenticate anyone. Pin node IDs
with --admit-peer.
Agent memory
gnarl memory remember "<fact>"
gnarl memory recall "<query>"
gnarl memory setup | invite | join # aliases of `gnarl mesh …`
The same tools are exposed over MCP at http://localhost:8080/mcp — see
Agent memory.
MCP clients
gnarl mcp install --client claude # or cursor, or codex
gnarl mcp install --client cursor --print # show the block, change nothing
Writes the MCP server entry into the client's configuration, backing the file up first and preserving every other key in it. Codex is always printed rather than written, because merging into a TOML file we do not own would reformat it.
| Flag | Meaning |
|---|---|
--client | claude, cursor, or codex |
--profile | tool surface to expose (default gnarl-memory) |
--port | node port (default 8080) |
--no-tls | the node is serving plain HTTP |
--print | print the block instead of editing the file |
The endpoint is always 127.0.0.1. /mcp answers the local machine only.
Indexes
gnarl index create <name> --field body:text --field tag:keyword
gnarl index list
gnarl index show <name> # declared fields
gnarl index count <name> # document count
gnarl index policy <name> # where this index's data may go
gnarl index delete <name>
--field is repeatable and takes NAME:TYPE. The CLI builds the request body
for you; a malformed pair is refused locally rather than by the node.
Placement
gnarl index policy notes # show
gnarl index policy notes --placement local # never leaves this machine
gnarl index policy crawl --placement public --replicas 6
| Placement | Who may hold, read, or learn of it |
|---|---|
local | nobody but this node |
mesh | peers of your own mesh — the default |
public | any peer, including strangers |
Only the node that created an index may change its placement; any other answers 403. Narrowing takes effect on the next anti-entropy cycle and drops replicas already held. Omitted flags are left alone, so a partial change cannot widen anything by accident.
See Data placement for what mesh means on a public
node — it is the setting that lets one node serve the commons while keeping its
memory private.
Documents
Documents are written over HTTP — there is no gnarl verb for a single
document yet:
curl -X POST http://localhost:8080/v1/indexes/notes/_doc \
-H 'content-type: application/json' \
-d '{"body": "decentralized search across distance"}'
Searching
gnarl search <query> [flags]
| Flag | Default | Description |
|---|---|---|
--index <name> | all | Restrict to one index |
--limit <n> | 10 | Hits to return |
--explain | off | Print the plan and timings |
--require-complete | off | Fail rather than return a partial answer |
gnarl search "glacier retreat" --index imagery --limit 25 --explain
--require-complete matters on a mesh: an index is spread across claims, and a
claim whose holder is asleep simply does not answer. Without it you get what was
available, with coverage reported.
Manifests, peers, identity
gnarl manifest list
gnarl manifest show <index>
gnarl peer list
gnarl peer show <node-id>
gnarl identity show
Backup and restore
gnarl repo add <name> --path <dir>
gnarl repo add <name> --endpoint <url> --bucket <b> --region <r> \
--access-key-id <id> --secret-access-key <secret> [--prefix <p>] [--virtual-host]
gnarl repo list
gnarl repo remove <name>
gnarl snapshot create <repo> <snapshot> [--index <name>] [--namespace <ns>]
gnarl snapshot list <repo>
gnarl snapshot show <repo> <snapshot> # includes whether the signature verifies
gnarl snapshot restore <repo> <snapshot>
gnarl snapshot delete <repo> <snapshot> # descriptor only; cleanup reclaims bytes
gnarl snapshot cleanup <repo> [--grace-seconds <n>]
gnarl snapshot jobs [--id <job-id>]
Long operations wait by default. --no-wait returns the job id instead.
Four flags are omitted above because they need their own explanation rather
than a line in a list — --encryption-key (unrecoverable if lost),
--allow-overwrite-live-index (destroys everything written since the
snapshot), --allow-unverified-signer (accepts a backup this node cannot
attribute to your fleet) and --allow-shared-pool (captures every tenant in a
pool). See Backup and restore.
Access control
gnarl rbac issue --user <name> [--role reader|writer|admin] [--ttl <seconds>]
| Flag | Default | Description |
|---|---|---|
--user <name> | required | Subject the token is minted for |
--role <role> | reader | reader, writer, or admin |
--ttl <seconds> | 86400 | Token lifetime |
--data-dir <path> | ~/.lucenia | Where the issuer key lives |
--issuer-key <path> | <data-dir>/rbac/issuer_ed25519.bin | Issuer key to sign with |
| RBAC is private-mesh only. A private mesh enables it by default; a public mesh | ||
| stays open by design. |
Demo
gnarl demo # golden scenario, end to end
Exit codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Failure |
2 | Bad usage — an unknown flag or malformed argument |
3 | Coverage was incomplete and --require-complete was set |
Codes 4 (verification failed) and 5 (refused by policy) are reserved and not
emitted yet: both conditions exit 1 today.