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 β vault,
file, node, s3-key, s3-perm, forwards, agent):
| 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. |
The "direct" family (set-password, token, mgmt-keys, networks) takes
--config / -c to locate config.yaml instead β they don't speak HTTP.
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
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
| 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.). |
keyid accepts the key as a positional arg, --key-file <path> (use -
for stdin), or piped stdin.
# 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
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
| 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). |
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: direct (daemon must be stopped).
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]
meshhold mgmt-keys show <id> [--json]
meshhold mgmt-keys rm <id>
| 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. |
--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. |
--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
networks
Family: direct (daemon must be stopped).
Manages the saved-networks roster the daemon persists at
<metadata_dir>/networks.json.
meshhold networks list [--json]
meshhold networks set-swarm-key <id> (--key <psk> | --regenerate)
| Subcommand | Purpose |
|---|---|
list |
List saved networks; active network is marked with *. |
set-swarm-key |
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. |
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 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. |
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. |
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, 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:. |