CLI reference
Every meshhold subcommand and flag.
The meshhold binary is both the node daemon and a thin client for its REST
surface. The same binary ships on every platform — Linux package, Windows
installer, macOS tarball — and every subcommand documented here is available
on all of them.
meshhold [--version] <command> [args] [flags]
A bare meshhold (or meshhold --help) prints the top-level subcommand
list. meshhold <command> --help always works and is the authoritative
source if anything below drifts from the binary.
The commands split into three families by how they reach the daemon:
| Family | Mode | Talks to | Daemon must be… |
|---|---|---|---|
| local | offline | nothing | irrelevant |
| direct | offline | BadgerDB (file lock) | stopped |
| API | online | REST /api/v1/… |
running |
The header of every subcommand section below states which family it belongs
to. The "direct" family inherits the same constraint already documented on
set-password — they take an exclusive lock on the
metadata store, so they only work when the node daemon isn't holding it.
Global flags
These apply to every API-using subcommand (the --api family — auth,
vault, file, node, profile, s3-key, s3-perm, forwards,
agent, status, replicate-now, settings, chat, vpn, audit,
logs, bug-report, send / recv / transfers, plus the online
networks invite / networks add / networks add-peer):
| Flag | Default | Notes |
|---|---|---|
--api |
http://127.0.0.1:8080 |
REST API base URL. Override when calling a remote daemon over a tunnel. |
--password |
$MESHHOLD_PASSWORD |
Web UI password used for the implicit login. Reading from env keeps it off argv. |
How a credential is chosen. When neither --password nor
$MESHHOLD_PASSWORD is set, the CLI uses a saved token from
auth login (~/.meshhold/cli.json), and only then falls back
to the daemon's local admin token (<metadata_dir>/cli-admin.token,
readable by root / the daemon user). Running auth login once is what
lets a non-root user drive these commands without read access to the
daemon's root-owned config.yaml and admin-token files.
The token "direct" subcommand takes --config / -c to locate
config.yaml instead of --api — it talks straight to BadgerDB and
never speaks HTTP.
Several commands are auto: they prefer the running daemon's REST
API and fall back to opening the metadata store directly only when the
daemon is stopped, so they work in both states without a flag. These
are set-password, 2fa, mgmt-keys (self keys), networks list /
networks set-swarm-key, and blocks gc. They accept --config / -c
for the offline fallback path. (telemetry is local: it only
rewrites config.yaml and never touches a running daemon.)
daemon
Family: local — this is the daemon.
Runs the node. Reads ~/.meshhold/config.yaml (Linux) or
%LOCALAPPDATA%\MeshHold\config.yaml (Windows) and brings up the libp2p
host, REST listener, replication scheduler, and any optional surfaces
(S3, push, port-forwards, agent).
| Flag | Default | Notes |
|---|---|---|
--config, -c |
OS default | Path to config.yaml. |
--log-level |
info |
debug / info / warn / error. |
--log-format |
console |
console for humans, json for log aggregators. |
meshhold daemon --log-level=debug --log-format=json
keygen
Family: local. No daemon, no config.
Generates a fresh 32-byte swarm key in the libp2p PSK
(/key/swarm/psk/1.0.0/) format. Every node that wants to join the same
network must share this key — treat it as sensitive.
| Flag | Default | Notes |
|---|---|---|
--output, -o |
stdout | Write to file instead. File is created mode 0600. |
meshhold keygen -o ~/.meshhold/swarm.key
keygen-reality
Family: local.
Generates the X25519 keypair the REALITY (TLS-masquerade) transport uses.
The private half is written to disk; the public half goes to stdout in
base64url form so you can paste it into the reality_pub field of an
invite URL.
| Flag | Default | Notes |
|---|---|---|
--output, -o |
~/.meshhold/reality.key |
Private-key path. Mode 0600. |
--force |
false |
Overwrite an existing private-key file. |
The command refuses to clobber an existing file unless --force is given —
losing the private key invalidates every peer that already has the public
half.
check-reality-dest
Family: local.
Probes a candidate REALITY destination (<host:port>) for the four
properties the transport relies on: TCP reachability, TLS 1.3 negotiation,
X25519 key share, and an ALPN response. Designed to run before you flip
node.obfs.reality.enabled = true.
meshhold check-reality-dest www.microsoft.com:443
Exits non-zero when the destination is not REALITY-compatible, so you can wire it into a first-run script.
| Flag | Default | Notes |
|---|---|---|
--timeout |
10s |
Combined TCP + TLS-handshake budget per probe. |
Sample output:
TCP www.microsoft.com:443 OK
TLS 1.3 OK
X25519 OK
ALPN "h2"
ServerCert CN=www.microsoft.com (issuer: CN=Microsoft RSA TLS CA 02)
set-password
Family: auto — talks to the daemon over loopback when one is running,
falls back to direct BadgerDB writes when it isn't. There is no --offline
flag; the CLI picks the right path automatically.
Sets the bcrypt hash the Web UI / REST API check against. Reads the new password from, in order:
$MESHHOLD_PASSWORDenv var- piped stdin (single line, non-interactive)
- interactive TTY prompt with confirmation
| Flag | Default | Notes |
|---|---|---|
--config, -c |
OS default | Path to config.yaml. |
MESHHOLD_PASSWORD='swordfish' meshhold set-password
auth
Family: API. Controls how the CLI authenticates to a local (or
tunnelled) daemon. Run auth login once and every other --api
subcommand authenticates with the saved token — no password on the
command line, and no read access to the daemon's root-owned
config.yaml / cli-admin.token required. This is the supported way to
drive the CLI as a non-root user.
The token is a long-lived bearer (mhk_…) held in BadgerDB on the
daemon and saved to ~/.meshhold/cli.json (mode 0600) on the client.
It survives daemon restarts and upgrades, is independent of the Web UI
password (changing the password does not revoke it — same model as
S3 tokens), and has no expiry: revoke it explicitly with auth logout
or auth revoke. The daemon stores only the token's SHA-256, so a
database leak yields no usable credential.
| Subcommand | What it does |
|---|---|
auth login |
Prompt for the password once, mint a token, save it to ~/.meshhold/cli.json. |
auth logout |
Revoke the saved token on the daemon and delete the local file. |
auth list |
List active CLI tokens; this device is marked with *. |
auth revoke <id> |
Revoke a token by id (a unique id prefix from auth list is enough). |
auth login reads the password from $MESHHOLD_PASSWORD, piped stdin,
or an interactive prompt. If 2FA is enabled, pass --totp <code> or
enter it when prompted.
| Flag | Default | Notes |
|---|---|---|
--api |
http://127.0.0.1:8080 |
Daemon the token is minted against and saved for. |
--label |
hostname | Free-form label shown by auth list. |
--totp |
— | TOTP code, when 2FA is enabled. |
# Log in once on a server, as an ordinary (non-root) user:
meshhold auth login
# From then on every --api command authenticates with the saved token,
# e.g. `meshhold node …`, `meshhold vault …`, `meshhold mgmt-keys …`.
# Revoke a lost device's token from any logged-in machine:
meshhold auth list
meshhold auth revoke fe774279
vault
Family: API.
meshhold vault list
meshhold vault get <vault_id>
meshhold vault keyid [<key>] # also: --key-file <path>
meshhold vault create
meshhold vault s3-alias <vault_id> <alias> # pass "" to clear
meshhold vault share <vault_id>
meshhold vault join <invite-or-key> # flags below
meshhold vault rename <vault_id> <new-name>
meshhold vault ignore <vault_id> # view; --set <glob> (repeat) / --clear
meshhold vault sync <vault_id> # --wait --node <id> / --replicas N
meshhold vault rm <vault_id> # --force to confirm
| Subcommand | Purpose |
|---|---|
list |
Table of vaults: ID, NAME, TRUSTED, RF, STORAGE_PATH. |
get |
Per-vault detail including replication factor, full-sync flag, storage path. |
keyid |
Derive the canonical 16-byte hex vault_id from a key (Argon2id + HKDF). Runs locally — the key never leaves the CLI process. |
create |
Mint a fresh random 32-byte key and print key + derived vault_id. |
s3-alias |
Set or clear the alias under which the vault is exposed by the S3 endpoint. Aliases follow DNS-bucket rules (3–63 chars, lowercase, etc.). |
share |
Print the meshhold://join invite another device scans / pastes to join this vault. Also covers chat rooms (rooms are vaults of type chat). Only works for vaults this node holds the key for. |
join |
Add a vault from an invite (or a bare key). When the invite carries a swarm key, the node joins that network first so replication can reach the vault's peers. |
rename |
Change a vault's display name. Runtime-added vaults only — config.yaml vaults are read-only via the API. |
ignore |
View (no flags) or replace this node's gitignore-style ignore globs: --set <glob> (repeat per pattern), --clear to remove all. Stacks on built-in defaults + a .holdignore file. Runtime-added vaults only. |
sync |
Flush local changes into the catalog and kick replication now. --wait blocks until copies land (--node <peer_id> = that backup holds every file; --replicas N = N+ holders; default = replication_factor), with --timeout/--interval. Exits non-zero on timeout — usable as a backup gate. |
rm |
Forget a runtime-added vault (its key, storage path, metadata). Gated behind --force. Blocks are reaped lazily by blocks gc; peers keep their copies. |
keyid accepts the key as a positional arg, --key-file <path> (use -
for stdin), or piped stdin.
join flags (used mainly on the bare-key path; invite values are filled
in automatically and overridden by any flag you pass):
| Flag | Default | Notes |
|---|---|---|
--name |
— | Local display name (overrides the invite's vault_name). |
--type |
from invite, else storage |
storage or chat. |
--storage-path |
empty | Back the vault with this folder. Empty = observer / streaming mode — blocks are decrypted on demand, never written to local disk. |
--rf |
0 |
Replication factor. 0 = daemon default (3 storage / 2 chat). |
# Onboard a vault key shared with you out-of-band:
meshhold vault keyid --key-file ./shared.key
# 5e3b...c4
# Generate one from scratch:
meshhold vault create
# key: 4c1e...f0
# vault_id: 9a44...20
# Share a vault, then join it from another headless node:
meshhold vault share 9a44...20
# meshhold://join?swarm_key=…&bootstrap=…&vault_key=…
meshhold vault join 'meshhold://join?swarm_key=…&vault_key=…' --storage-path /srv/vault
file
Family: API.
meshhold file ls <vault_id>
meshhold file get <vault_id> <path>
meshhold file upload <vault_id> <path> <local_file> # local_file '-' = stdin
meshhold file download <vault_id> <path> <local_file> # local_file '-' = stdout
meshhold file rm <vault_id> <path>
| Subcommand | Purpose |
|---|---|
ls |
Table of files in the vault — size, modified-at (RFC 3339), deleted flag, path. |
get |
File metadata: file_id, content + parent hash, modified-by, block count. |
upload |
Stream a local file (or stdin) into the vault. |
download |
Stream a vault file to a local file (or stdout). |
rm |
Soft-delete: marks the file as deleted; blocks linger until evicted. |
Both upload and download accept - as the local-file argument for
stdin / stdout. Combined, they pipe across vaults:
meshhold file download v1 docs/spec.md - \
| meshhold file upload v2 docs/spec.md -
node
Family: API.
meshhold node list
meshhold node self
meshhold node share [node_id]
meshhold node connect <invite> # --network <id>
| Subcommand | Purpose |
|---|---|
list |
Peer table — node ID, self/peer flag, reliable flag, last-seen, direct addrs. |
self |
Just the local node's libp2p peer ID (handy in scripts). |
share |
Print a node-share meshhold://join invite — this node's peer ID + reachable addresses, but no swarm key. With a node_id argument, builds a "connect through this node" invite for a remote peer the daemon currently knows addresses for. |
connect |
Graft a node-share invite's addresses onto a saved network (and dial the peer immediately when that network is active). Defaults to the active network; --network <id> targets a specific one. Convenience wrapper over networks add-peer. |
# On node A — share yourself:
meshhold node share
# meshhold://join?peer_id=12D3KooW…&host=…&plain_port=…
# On node B (already on the same network) — start talking to A directly:
meshhold node connect 'meshhold://join?peer_id=12D3KooW…&host=…&plain_port=…'
profile
Family: API.
Reads and edits this node's user profile — the values the daemon persists in its metadata store and advertises to peers over the hello / topology beat. There is no offline path: a profile change has to flow through the running daemon so peers pick it up, so the daemon must be running.
meshhold profile show [--json]
meshhold profile set-name <display-name>
meshhold profile set-country <code>
meshhold profile set-allow-calls <true|false>
| Subcommand | Purpose |
|---|---|
show |
Print the profile: node ID, display name, country code, and the allow_calls flag. --json for machine-readable output. |
set-name |
Set the human-readable name this node advertises to peers. Without it a headless node shows only its opaque peer ID in everyone's Network page. |
set-country |
Set the declared country (ISO 3166-1 alpha-2, e.g. RS). Pass "" to clear it and fall back to the auto-detected country. |
set-allow-calls |
Master switch for "this node accepts audio/video calls". When false, every peer's Network page hides the call buttons for this node — so a headless Linux/VPS host doesn't grow ringing-but-unanswerable buttons in everyone's UI. |
allow_calls defaults are platform-dependent until you set them
explicitly: true on Android / Windows / macOS (devices with a human at
the keyboard), false on Linux and other headless runtimes. That's why
calls to a Linux node are off by default — set-allow-calls true is the
way to turn them on from the command line (the Profile page in the Web UI
has the same toggle). An explicit value sticks across upgrades.
# Allow calls to this Linux node (e.g. a desktop with a mic + camera):
meshhold profile set-allow-calls true
# allow_calls set to true
meshhold profile show
# node_id: 12D3KooW…
# display_name: garage-pi
# country_code: RS
# allow_calls: true
Note that this flag only governs whether peers offer the call button and whether this node accepts incoming calls. Actually answering still needs working audio/video I/O — on a truly headless server with no mic, camera, or speakers the button will appear but there's nothing to answer with.
token
Family: direct (daemon must be stopped).
Legacy S3-token management — independent of the Web UI password. These
talk straight to BadgerDB. New deployments should prefer the online
s3-key / s3-perm commands.
meshhold token create --vaults <id,id,…> [--description <text>]
meshhold token list
meshhold token delete <key_id>
| Flag | Default | Notes |
|---|---|---|
--config, -c |
OS default | Config path. |
--vaults |
— | Comma-separated vault_ids the token grants access to. Required on create. |
--description |
empty | Optional human-readable label. |
The create subcommand prints the secret once — capture it then.
s3-key
Family: API.
meshhold s3-key add [--label <text>]
meshhold s3-key list
meshhold s3-key delete <access_key_id>
| Subcommand | Purpose |
|---|---|
add |
Mint a new (access_key_id, secret_access_key) pair. Secret is shown only once. --label accepts a free-form name like backup-laptop. |
list |
Table of keys — without secrets. |
delete |
Drop a key; cascades to its grants. |
s3-perm
Family: API.
meshhold s3-perm grant <access_key_id> <vault_id> <perms>
meshhold s3-perm revoke <access_key_id> <vault_id>
meshhold s3-perm list [--key <id>] [--vault <id>]
<perms> accepts read, write, read+write, plus the shorthands r,
w, rw, read,write. To remove a grant entirely use revoke, not
grant <... > none.
mgmt-keys
Family: auto — the self-key subcommands talk to the running daemon
over REST and fall back to the metadata store when it's stopped (the
peer subcommands are REST-only — see below).
Manages this node's self management keys — named credentials another
device presents when it wants to invoke a capability on this node
(currently tunnel and camera). See the
mesh-VPN scenario for how these get consumed.
meshhold mgmt-keys list [--json]
meshhold mgmt-keys add --name <text> [--caps <list>] [--expires-in <dur> | --never-expires]
[--allow <proto:port> ...] [--key <secret>]
meshhold mgmt-keys show <id> [--json]
meshhold mgmt-keys share <id> [--node-id <peer>]
meshhold mgmt-keys rm <id>
share prints the unified meshhold://join?mgmt_key=… invite (the form
another device's scanner recognises as a management key). Because the
command runs offline it can't discover this node's own peer ID — pass
--node-id <peer> to embed it so the receiver can auto-bind the key to the
right node. Without it the invite still works; the receiver picks the node
manually.
| Flag | Default | Notes |
|---|---|---|
--config, -c |
OS default | Config path. |
--name |
— | Required on add. Display label. |
--caps |
tunnel,camera |
Comma-separated capability list. Known values: tunnel, camera, meshlan (the legacy slug mesh-route is still accepted). meshlan is mutually exclusive with tunnel/camera. |
--expires-in |
30d |
Validity period from now. Accepts Go duration syntax plus the d suffix (30d, 12h30m). |
--never-expires |
false |
Mint a non-expiring key. |
--allow |
— | Open a local port to holders of the key: proto:port or proto:lo-hi (tcp:22, tcp:8000-8100). Repeatable. Used by meshlan keys for the per-node port ACL (default-deny). |
--key |
— | Import an existing 32-byte secret (base64/hex) instead of generating one — how a node joins a shared meshlan network key. |
--json |
false |
Machine-readable output on list / show. |
<id> accepts any unambiguous prefix of the full key ID — paste the
short 8-char form list prints.
meshhold mgmt-keys add --name="My phone" --caps=tunnel,camera --expires-in=30d
meshhold mgmt-keys add --name="Friend exit" --caps=tunnel --never-expires
# Stand up a mesh-LAN network (see the Flat Mesh LAN scenario):
meshhold mgmt-keys add --name="Home net" --caps=meshlan --never-expires \
--allow tcp:22 --allow tcp:445
# …then JOIN it on another node with the printed secret:
meshhold mgmt-keys add --name="Home net" --caps=meshlan --never-expires \
--key <secret>
mgmt-keys peer
Family: API (daemon must be running).
The subcommands above manage self keys — the credentials this node
hands out. mgmt-keys peer manages the mirror image: the credentials
other nodes have granted us, which forwards add --peer-key
and vpn up --key reference by ID. Storing one from the CLI is what
makes a fully headless port-forward / VPN setup possible.
meshhold mgmt-keys peer list [--json]
meshhold mgmt-keys peer add <invite-or-key> --name <text> [--node-id <peer>]
meshhold mgmt-keys peer rename <id> <new-name>
meshhold mgmt-keys peer rm <id>
add accepts either the full meshhold://join?mgmt_key=… invite the
other node's mgmt-keys share (or its Web UI) produced — it parses the
issuing node_id and any password-gate parameters out of the URL — or
a bare key, in which case pass --node-id to bind it to the issuer.
<id> accepts any unambiguous prefix of the ID shown by peer list.
# On node A (the exit), mint and share a tunnel key:
meshhold mgmt-keys add --name="for my laptop" --caps=tunnel --never-expires
meshhold mgmt-keys share <id>
# meshhold://join?mgmt_key=…&node_id=12D3KooW…
# On node B (the client), store it, then use it in a forward:
meshhold mgmt-keys peer add 'meshhold://join?mgmt_key=…&node_id=12D3KooW…' --name "A exit"
meshhold mgmt-keys peer list # copy the short id
networks
Family: mixed. list / set-swarm-key are auto — they prefer
the running daemon's /api/v1/networks and fall back to opening
networks.json (and the metadata Badger) directly when it's stopped,
so they work in either state. invite / add / add-peer are API
(daemon must be running). Each per-command --help states which.
Manages the saved-networks roster the daemon persists at
<metadata_dir>/networks.json.
meshhold networks list [--json] # direct
meshhold networks set-swarm-key <id> (--key <psk> | --regenerate) # direct
meshhold networks invite <network_id> [--source own|upstream] # API
meshhold networks add <invite> # API
meshhold networks add-peer <network_id> <invite> # API
| Subcommand | Family | Purpose |
|---|---|---|
list |
direct | List saved networks; active network is marked with *. |
set-swarm-key |
direct | Rotate a network's PSK. Either supply the new key (--key) or have one generated (--regenerate, printed to stdout). After rotation, restart the daemon and re-pair every other node under the new key — gossip-driven rotation is intentionally not implemented. |
invite |
API | Print the shareable meshhold://join URL for a saved network (the swarm key is embedded, so it's rendered daemon-side). --source own advertises this node's own addresses; --source upstream (default) re-emits the bootstrap roster originally received. |
add |
API | Join the swarm carried by a meshhold://join invite; auto-connects when this node is currently offline. Accepts network, vault, and room invites (only the swarm portion is consumed — use vault join to also add the vault). |
add-peer |
API | Append a node-share invite's addresses to a saved network (see also node connect). |
forwards
Family: API.
Manages TCP/UDP port forwards that ride the libp2p tunnel mesh (inherits
multi-hop routing + libp2p Circuit Relay v2). Direction mirrors
ssh -L / ssh -R.
meshhold forwards list [--json]
meshhold forwards add --name <n> (--forward | --reverse) --proto <tcp|udp> \
--listen <addr> --remote <host:port> \
--peer-node <peer_id> --peer-key <key_id> \
[--id <stable-id>] [--no-autostart]
meshhold forwards rm <id>
meshhold forwards start <id>
meshhold forwards stop <id>
| Flag | Default | Notes |
|---|---|---|
--name |
— | Required on add. Human-readable label. |
--forward |
— | ssh -L style: listen locally, peer dials the remote. |
--reverse |
— | ssh -R style: peer binds the listener, we dial back here. |
--proto |
tcp |
tcp or udp. |
--listen |
— | Required. Bind address (e.g. :16261, 127.0.0.1:1194). |
--remote |
— | Required. Dial destination (host:port). |
--peer-node |
— | Required. Counter-party libp2p peer ID. |
--peer-key |
— | Required. Name of the peer mgmt key stored locally. |
--id |
pf-<name> |
Stable handle. Defaults to a slug of --name. |
--no-autostart |
false |
Register but don't start. |
Exactly one of --forward / --reverse must be set.
Two canonical shapes:
# Expose a local Project Zomboid server through a public VPS:
meshhold forwards add \
--name pz-via-vps --reverse --proto udp \
--peer-node 12D3KooW…VPS --peer-key vps-key-id \
--listen :16261 --remote 192.168.1.50:16261
# Connect through a domestic peer to a remote OpenVPN:
meshhold forwards add \
--name openvpn --forward --proto udp \
--peer-node 12D3KooW…HOME --peer-key home-key-id \
--listen 127.0.0.1:1194 --remote 10.0.0.5:1194
agent
Family: API.
Drives the local daemon's universal-AI-agent surface — list / create / delete agent instances, share them with other devices, manage Code-mode workspaces, and trigger the OAuth sign-in.
meshhold agent list [--json]
meshhold agent show <id> [--json]
meshhold agent create [--name <text>]
meshhold agent delete <id>
meshhold agent share <id>
meshhold agent join <invite>
meshhold agent reset-key <id> --force
meshhold agent login <id>
meshhold agent workspace add <instance-id> <path> [--name <text>]
meshhold agent workspace remove <instance-id> <path>
| Subcommand | Purpose |
|---|---|
list / show |
Inspect instances and their workspaces / auth status. |
create |
Mint a new instance on the local node. --name defaults to Claude N for the next free slot. |
delete |
Stop live sessions, drop the per-instance config dir. |
share |
Print the meshhold://join/… URL another device should scan or paste. |
join |
Add a remote agent instance from a meshhold://join invite produced by share on the host node. |
reset-key |
Rotate the access key and kick every device that joined with the old one. --force is mandatory — there's no interactive confirm. |
login |
Trigger claude auth login on the daemon host. Opens a browser tab on that machine; the command blocks until OAuth finishes (≤ 10 min). |
workspace add |
Register a directory as a Code-mode workspace. --name defaults to the basename of the path. |
workspace remove |
Unregister a workspace by path. |
status
Family: API.
One-shot health summary of the running daemon — the first command to
reach for on a headless box. Folds /system/status, /system/nat, and
/version into a single round-trip: version, node ID, network state,
enabled surfaces (S3 / media / enrich), TLS, at-rest encryption, public
IP, and listen addresses. --json for the raw objects.
meshhold status
# version: 0.7.208
# node_id: 12D3KooW…
# running: true
# network: connected (home)
# surfaces: s3=false media=true enrich=true
# tls: on (self-signed)
# at_rest: on (linux-secret-service)
replicate-now
Family: API.
Forces one immediate replication cycle instead of waiting for the periodic tick — handy right after joining a vault or adding a peer. The daemon pulls any missing blocks and re-announces what it holds. Requires the P2P stack to be up; an offline node returns an error.
meshhold replicate-now
# replication cycle complete
settings
Family: API.
Reads and edits the runtime toggles the Web UI's Features panel owns —
these live in the metadata store, not config.yaml, so this is the
only headless way to flip them. With no flags it prints the current
state; each flag actually passed is sent as a partial update (the rest
are left untouched).
meshhold settings # show
meshhold settings --enrich=true # start the metadata enricher
meshhold settings --node-reliable=false # mark this node unreliable
meshhold settings --tmdb-key=<key> # "" clears it
meshhold settings --blocks-quota=53687091200 # 50 GiB cap; 0 = uncapped
| Flag | Notes |
|---|---|
--node-reliable |
Whether this node is a reliable replication target. |
--auto-answer |
Auto-answer incoming calls (camera / mic pickup). |
--media |
Enable the media library + Player surface. |
--enrich |
Run the metadata-enricher workers (started/stopped live). |
--tmdb-key |
TMDB API key for video enrichment. "" clears it. |
--acoustid-key |
AcoustID API key for music enrichment. "" clears it. |
--blocks-quota |
Block-store cap in bytes. 0 = uncapped. |
--json |
Machine-readable output. |
chat
Family: API (daemon must be running and connected to a network).
Use chat rooms from the terminal. A room is addressed by its full id,
an unambiguous id prefix, or its display name. Rooms are vaults of type
chat — create / join one with vault join … --type chat and share
one with vault share.
meshhold chat list
meshhold chat read <room> [--limit <n>] [--mark-read]
meshhold chat send <room> <text>
read renders media / location messages as compact placeholders and
can --mark-read up to the newest line shown.
meshhold chat list
meshhold chat send "Family" "running late, back by 7"
meshhold chat read Family --mark-read
vpn
Family: API.
Route this node's traffic through a remote exit node from the command
line. The exit is authorised with a peer mgmt key this node holds (add
one with mgmt-keys peer add).
meshhold vpn up --exit <peer_id> --key <peer_key_id> [--name <label>]
meshhold vpn down
meshhold vpn status
status always works (reports not-running / not-supported cleanly);
down is idempotent. System-VPN needs a self-managed TUN, so up
works on Windows (via the helper service) and returns a clear error on
platforms where the fd has to arrive from a platform VpnService.
Exit routing — vpn routes, vpn config, vpn policy
The full split-tunnel / multi-exit model (see
Exit routing) is
scriptable headlessly. All three talk to the daemon and persist to the
vpn: config block.
Kept-local routes — destinations that stay off the exit (LAN, exceptions):
meshhold vpn routes list
meshhold vpn routes add <cidr> [--note <label>]
meshhold vpn routes rm <id>
list shows system (read-only), auto-detected host subnets, and custom
routes; config-managed rows are tagged [config] and can't be removed here.
Exit selection, fail behaviour, default route — config show prints the
current block; config set changes only the flags you pass:
meshhold vpn config show
meshhold vpn config set [--enabled] [--exit <peer_id>] [--key <peer_key_id>]
[--name <label>] [--fail-mode drop|direct]
[--default-route exit|direct|drop]
[--auto-local-subnets]
--fail-mode direct is fail-open (send tunneled traffic out the host when
the exit is down; Windows/Android, else drops). --default-route direct
makes a selective VPN (only policy destinations are tunnelled).
--enabled arms boot auto-start.
Exits (multi-exit policy) — route destination prefixes through exit nodes.
A 0.0.0.0/0 rule is a full tunnel. For a destination the daemon tries the
matching exits highest-priority first and fails over to the next live one
(prefix length only breaks ties at equal priority), so two 0.0.0.0/0 rules at
different priorities are a hot-standby pair:
meshhold vpn policy list
meshhold vpn policy add <cidr> [<cidr>…] --exit <peer_id> --key <peer_key_id> \
[--priority <n>] [--name <label>] [--note <text>]
meshhold vpn policy state <id> <permanent|temporary|off> [--priority <n>]
meshhold vpn policy rm <id>
Egress / download QoS — prioritise and rate-shape tunnel traffic so a bulk transfer can't starve interactive flows. Rates are in kilobits/s; priority only takes effect once a rate is set (the shaper must be the bottleneck, not your ISP). See QoS:
meshhold vpn qos show # policy + live per-class telemetry
meshhold vpn qos preset [name] # list built-in presets, or apply one
meshhold vpn qos set --enabled --uplink-kbits <n> --downlink-kbits <n>
meshhold vpn qos set --from-file <policy.json> # full classes + rules
meshlan
Family: API.
Inspect the mesh-LAN overlay — the routable virtual network where every member reaches another member's local services by a stable virtual IP.
meshhold meshlan status [--json]
Shows this node's virtual IPs and the peers reachable by IP, or reports
disabled / enabled but idle (no meshlan key, or the TUN couldn't
come up). Membership and which local ports are open are controlled by
Mesh LAN (meshlan) management keys, not this command — see
mgmt-keys.
audit
Family: API.
Reads the daemon's security / activity audit log — logins, vault unlocks, key changes, swarm-key rotations, peers appearing / leaving.
meshhold audit list [--limit <n>] [--severity info|warn|critical] [--json]
meshhold audit export [file] # full JSON-Lines dump ('-' or omit = stdout)
logs
Family: API.
A snapshot of the daemon's in-memory log ring (not a live tail). Since the CLI logs to stderr with no file to tail, this is how you see what a running daemon has been doing on a headless install.
meshhold logs # last 1000 lines
meshhold logs --level=warn # warnings and errors only
meshhold logs --limit=200
meshhold logs --export node.log # dump the whole ring to a file ('-' = stdout)
bug-report
Family: API.
Builds a diagnostics bundle (daemon status, network snapshot, redacted config, audit tail, log ring) and optionally uploads it — the headless counterpart of the Profile-page "Report a problem" button.
meshhold bug-report send [--description <text>] # build + upload in one shot
meshhold bug-report build [--description <text>] # build on disk only
meshhold bug-report list # bundles saved on the host
send needs the operator to have configured a submit URL
(node.diagnostics.submit_url); otherwise use build and hand the
file over out-of-band.
2fa
Family: auto — prefers the running daemon's admin-token endpoint (no restart needed) and falls back to BadgerDB when it's stopped.
Enable / disable / inspect the optional TOTP second factor for the Web UI login. Compatible with Google Authenticator, Aegis, FreeOTP, etc.
meshhold 2fa setup # print the enrolment QR + recovery codes
meshhold 2fa disable
meshhold 2fa status
send / recv / transfers
Family: API.
Direct node-to-node file transfer — files stream peer-to-peer over libp2p and are never stored in a vault or chat.
meshhold send <file> [<file>...] [--to <node_id>] # interactive picker if --to omitted
meshhold recv [--once] # accept inbound transfers from a TTY
meshhold transfers # list recent + in-flight transfers
meshhold receive-policy [ask|accept-trusted|accept-from|off] # show or set consent policy
meshhold incoming-folder [path] # show or set where received files land
send reads the file paths on the daemon's host (the common case: CLI
and daemon co-located). recv is the headless answer to "the receive
policy is ask but there's no Web UI open to click Accept".
blocks
Family: auto (REST when the daemon is running, direct BadgerDB walk when it's stopped).
Ciphertext-store maintenance.
meshhold blocks gc [--dry-run]
gc deletes orphan blocks (no catalog entry references them) and
redundant blocks (regenerable on demand from a file-mode source), then
reports the bytes freed. --dry-run reports without touching disk.
telemetry
Family: local — rewrites config.yaml only; takes effect at the
next daemon start.
Inspect or toggle the once-a-day anonymous usage beat.
meshhold telemetry status
meshhold telemetry disable
meshhold telemetry enable
See privacy and telemetry for the exact fields the beat carries.
Hidden subcommands
_mcp-approve is reserved for the daemon's own use — it spawns this
process under claude --permission-prompt-tool to bridge MCP approvals
back over loopback REST. The leading underscore is the convention for
internal commands hidden from --help; you should never need to invoke
it directly.
Environment variables
A small set of env vars are read by the CLI:
| Variable | Read by | Purpose |
|---|---|---|
MESHHOLD_PASSWORD |
set-password, auth login, every --api subcommand |
Web UI password; keeps the secret off argv in scripts. |
MESHHOLD_ACOUSTID_API_KEY |
daemon |
Fallback for node.enrich.music.acoustid_api_key. |
MESHHOLD_TMDB_API_KEY |
daemon |
Fallback for node.enrich.video.tmdb_api_key. |
MESHHOLD_DAEMON_URL |
_mcp-approve (internal) |
Loopback REST URL handed in by the daemon. |
MESHHOLD_BEARER_TOKEN |
_mcp-approve (internal) |
Loopback-scoped bearer. |
MESHHOLD_SESSION_ID |
_mcp-approve (internal) |
Owning agent session ID. |
Exit codes
All commands follow the standard cobra convention:
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Any error — invalid flags, missing arguments, daemon unreachable, REST 4xx/5xx, BadgerDB lock conflict, etc. The error message is written to stderr prefixed with error:. |