Skip to content

Claude Gateway — CLI Reference ​

Auto-generated from the route manifest (scripts/gen-cli.ts). Do not edit by hand. See the HTTP API reference for integration protocols.

The claude-gateway binary accepts friendly <noun> <verb> subcommands.

Starting the gateway is explicit. claude-gateway gateway start runs the server in the foreground; every other invocation — no arguments, --help, or a typo — prints help or an error and exits, so exploring the CLI can never leave a stray server on the gateway port.

Upgrading from < 1.8: a service unit whose ExecStart runs the binary with no command still starts the server, with a deprecation warning. Change it to claude-gateway gateway start (or reinstall the unit with claude-gateway service install); a future release will drop the shim.

Global flags ​

FlagMeaning
--url <url>Gateway base URL (else $CLAUDE_GATEWAY_URL, else http://<bind>:<port> when a gateway is running on this host, else config.gateway.publicUrl)
--key <key>API key (else $CLAUDE_GATEWAY_API_KEY, else the first admin key in config)
--jsonPrint the raw JSON response only (stdout reserved for JSON)
--data <json>JSON object merged into the request body (write commands)
--helpShow help for the command

Which address the CLI uses. config.gateway.publicUrl describes this gateway as seen from outside, so on the gateway's own host both addresses are the same server — the public one just adds a reverse-proxy hop. Routing through it makes every command depend on that proxy, and a proxy that enforces its own authentication (which the CLI has no credentials for) answers 401 to commands that work over loopback. So when a gateway is running on this host (detected from the pidfile it writes on boot, a file read and a signal-0 — no subprocess), the local bind wins over publicUrl. --url and $CLAUDE_GATEWAY_URL still override, for deliberately exercising the proxy path or reaching another host.

The port comes from that same pidfile, whose second line is the port the gateway actually bound. $PORT describes the shell running the CLI, not the shell the server was started from: after PORT=9000 make start in one terminal, a plain CLI in another would otherwise address port 10850, where nothing listens. $PORT is still the fallback when no gateway is running, or when the pidfile predates the port line.

A pidfile can also outlive its gateway — killed without cleanup, and the pid reissued to some unrelated process. The local address is still preferred, but publicUrl names the same gateway, so a local address that cannot be reached at all is retried once there, with a line on stderr saying so. An address that answered — including with an error — is never retried: the gateway was reached, and asking somewhere else would only hide its answer.

agents and channels settle the address once, at the start of the session, rather than per request: they thread one base URL through an interactive flow, so a mid-wizard switch would be worse than a single decision up front. The rule for choosing is the same one described above.

gateway status and service install go further: they report on the gateway process on this host, so they resolve --url → http://<bind>:<port> and ignore $CLAUDE_GATEWAY_URL and publicUrl entirely — a proxy still answering from a different instance would otherwise report a dead local service as healthy.

doctor probes the address the CLI will use (that check decides its exit code) and adds an informational probe of the other address whenever the two differ, marked [--]. A public URL that rejects an unauthenticated /health is reported, but never fails doctor — the CLI is not using that address. It also checks gateway.publicUrl itself (marked [warn] when unset — a capability gap, not a misconfiguration — and [!!] when set but nothing answers), since that value backs generate_image reference edits, share_file, and /cli, and nothing else surfaces its absence (#472). Skipped when --url/$CLAUDE_GATEWAY_URL point at a different host — it is this host's own config file, so it is not context for a question about another machine.

Colour. Help and diagnostic text is coloured only when the stream it is going to is a terminal — stdout for a requested help listing, stderr for everything else — so piping either one strips the escapes. Set NO_COLOR to turn colour off, or FORCE_COLOR=1 to keep it through a pipe; NO_COLOR wins if both are set. Result output is never coloured at all: printResult does not touch the palette, so --json piped into jq is unaffected by any of this.

Lifecycle & diagnostics (do not require a running server) ​

CommandDescription
claude-gateway gateway startRun the gateway in the foreground (the only command that boots it)
claude-gateway gateway statusShow the owning manager and /health on the local bind address
claude-gateway gateway restartRestart via the owning manager (systemd-user/systemd-system/pm2/foreground)
claude-gateway gateway stopStop the gateway
claude-gateway gateway logs [--follow] [--lines <n>] [--agent <id>] [--json]Read the gateway log files directly (no running server needed)
claude-gateway doctorCheck local startup files, runtime/voice dependencies, key resolution, manager, and connectivity
claude-gateway doctor fixConfirm and repair supported local setup problems; never starts/restarts the gateway
claude-gateway debug-bundleWrite a small redacted diagnostics bundle for a stuck session
claude-gateway api <METHOD> <path>Escape hatch: call any endpoint directly

A bare claude-gateway gateway (or crons, service, …) prints its verbs and exits 1 — you forgot the verb. The same listing with --help exits 0, and -h is accepted wherever --help is. --help never has a side effect: debug-bundle --help prints usage rather than writing a bundle.

Where help goes. A listing you asked for (--help, or a bare claude-gateway) is the command's result and goes to stdout, so claude-gateway --help | less and claude-gateway crons --help | grep create work. The same listing printed because the invocation was wrong goes to stderr with a non-zero exit, leaving stdout carrying results only. Single-dash tokens other than -h/-V are values, not flags, so a -5 argument is never mistaken for one.

Running as a service ​

CommandDescription
claude-gateway service install [--manager systemd|pm2] [--scope user|system] [--run-as <user>] [--after <target,...>] [--env-file <path>] [--env KEY=VALUE,...] [--config <path>] [--yes] [--print] [--force]Generate and start a service
claude-gateway service status [--manager systemd|pm2] [--scope user|system]Report installed/enabled/active state as JSON
claude-gateway service uninstall [--manager systemd|pm2] [--scope user|system] [--yes]Stop and remove the service
claude-gateway service start [--manager systemd|pm2] [--scope user|system]Start the installed service (found even when inactive)
claude-gateway service stop [--manager systemd|pm2] [--scope user|system]Stop the installed service
claude-gateway service restart [--manager systemd|pm2] [--scope user|system]Restart the installed service (starts it if it was stopped)
  • systemd (the default) installs a user unit at ~/.config/systemd/user/claude-gateway.service — no sudo, and it runs as the user that owns ~/.claude-gateway. Run loginctl enable-linger <user> once if it must survive logout.
  • --scope system (systemd only) installs a root-owned unit at /etc/systemd/system/claude-gateway.service instead, for automated/infra provisioning that needs a fixed system account. Requires already running as root (never escalates via sudo) and --run-as <user>, which becomes the unit's User=; WantedBy= is multi-user.target instead of default.target, and the loginctl hint is skipped. It never triggers the system-scope conflict check below against itself.
  • --after <target,...>, --env-file <path>, and --env KEY=VALUE,... customize the generated unit (either scope): extra After= ordering targets, an EnvironmentFile=-<path> for secrets that never appear in the unit text, and additional non-secret Environment= lines. --env refuses to override HOME, PATH, or GATEWAY_CONFIG.
  • Every path in the generated unit (node, entry point, config, working directory) is absolute, and ExecStart always uses the explicit gateway start command. No secrets are written into the unit — the gateway reads ~/.claude-gateway/.env itself.
  • --print shows exactly what would be installed and exits without touching anything. Install and uninstall both ask for confirmation unless --yes is given (uninstall stops a running gateway), and refuse to run non-interactively without it.
  • install verifies /health on the local bind address, and uninstall reports the state the manager actually reports afterwards — never the state that was intended.
  • Exit codes for install, start and restart (every command that is supposed to leave a running gateway behind): 0 fully healthy, 1 the action itself failed — install/enable, or the start/restart — or a validation/confirmation gate refused, 2 the action succeeded but /health never answered within the poll window. Distinguishing 1 from 2 by exit code alone means a caller doesn't have to parse the JSON result on stdout just to tell "didn't happen" apart from "happened, health unconfirmed". status, stop and uninstall never return 2; they are not trying to produce a service that answers.
  • (systemd, user-scope installs) install refuses by default if a claude-gateway.service unit already exists and is enabled or active at system scope (e.g. from provisioning outside this CLI) — it prints the exact sudo systemctl disable --now claude-gateway.service to resolve it; pass --force to install anyway.
  • Re-running install against an already-active unit whose rendered content changed restarts it automatically; unchanged content leaves the running unit alone.
  • After installing, gateway restart/stop detect and drive that same service.
  • service start\|stop\|restart act on the unit selected by --manager/--scope, the same way status/uninstall do — discovered from disk, so they find an installed-but-inactive service too. This is different from gateway restart/stop, which only drive whatever manager is currently reported active, and can never start a stopped service. stop on an already-stopped (or never installed) service is a no-op success, matching uninstall's idempotence; start/restart on a service that was never installed is an error telling you to run service install first.
  • start on a service that is already running still checks /health and still reports it, so the result is the same shape either way and 2 still means "running, but answering nothing" — a process manager calling a wedged gateway active is precisely the case a health check is for.

App Store (Docker-compose apps) ​

CommandDescription
claude-gateway app listList installed apps and their status
claude-gateway app start <name>Start a stopped app
claude-gateway app stop <name>Stop a running app
claude-gateway app restart <name>Restart an app
claude-gateway app uninstall <name> [--yes]Remove an app's containers and installed files (keeps backups)
claude-gateway app install <source> [--version <v>] [--commit <sha>] [--env KEY=VALUE,...] [--env-file <path>] [--ports NAME=PORT,...] [--wait]Install an app

A thin client over /v1/apps (see the Apps API for the full HTTP reference) — every action is the same admin-gated call the dashboard's App Store UI makes, so there is only one authorization/behavior path to keep correct.

<source> is classified by shape, so there is no separate --registry-app/--github-url/ --local-path flag for the common case: an http(s):// URL is a GitHub source, a path starting with /, ./, ../, or ~ is a local (symlinked, dev-mode) source, and anything else is a registry app name. --version only applies to a registry source; --commit only to a GitHub source (both optional — a GitHub install with no --commit resolves HEAD).

--env and --env-file both fill the install's env_vars, and are merged (--env wins on a conflict). Prefer --env-file for secrets: a value passed on the command line is readable by every local user in /proc/<pid>/cmdline for as long as the install runs, and is written verbatim to the caller's shell history. The file is ordinary dotenv text — KEY=VALUE lines, # comments, and surrounding quotes stripped — parsed exactly like the gateway's own .env.

install is asynchronous: the server returns a jobId immediately and this command never reports "installed" on its own — only that the job was accepted. Poll it with claude-gateway api GET /v1/apps/jobs/<jobId>, or pass --wait to have this command poll here and print the real outcome (streaming the job's log lines to stderr as they arrive).

uninstall asks for confirmation unless --yes is given, and refuses to run non-interactively without it — same convention as service install/uninstall. It removes the app's containers and installed files, but never its backups.

Versions & updates ​

CommandDescription
claude-gateway versionPrint the installed gateway version
claude-gateway update check [--latest]Read-only: installed vs. published gateway version
claude-gateway update [--latest] [--yes]Show current → target, confirm, then npm install -g the target
claude-gateway claude versionPrint the installed Claude Code version
claude-gateway claude update checkRead-only version check for Claude Code
claude-gateway claude update [--yes]Update Claude Code through its own native updater

The gateway follows the npm stable dist-tag, which make promote moves; if nothing has been promoted yet it falls back to latest. --latest skips stable and targets the newest release.

Both use the same detection and install strategy as the dashboard's Update button (src/packages/registry.ts). Claude Code is updated by its native updater, never by npm install -g — that would install a second copy that isn't the binary on PATH. Updating the gateway replaces the files on disk; the running process keeps serving the previous build until claude-gateway gateway restart.

Orchestration tasks (require a running server) ​

Use claude-gateway tasks <verb> --agent <id> --session <id> with the usual connection flags.

CommandAction
tasks listActive tasks; --all includes finished tasks; --page <n> --page-size <n> selects a zero-based page
tasks show <task-id>Details and the full retained result
tasks watch [task-id]Poll every 3 seconds; emit changes; Ctrl+C stops watching only
tasks cancel <task-id>Request user cancellation; stopping is distinct from stopped

Page size is 1–100 (default 10). Watching one task ends at a terminal state; list watching continues and includes new tasks on that page. The authenticated principal must belong to the session. These commands use the running gateway's task controls; they never modify its SQLite database directly. Legacy mode returns ORCHESTRATION_DISABLED.

Agents & channels (require a running server) ​

CommandDescription
claude-gateway agents listList agents accessible by this key
claude-gateway agents create [id] [--description <v>]Interactive wizard — generate workspace files, confirm, optionally connect Telegram/Discord
claude-gateway agents update [--agent <id>]Regenerate AGENTS.md, or connect/update/disconnect Telegram/Discord/LINE/Slack
claude-gateway channels pending --agent <id> [--channel telegram|discord]List incoming pairing requests
claude-gateway channels approve --agent <id> --channel <v> --code <v>Approve a pending pairing request
claude-gateway channels deny --agent <id> --channel <v> --code <v>Deny and remove a pending pairing request

Resource commands ​

crons ​

CommandMethodPathAuthDescription
claude-gateway crons create --agentId <v> --name <v> [--type <v>] [--schedule <v>] [--scheduleKind <v>] [--scheduleAt <v>] [--command <v>] [--prompt <v>]POST/v1/cronskeyCreate a cron job
claude-gateway crons delete <id>DELETE/v1/crons/:idkeyDelete a cron job
claude-gateway crons get <id>GET/v1/crons/:idkeyGet a single cron job
claude-gateway crons list [--agent <v>]GET/v1/cronskeyList cron jobs accessible by this key
claude-gateway crons run <id>POST/v1/crons/:id/runkeyTrigger a cron job now
claude-gateway crons runs <id> [--limit <v>]GET/v1/crons/:id/runskeyGet cron job run history
claude-gateway crons statusGET/v1/crons/statuskeyCron scheduler status
claude-gateway crons update <id> [--name <v>] [--type <v>] [--schedule <v>] [--scheduleKind <v>] [--scheduleAt <v>] [--timezone <v>] [--command <v>] [--prompt <v>] [--telegram <v>] [--discord <v>] [--timeoutMs <v>] [--deleteAfterRun] [--enabled]PUT/v1/crons/:idkeyUpdate a cron job

pstack ​

CommandMethodPathAuthDescription
claude-gateway pstack disable <agentId>DELETE/v1/agents/:agentId/pstackkeyRemove pstack from an agent; its workers stop loading the plugin
claude-gateway pstack enable <agentId> [--roles <v>] [--defaultEffort <v>]PUT/v1/agents/:agentId/pstackkeySet up pstack for an agent: its workers load the pstack Claude Code plugin with these role models (full model IDs)

Doctor and local repairs ​

bash
claude-gateway doctor
claude-gateway doctor --config /path/to/config.json --json
claude-gateway doctor fix
claude-gateway doctor fix --yes --config /path/to/config.json --json

Without --json, doctor prints only the readable check list; it does not append a JSON dump. Use doctor --json to write a compact JSON report to stdout without the readable report. Scripts that previously parsed plain doctor output must add --json. Exit codes are unchanged. For scripted repairs, use doctor fix --yes --json; interactive confirmation and command errors are separate from the check report.

doctor is read-only. It checks Node.js 22+, Claude Code, Bun, ffmpeg, ffprobe and optional Codex; configuration readability, basic structure and owner permissions; runtime/log paths; failed Linux user-service state; known startup log signatures; and the existing API connectivity checks. It does not validate provider credentials, inspect every agent setting, or prove that an audio codec works merely because a binary starts. Missing optional voice binaries appear as warnings. Missing required tools or failed startup/connectivity checks produce exit code 1. Informational historical log matches do not fail the command.

fix asks for confirmation; --yes is required without an interactive terminal. Supported actions are configuration backups and owner permission/BOM repair, missing log-directory creation, resetting a failed Linux user-service state, and installing the ffmpeg package on apt-based Linux or macOS with Homebrew. Other platforms/package managers receive manual instructions. Node.js, Bun, and Claude Code are diagnosed but not installed by this repair command.

Repair rechecks the local setup and health. The command may exit 1 after successful repairs because the gateway is still stopped or another problem remains. Inspect the individual JSON checks and start the gateway/service explicitly afterward. Backups are private files named config.json.doctor-*.bak beside the original. The command never restores a backup automatically or replaces secrets/agents.

Explicit --url or CLAUDE_GATEWAY_URL selects remote diagnosis: local file and binary checks are omitted and fix is refused, including when the URL happens to point to localhost. Run on the target host without those overrides to repair it. The invoking user/PATH may differ from a service/container; run checks in the actual gateway environment. See startup troubleshooting.

For a config owned by the current user without read permission, repair restores access before it can copy the contents into a private backup. It does not change file contents during this step. Linux supports mode 000 through a pinned file descriptor and /proc; other systems can recover write-only files. If neither method is available, restore owner read access manually and rerun doctor.

Refresh an app agent runtime ​

claude-gateway app refresh-runtime APP_NAME [--config PATH] regenerates optional host Codex mounts and recreates only an already stopped installer-owned agent service. It is local-only, keeps backups, and never installs Codex or stops active workers. Drain new work and stop only the agent first; see the maintenance workflow.

Doctor reports native worker auth portability (local configuration, not network or quota verification), effective Codex executable selections, resolved paths, versions and container layout compatibility as optional checks. doctor fix does not install Codex. Service PATH may differ from the interactive doctor environment.

Claude Code inside.