Reference

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:

  1. $MESHHOLD_PASSWORD env var
  2. piped stdin (single line, non-interactive)
  3. 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 routeconfig 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:.