CLI

CLI Getting Started

The snug command mirrors the HTTP API service for service. It is built from the same crate as the server, so a build only exposes the services its features enable - snug help shows exactly what your binary includes.

cargo run --bin snug --features full -- help
# or build once and put it on PATH
cargo build --release --bin snug --features full

Discover commands instead of memorizing them:

snug help                        # all services
snug kv help                     # commands for one service
snug kv set --help               # flags and curated examples

Authentication

# Sign up (or log in) - stores credentials in ~/.snug/credentials.json
# and refreshes the access token automatically
snug auth signup --email you@example.com --password '...'

# Or mint a token locally against your own server's keypair
export SNUG_API_TOKEN=$(snug jwt generate user -u dev -t <tenant> --format compact)

# Verify connectivity
snug health api

Token resolution order: --token flag, then SNUG_API_TOKEN, then stored credentials. health, metrics, the jwt subcommands, and the public auth flows work without a token; everything else requires one. See Authentication for tenants and scopes.

Global flags

FlagDefaultDescription
-u, --api-url$SNUG_API_URL or http://localhost:4000API base URL
-t, --token$SNUG_API_TOKENJWT or snug_key_... API key
--outputtabletable, json, or compact
-q, --quietoffSuppress non-error output
-v, --verboseoffRequest/response details, on stderr
--timeout30Request timeout in seconds

Global flags go on snug, not the subcommand: snug --output json kv get mykey - putting --output after the subcommand fails with "unexpected argument". (Inside jwt generate, -u/-t mean subject/tenant; the global meanings apply everywhere else.)

The output contract

Learn this once and every service scripts the same way. Each shape below was captured from a real run.

Streams. Structured payloads go to stdout; progress, warnings, errors, and --verbose traces go to stderr, so jq pipelines stay clean. When stdout is piped, the CLI also suppresses its cosmetic blank-line padding - captured output is exactly the payload.

Exit codes. 0 on success, non-zero on any failure, independent of output format. Branch on the exit code, never by grepping output.

JSON mode prints the bare payload - the server's envelope is unwrapped, so address fields directly (jq '.key', not jq '.data.key'):

snug --output json kv get docs/hello
{
  "key": "docs/hello",
  "value": { "greeting": "hello world" },
  "content_type": "application/json",
  "updated_at": "2026-08-28T03:41:02.485104Z"
}

Failures still print parseable JSON on stdout (plus a human message on stderr, plus a non-zero exit):

snug --output json kv get docs/missing; echo "exit: $?"
{
  "error": {
    "status": 404,
    "body": {
      "status": 404,
      "msg": "Key not found: docs/missing",
      "error": "key_not_found"
    }
  }
}
exit: 1

A failure before any request is made emits {"error": {"message": "..."}} instead.

Destructive operations need --force in scripts

Deletes prompt for confirmation on a TTY. In non-interactive mode (--output json or --quiet) they refuse to prompt and exit non-zero:

{
  "error": "confirmation_required",
  "resource": "key",
  "name": "docs/hello",
  "hint": "pass --force to confirm destructive delete in non-interactive mode"
}

Always pass --force explicitly in scripts. Note that REST deletes are idempotent - deleting something already gone succeeds - so gate on a head/get first when "must have existed" matters.

Scripting patterns

# Existence-gated work: head exits non-zero on a missing key
if snug kv head cache/token/xyz >/dev/null 2>&1; then
  snug kv ttl cache/token/xyz --extend 3600
fi

# JSON pipeline - no .data wrapper
snug --output json kv list --tags ui | jq -r '.keys[].key'

# Multi-identity testing: --token overrides for one command only
TOKEN_B=$(snug jwt generate user -u bob -t <tenant> --format compact)
snug --token "$TOKEN_B" kv set bob/data '{"x":2}'

One cross-cutting gotcha: when a command takes an optional --id, it defaults to the token's sub claim - convenient, but surprising when switching tokens mid-script.

On this page