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 β€” 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:

  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

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:.