<!-- https://zunder-design-preview.pages.dev/docs/reference/cli · Markdown version of the page -->

# CLI

Every zunder-guard command and flag, with its environment variable, as the code defines them.

{/* Source of truth: crates/zunder-guard/src/main.rs (subcommands, flags, environment variables) and
    crates/zunder-guard-mcp/src/cli.rs (the flags of `mcp`). Behaviour: docs/guard.md, "The command line".
    Checked against the code on 6 Oct 2026. Change this page in the same commit as the CLI. */}

:::note[Planned]
Guard has no release yet. The commands below exist in Guard's source and run on Hyperliquid testnet in our hands; they are not published as a download. Names can still change before the first release.
:::

Every refusal exits with status 2 and its reason on standard error.

## Global options

| Option | Environment | Default | Meaning |
|---|---|---|---|
| `--home DIR` | `ZUNDER_GUARD_HOME` | `~/.zunder-guard` | Guard's home: the config, the journals, the key file |
| `--config FILE` | | `guard.toml` in the home | another config file |
| `--version`, `--help` | | | the version; the help text |

## Commands

| Command | What it does | Who runs it |
|---|---|---|
| `zunder-guard init [--interactive \| --non-interactive] [--rules zr1_…] [--account 0x…] [--network paper\|testnet\|mainnet] [--account-network testnet\|mainnet] [--confirm-mainnet 0x…] [--equity-cap USDC] [--key-stdin \| --no-key] [--listen L] [--ip-share S] [--key-store auto\|file\|systemd-creds] [--force] [--client-key-out FILE] [--refuse-mainnet REASON]` | the setup: the rules, the account, the mode, the API wallet key (checked with Hyperliquid and stored), a client key for your bot (shown once), a pairing code, the risk journal for paper or testnet. Interactive on a terminal (the default); `--non-interactive` takes every answer from the flags and the key from standard input (`--key-stdin`) | a person, or an installer |
| `zunder-guard config get network\|mode\|account\|listen\|rules` | prints one configured value; `network` and `mode` both print the mode (paper, testnet or mainnet), `rules` prints your rules as a `zr1_` code | anyone on the machine |
| `zunder-guard key check --key-stdin` | reads the key from standard input, checks with Hyperliquid that it is an API wallet of the configured account, prints its address (never the key), and records it as `api_wallet` when the config names none | a person, or the installer |
| `zunder-guard pair` | a new client key for a bot, shown once, and a new pairing code; a running Guard accepts it after a restart | a person |
| `zunder-guard client add --out FILE` | a new client key written to a new file only its owner can read (mode 0600), never shown; for a bot or an [MCP](https://zunder-design-preview.pages.dev/docs/integrations/mcp) agent that reads its key from a file. A running Guard accepts it after a restart | a person |
| `zunder-guard run [--network paper\|testnet\|mainnet] [--listen L] [--ip-share S] [--key-stdin \| --key-file FILE] [--container] [--rules zr1_…] [--account 0x…]` | runs Guard until stopped (SIGTERM or Ctrl-C) | a person or a service |
| `zunder-guard health [--listen L \| --url http://host:port]` | exits 0 when Guard's `GET /healthz` answers 200 | a health check |
| `zunder-guard journal-init --mode paper\|testnet\|mainnet --note "…"` | starts a risk journal at the account's equity now. Never replaces one | a person |
| `zunder-guard journal-resume --mode paper\|testnet\|mainnet --note "…"` | clears a drawdown halt after a person's review; the note goes into the journal. Stop Guard first | a person |
| `zunder-guard journal-show --mode paper\|testnet\|mainnet` | prints the risk journal's records, one JSON line each. Reads only | anyone on the machine |
| `zunder-guard kill --reason "…"` | pulls the [kill switch](https://zunder-design-preview.pages.dev/docs/concepts/kill-switch): writes the kill file; a running Guard opens nothing and flattens | anyone on the machine |
| `zunder-guard check-config` | validates the config and prints its rules; reads no key, connects to nothing | anyone |
| `zunder-guard mcp [--network paper\|testnet\|mainnet] [--key-file FILE \| --key-stdin] [--guard-url URL] [--confirm-account 0x…] [--kill-file FILE]` | the [MCP server](https://zunder-design-preview.pages.dev/docs/integrations/mcp) for AI agents over standard input and output, trading only through a Guard on this machine | an MCP client |

`journal-init`, `journal-resume` and `journal-show` need `--mode`; `journal-init` and `journal-resume` also need `--note`. A mainnet `journal-init` or `journal-resume` needs `ZUNDER_MAINNET_CONFIRM` too.

## `run`, in detail

- **The network.** Without `--network` (or `ZUNDER_GUARD_NETWORK`), Guard runs paper mode, and only a paper config starts that way: a testnet or mainnet config refuses rather than quietly running paper. With it, the config's mode must be the same. So a testnet config starts with `zunder-guard run --network testnet`, or with `ZUNDER_GUARD_NETWORK=testnet` set.
- **The key.** `--key-stdin`, or (testnet only) `--key-file FILE` (`ZUNDER_GUARD_KEY_FILE`), or else the file `api-wallet-key` in the home. A key file must be readable by its owner only (mode 0600); in a container (`--container`, `ZUNDER_GUARD_CONTAINER`, or Docker's `/.dockerenv`) a readable file is accepted with a warning, as long as nobody else can write it. Paper mode reads no key.
- **Mainnet** needs all of this at once: a mainnet config with every guard (`allow_mainnet = true`, the equity cap, the account, the API wallet, no rule looser than the defaults), `ZUNDER_MAINNET_CONFIRM` set to the account's address at this start, a risk journal started for mainnet and this account, and the key on standard input only (`--key-stdin`). Everything but Hyperliquid's own checks is checked before the key is read. See [Paper, testnet and mainnet](https://zunder-design-preview.pages.dev/docs/concepts/networks#mainnet).
- **An empty home** with `--rules` (`ZUNDER_GUARD_RULES`) and `--account` (`ZUNDER_GUARD_ACCOUNT`) is set up for paper mode first, as `init --non-interactive` would. Testnet and mainnet need `init`.
- **The listen address** comes from the config (`127.0.0.1:8547` by default); `--listen` (`ZUNDER_GUARD_LISTEN`) overrides it at start, with a warning for anything but loopback.
- **The IP share** comes from the config's [`ip_share`](https://zunder-design-preview.pages.dev/docs/reference/config#top-level-keys) (`1` by default); `--ip-share` (`ZUNDER_GUARD_IP_SHARE`) overrides it at start. Several Guards on one machine: `1/N` each. A share too small to keep Guard safe is refused.

## Environment variables

| Variable | Read by | Same as |
|---|---|---|
| `ZUNDER_GUARD_HOME` | every command | `--home` |
| `ZUNDER_GUARD_RULES` | `init`, `run` | `--rules` |
| `ZUNDER_GUARD_ACCOUNT` | `init`, `run` | `--account` |
| `ZUNDER_GUARD_NETWORK` | `init`, `run` | `--network` (the mode) |
| `ZUNDER_GUARD_LISTEN` | `run`, `health` | `--listen` |
| `ZUNDER_GUARD_IP_SHARE` | `init`, `run` | `--ip-share` |
| `ZUNDER_GUARD_KEY_FILE` | `run` | `--key-file` |
| `ZUNDER_GUARD_CONTAINER` | `run` | `--container` |
| `ZUNDER_MAINNET_CONFIRM` | `run`, `journal-init`, `journal-resume` on mainnet | no flag: the person's confirmation, the account's address, at every mainnet start (the same variable as Zunder's own runner) |

The other flags have no environment variable. The `mcp` command reads no environment variable at all: its client key comes from `--key-file` or `--key-stdin` only.

## Guard's status

There is no `status` command. A running Guard answers its status on its own address, to anyone on the machine:

```sh
curl -s http://127.0.0.1:8547/guard/status
```

The answer is JSON: the mode, the account, `killed` (the reason, or null), the risk state (`active`, `halted_for_day` or `stopped`), equity, positions, open orders, alerts, the kill file's path, your rules as a `zr1_` code and the last event. Guard's decisions, newest last: `curl -s 'http://127.0.0.1:8547/guard/events?since=0'`. Both are listed in [Events](https://zunder-design-preview.pages.dev/docs/reference/events).

## Commands only a person runs

`init`, `pair`, `client add`, `journal-init` and `journal-resume` change what Guard may do. Guard never runs them itself, and neither the MCP server nor any web page can. `journal-resume` also needs Guard stopped: it opens the risk journal, which a running Guard holds locked.

## Example

```sh
zunder-guard kill --reason "bot looping on SOL"
curl -s http://127.0.0.1:8547/guard/status
# {"schema":1, … "killed":"bot looping on SOL", … "kill_file":"/home/you/.zunder-guard/kill", …}
```

The kill switch stays pulled until a person removes the kill file (its path is `kill_file` in the status) and restarts Guard.

## Not in Guard

These names appeared in earlier drafts of these docs. They do not exist; use the command on the right.

| Not a command | Use instead |
|---|---|
| `zunder-guard status` | `curl -s http://127.0.0.1:8547/guard/status` |
| `zunder-guard resume` | `zunder-guard journal-resume --mode M --note "…"` |
| `zunder-guard journal show` | `zunder-guard journal-show --mode M` |
| `zunder-guard rules export` | `zunder-guard config get rules` |
| `zunder-guard-mcp` (a separate program) | `zunder-guard mcp`; releases ship one program |

:::note[Planned]
Not in the first release: listing and revoking client keys (`client list`, `client revoke`), and repairing a journal whose last line was cut off. Until then, a client key is revoked by removing its address from `[auth] clients` in `guard.toml` and restarting Guard, and a damaged journal stays refused until a person looks at it.
:::
