Warden CLI Reference

Every subcommand, flag, and exit behavior below matches the CLI source (cmd/warden). With no subcommand, warden prints usage and exits 1.

CLI Experience

Warden has a consistent, security-focused terminal identity. Colors and banners are semantic and degrade gracefully.

  • Banner: A large WARDEN ASCII banner is shown for warden (no args), warden init, and warden doctor when stderr is a TTY. It is never shown for warden --version, warden --help, or warden run so that scripted and sandboxed invocations stay clean. On narrow terminals (COLUMNS<60) the banner falls back to a compact header; on CI or when output is piped it is omitted entirely.

  • Header: warden doctor and warden init show a compact header (WARDEN + MCP Server Sandbox Runtime + Version …) when the large banner is not used.

  • Colors: Green marks success/allowed/sandbox-active, red marks blocked/denied/errors, cyan is used for structural headings/paths/metadata, and normal foreground is used for body text. Colors are semantic and never the sole signalevery state also has a text marker (✓/✗ + ALLOWED/ BLOCKED where applicable) for accessibility.

  • Color control: Honors NO_COLOR=1 (https://no-color.org), FORCE_COLOR/CLICOLOR_FORCE, and TERM=dumb. Set NO_COLOR=1 or WARDEN_NO_COLOR=1 to disable all ANSI. Use WARDEN_NO_UNICODE=1 to force ASCII fallbacks (OK/x instead of ✓/✗).

  • Non-TTY / CI: When CI is set or stderr is not a TTY, Warden avoids animations and uses deterministic bracketed progress ([1/4] … OK) so logs stay clean. Spinners are short, never leave stray characters, and are disabled in CI. Set WARDEN_NO_SPINNER=1 to force static output.

  • First-run: The first time Warden is invoked interactively it shows a one-time welcome (Welcome to Warden + capabilities + warden init/run/ doctor hints) and then never again. The marker lives at ${XDG_STATE_HOME:-~/.local/state}/warden/welcomed (0600). It is suppressed in CI, when WARDEN_NO_FIRST_RUN=1 is set, or when neither stdout nor stderr is a TTY. It never blocks warden run and never requires interactive input.

  • Installation: The published npm package ships only the bin/warden launcher (no install script), so npm install -g warden-sandbox-cli prints npm's standard output only. The platform binary is fetched on the first warden invocationthe launcher prints Downloading warden v<version>..., downloads the matching GitHub Release binary into ~/.cache/warden/<version>/, and execs it. Later runs reuse the cache. See Install for the expected output.

  • Fail-closed messaging: When a backend cannot be initialized, Warden prints ✗ Warden refused to start: sandbox backend unavailable with the specific reason and fails closed by designit will never run your MCP server without a working sandbox. This is not just UIit reflects the sandboxerr.RefuseToRun error that prevents any unsandboxed fallback.

warden run

Run a server under a policy. The sandbox backend is auto-detected (native first, Docker fallback, fail-closed when nothing is available).

warden run --policy <file> [--backend auto|linux|seatbelt|windows|docker]
           [--approve] [--approve-timeout <dur>] -- <command...>
FlagMeaning
--policy <file>Required. Policy YAML. Relative filesystem grants resolve against this file's directory.
--backend <name>Force a backend. Default auto. Aliases: bwrap → linux; macos, darwin, sandbox-exec → seatbelt; appcontainer, lowbox → windows. Unknown or unavailable backends fail closed.
--approveInteractive approval: prompt on first out-of-policy access instead of hard-failing. Network approvals apply live; filesystem approvals are saved to the policy and the server restarts. Requires a terminalfails closed without one.
--approve-timeout <dur>Per-prompt timeout (e.g. 30s, 2m). Zero waits indefinitely; expiry denies. Requires --approve.
-- <command...>Command to sandbox. Optional separatoreverything after the flags is the command anyway; -- lets the command itself start with a flag-like token.

Command resolution: the CLI tail wins, otherwise the policy's command: is used, otherwise the run fails with a usage error. Bare executable names (npx, uvx, node) are resolved via PATH lookup and fail closed when unresolvable; prefer absolute paths in checked-in policies. Exit code: the sandboxed process's exit code, 1 on runtime failure, 2 on usage/policy errors.

Run output: warden run never prints the large bannerit prioritizes runtime information. When stderr is a TTY (and CI is not set and WARDEN_QUIET is empty) it prints a compact pre-launch summary to stderr only (so MCP stdio is untouched):

WARDEN
──────────────────────────────────────
Policy     policy.yaml
Backend    linux
Command    /usr/bin/node server.js

Filesystem
  ✓ /workspace/project (read)
  ✗ everything else

Network
  ✓ api.github.com
  ✗ everything else

Environment
  ✓ GITHUB_TOKEN
  ✗ all unspecified variables

──────────────────────────────────────
✓ Sandbox active

Only information from the loaded policy is shownno permissions are invented. In CI / piped / WARDEN_QUIET=1 the summary is suppressed so warden run stays silent apart from the sandboxed process's own stdio. Security events are recorded to the audit log (warden logs) as structured allowed/blocked JSON; the CLI formats them as ✓ ALLOWED / ✗ BLOCKED when displayed (color reinforces but never replaces the text marker).

warden trace

Run unsandboxed but instrumented; record every file/network access to the trace log for init to consume:

warden trace -- <command...>

Prints the trace file path on stderr on exit. Exits with the command's exit code.

warden init

Generate a conservative starter policy from a trace log. Never overwrites an existing file:

warden init [--log <file>] [--output <file>] [-- <command...>]
FlagMeaning
--log <file>Trace log to read. Default: the latest trace.
--output, -o <file>Where to write. Default policy.yaml. Refuses to overwrite.
-- <command...>Recorded into the generated policy's command: field.

Only successful observed accesses become grants; blocked events never do; runtime paths (/usr, /proc, …) are omitted. Review and tighten the output before relying on it.

warden logs

Inspect the persistent JSONL audit log (${XDG_STATE_HOME:-~/.local/state}/warden/audit.jsonl):

warden logs [--log <file>] [--tail <n> | -n <n>] [--follow | -f]

--tail 0 prints nothing; --follow polls for appended events (survives truncation). Prints "no audit events recorded yet" when the log doesn't exist.

warden doctor

Check that the host can actually enforce a sandbox. It probes the real backends (bubblewrap/Seatbelt/AppContainer/Docker), strace availability on Linux, the egress proxy, and the policy engine, and prints a security-oriented report:

warden doctor

Example output (TTY):

WARDEN DOCTOR

Environment
────────────────────────────────
✓ Operating system        linux/amd64
✓ Sandbox backend         linux
✓ Namespace support       available (bwrap)
✓ Network proxy           available (egress allowlist)
✓ Policy engine           ready (YAML + validation)
✓ Fail-closed             enabled (never runs unsandboxed)

Security posture
────────────────────────────────
✓ Filesystem isolation    explicit paths only
✓ Environment filtering   explicit vars only
✓ Network policy          explicit hosts only
✓ Fail-closed behavior    enforced

Status: READY

When the backend is unavailable it prints Status: NOT READY in red, the underlying RefuseToRun reason, and Warden fails closed when sandboxing is unavailable. The exit code is machine-readable so scripts and CI can gate on it:

  • 0ready to run
  • 1NOT READY (sandbox backend or a required tool such as strace is missing)
  • 2usage error (unexpected arguments)

warden version

Print the stamped build version and exit 0. This stays script-friendly and never prints a banner:

warden version
warden --version

Single line warden version <semver> (or dev for unstamped builds). Release builds stamp the version at link time (the npm launcher downloads that exact version's binary); unstamped source builds report dev. Accepts version, --version, -version, and -v.

warden add

Connect one MCP server to one MCP client in a single command, sandboxed behind Warden:

warden add slack to claude-desktop
warden add notion to claude-desktop
warden add @scope/[email protected] to claude-code \
    --allow-host api.example.com --allow-env SCOPE_KEY
warden add --list-clients

The first argument is a catalog pack id (warden packs list) or any npm MCP package (name[@version]). add prepares the pinned npm release inside the sandbox (registry-only egress, install scripts disabled), generates a deny-by-default policy, and registers a wrapped launcher in the client's config after a preview + confirmation prompt.

FlagMeaning
to <client> / --client <id>Target client adapter: claude-desktop, claude-code, cursor, codex, vscode, gemini, cline, cascade, generic.
--name <name>Entry name in the client config (default: pack id / package name).
--scope project|userConfig scope (default project; claude-desktop defaults to user).
--config <file>Explicit configuration path.
--runtime <dir>Prepared runtime directory (default ~/.local/share/warden/runtimes/..., honors XDG_DATA_HOME).
--data <dir>Data directory for packs that need one.
--policy <file>Use your own reviewed policy; its command becomes the upstream.
--allow-host <host>Generic npm servers: grant network egress (repeatable).
--allow-env <name>Generic npm servers: forward an env var name (repeatable).
--allow-read <path> / --allow-write <path>Generic npm servers: grant a read/write path (repeatable).
--backend <name>Sandbox backend (default auto).
--yesApply without the confirmation prompt.
--dry-runPreview only; writes nothing, starts no process.
--list-clientsShow every detected client config and its current entries (read-only; cannot be combined with other arguments).

Behavior: catalog packs use their reviewed pack grants; any other npm package gets a deny-by-default starter policy (no network, no env, no paths beyond the runtime) extended only by the --allow-* flags. Credentials are reported by name only and never written into config files. Before writing, add probes the real sandbox with an inert Warden process and refuses to modify the config if the backend is unavailable. Undo anytime with warden unwrap --client <client> --server <entry>. See warden help add.

warden gateway

Wrap gateway-registered servers so the gateway launches each one sandboxed:

warden gateway init --config <file> --policies <dir>
# Per-server deny-by-default policies (<name>.yaml). Remote (SSE/HTTP)
# servers are skippedno local process to sandbox. Never overwrites.

warden gateway run --config <file> --policies <dir> --server <name> \
    [--backend ...] [--approve] [--approve-timeout <dur>] -- [extra args...]
# Run one registered stdio server sandboxed. Fails closed when the policy
# is missing or the server is remote. Bare launcher names resolve via PATH.

warden gateway wrap --config <file> --policies <dir> \
    [--warden-bin <path>] [--backend <name>] [--output <file>]
# Emit a gateway config whose stdio commands are prefixed with
# 'warden run --policy ...'. Point the gateway at the wrapped file.

warden gateway list --config <file> [--policies <dir>]
# Show registered servers: stdio vs remote, and policy presence.

Supported configs: Claude-style mcpServers JSON (Claude Desktop/Code, Cursor, VS Code servers, Docker MCP Gateway clients) and gateway YAML registries (upstreams: à la jonfairbanks/mcp-gateway, backends: à la MikkoParkkola/mcp-gateway).

Exit codes

CodeMeaning
0Success (for run: the sandboxed process exited 0)
otherFor run/trace: the sandboxed/traced process's own exit code
1Runtime failure (backend unavailable, enforcement error, approval restarts exhausted)
2Usage or policy error (bad flags, unreadable/invalid policy, no command)