Skip to content

Command line

vxnsin edited this page Sep 4, 2026 · 11 revisions

Command line

warden                 introduce itself and list the commands
warden --version       print the version

warden serve           run the registry
warden tui             open the dashboard

warden run             start a program on a port warden picked
warden env             claim a port and print it as environment
warden register        claim a port and print it
warden get             print the address of one service
warden ls              list every registered service
warden release         give a port back
warden reap            release ports held by something that is gone
warden heartbeat       push a lease out before it runs out
warden pool            show how much of the pool is in use
warden history         what happened to a port, or to a service
warden events          follow what happens, as it happens
warden webhook         where events are posted, and whether it works

warden apply           register everything a project's warden.toml asks for
warden export          write the reverse-proxy configuration

warden ports           show what is listening on this machine
warden kill            stop whatever is holding a port

warden nodes           list the wardens this one knows about

warden doctor          check everything and say what is wrong
warden service         start warden with the machine
warden setup           answer a few questions, once
warden settings        show what is in effect, and where it came from
warden update          say whether a newer warden exists, and fetch it

Every command that talks to a warden takes --url / -u and --token, both of which also read WARDEN_URL and WARDEN_TOKEN. Every command that prints a table also takes --json.

Two flags run through the fleet, and they mean different things:

Flag On Means
--all ls, pool, ports, tui Ask every warden, not just this one
--node NAME register, release, heartbeat Do it on that one warden, through this one

warden kill reads the local machine and ignores --url entirely. warden ports does too, unless --all is given, which needs a hub to ask.

--install-completion teaches your shell to complete every one of them; see Installation.


warden run

Start a program on a port warden picked, hold the port while it runs, and give it back when it exits.

$ warden run -- npm run dev
shop-api  ->  8000

  VITE ready, listening on http://localhost:8000

Everything after -- is the command. The child gets PORT, WARDEN_PORT, WARDEN_HOST, WARDEN_ADDRESS and WARDEN_SERVICE added to its environment — PORT because every framework already reads it.

Flag Meaning
--name Register under this name, default the current directory's
--kind, -k What the service is, default service
--project Group services of one codebase
--host Interface the process will bind to
--preferred-port Wish for this port, take another if it is not free
--require-port Insist on this port, and fail if it is not free
--ttl Hold a lease this long, renewed while the process runs
--anyway Start on a free port when no warden answers

Three things it does that a shell wrapper gets wrong:

  • The child's exit code becomes warden's, so nothing swallows a failure.
  • The port is released whether the program exits, crashes or is interrupted.
  • The registration carries the child's process id, which is what later tells a held port from an abandoned one. See warden reap.

With no warden reachable it refuses rather than picking a port itself, because guessing at a port is the thing warden exists to stop. --anyway overrides that, takes a free port from the pool, and says on standard error that nothing is registered.

warden env

The same claim, printed instead of wrapped — for an IDE run configuration, a Makefile, or anything else that cannot be started by warden.

$ warden env shop-api --kind backend
PORT=8000
WARDEN_PORT=8000
WARDEN_HOST=127.0.0.1
WARDEN_ADDRESS=127.0.0.1:8000
WARDEN_SERVICE=shop-api
Flag Meaning
--kind, -k What the service is, default service
--project, --host, --preferred-port, --require-port As warden register
--export Prefix each line with export
--write .env Set those keys in a dotenv file, leaving every other line alone
eval $(warden env shop-api --export)
warden env shop-api --write .env

--write replaces each key where it already stands and appends the rest. A .env is usually somebody else's file too, so nothing warden did not write is touched.

warden serve

Run the registry.

Flag Meaning
--host Interface to listen on
--port Port to listen on
--pool 8000-8999 Range of ports to hand out
--reserved 8080,9000-9010 Ports never handed out
--database Path to the registry database
--no-probe Do not test ports for existing listeners

Fleet settings have no flags; they are environment only. See Configuration.

warden register

Claim a port. Prints the port and nothing else, so it drops into a script.

Flag Meaning
--kind, -k Required. What the service is
--project Group services of one codebase
--host Interface the service will bind to, default 127.0.0.1
--preferred-port Wish for this port, take another if it is not free
--require-port Insist on this port, fail if it is not free
--ttl Release the port again after this many seconds
--pid Process id of the service
--count Claim this many at once, named <name>-1 upwards
--contiguous With --count, insist they run back to back
--node Register on that warden in the fleet, through this one

--preferred-port and --require-port cannot both be given.

More than one at once

A stack that needs four ports can ask four times and hope nothing takes one in between, or ask once:

$ warden register stack --kind backend --count 4
8800
8801
8802
8803

They come back as stack-1 to stack-4, chosen and written under one transaction, so either all four are held or none are. Asking again renews the same four rather than shuffling a running stack onto different ports.

--contiguous insists they run back to back, for the tools that will not take a scattered set. When no run is long enough it says so and writes nothing:

$ warden register row --kind backend --count 6 --contiguous
no run of 6 free ports in 8800-8809 on 127.0.0.1

warden pool says it before it comes to that, whenever the two numbers differ: 8800-8809 5 allocated 5 free 0 reserved 4 in a row.

A group takes no --require-port: that has no sensible answer for four ports at once, and a caller who needs one particular port needs one registration.

$ warden register shop-api --kind backend --project shop
8000

warden get

Print one service's address.

$ warden get shop-api
127.0.0.1:8000

Given as node/service, it asks that node through the hub:

$ warden get build-01/build-runner --url http://hub:7010
127.0.0.1:9000

Exits 1 with no service registered as 'x' when it is not there.

warden ls

List registered services.

Flag Meaning
--project Only this project
--kind Only this kind
--holders Also say whether each holder is still there
--stale Only the ones whose holder is gone
--all Ask every warden in the fleet, not just this one
$ warden ls --project shop
SERVICE   KIND      PROJECT  ADDRESS         PID
shop-api  backend   shop     127.0.0.1:8000  -
shop-web  frontend  shop     127.0.0.1:8001  -

--all asks every warden in the fleet and adds a NODE column. Nodes that did not answer are named on standard error, so a pipe still gets a clean table. See Cluster.

--holders adds a HOLDER column saying whether whoever asked for each port is still there:

$ warden ls --holders
SERVICE   KIND     ADDRESS         PID    HOLDER
shop-api  backend  127.0.0.1:8000  14204  running
old-job   worker   127.0.0.1:8001  9930   gone

A holder is gone when the process it named no longer exists, or when nothing is listening on its port. It costs one sweep of the machine's sockets, which is why it is asked for rather than always done, and it cannot be combined with --all: only the machine a service runs on can see whether it is still there.

A registration touched in the last 30 seconds is never called gone. A service that has registered and not yet bound its socket is starting, not dead.

warden release

Give a port back to the pool.

$ warden release shop-web
released shop-web

--node build-01 releases it on that warden instead, through the one you asked.

warden reap

Release the ports of services that are no longer there.

$ warden reap
release old-job? nothing is on 8001 and pid 9930 is gone [y/N]: y
released 1

It asks per service and names why each one qualifies. --yes skips the questions, for a machine that runs it on a schedule.

Nothing is ever reclaimed on its own. A service in the middle of a restart would lose its port to a timer, and a registry that takes ports back without being asked is worse than one holding a few dead rows.

warden heartbeat

Push a lease out before it runs out, for a service registered with a --ttl.

$ warden heartbeat ci-runner --ttl 600
127.0.0.1:9000
Flag Meaning
--ttl Seconds from now; without it, the lease it registered with
--pid Update the recorded process id at the same time
--node Do it on that warden in the fleet

warden pool

$ warden pool
8000-8999  2 allocated  997 free  1 reserved

Counts only ports inside the pool. A service on a --require-port outside the range is registered but not counted here.

--all reports every node and the fleet totals, with FREE coloured by how little is left — the point of the view being to spot the machine about to run out:

$ warden pool --all --url http://hub:7010
NODE      POOL       HELD  FREE  RESERVED
build-01  9000-9099  4     96    1
hub       8000-8999  2     997   1
web-02    9000-9099  98    2     1

The totals are a sum of what is left, never one pool the fleet shares: two nodes may hand out the same numbers on different machines.

warden history

What happened to a port, kept after it stopped being true.

$ warden history 8000
WHEN    WHAT        SERVICE   KIND     ADDRESS         PID
7s ago  released    shop-api  backend  127.0.0.1:8000  14204
2h ago  registered  shop-api  backend  127.0.0.1:8000  14204
Argument Meaning
what A port, or a service name. Everything recent without one
Flag Meaning
--limit How many to show, default 20

Every registration, renewal, move, release and expiry is written down as it happens, so warden history 8000 still answers for a service released weeks ago. Rows are dropped past a cap, so a warden left running for a year does not grow a database nobody asked for.

warden events

Follow what happens, as it happens. Runs until it is stopped.

$ warden events
09:41:02  registered   shop-api  127.0.0.1:8600
09:41:11  renewed      shop-api  127.0.0.1:8600
09:41:44  released     shop-api  127.0.0.1:8600
Flag Meaning
--json One event per line, flushed as it arrives, so it pipes

The stream carries every action — registered, renewed, moved, released, expired — and filtering is the reader's business. GET /v1/events is the same thing as server-sent events, behind the same token as every other read.

See Events and webhooks.

warden webhook

Where events are posted, and whether that is working.

$ warden webhook
address    https://discord.com/api/webhooks/...
shape      discord
events     registered and released
delivered  7
Flag Meaning
--test Post one made-up event from this machine now

Without --test the answer comes from the warden that is running. With it, the post goes out from this machine using this machine's settings — which is what warden setup has just written, and a different thing. If the two disagree, the running warden has not been restarted.

The address is always shown cut back to its host. A webhook URL is a credential and its path is the half worth stealing.

warden apply

Register everything a project's warden.toml asks for.

$ warden apply
SERVICE      KIND      ADDRESS         WHAT
shop-api     backend   127.0.0.1:8900  taken
shop-worker  worker    127.0.0.1:8901  taken
Flag Meaning
--file, -f Which manifest to read, default warden.toml here
--env Also write the ports into this env file
--release Give the project's ports back instead

Running it twice changes nothing the second time: it renews, and never shuffles a running project onto different ports. See Projects.

warden export

Write the reverse-proxy configuration for what is registered.

$ warden export caddy --domain example.com
Argument Meaning
proxy caddy, nginx or traefik
Flag Meaning
--domain Names become <service>.<domain>
--project, --kind Narrow it down
--all Take the whole fleet, each service at its own machine

It prints and stops. See Reverse proxy.

warden ports

What is listening on this machine. Needs no server.

Flag Meaning
--port Only this port
--udp / --no-udp Include UDP sockets, on by default
--all Ask every warden in the fleet, and add a NODE column

The WARDEN column names the service when the port came from the registry, which needs a warden to be reachable. Without one the column is simply blank; the rest still works.

See Ports and processes.

warden kill

Stop whatever is holding a port. Needs no server.

Argument Meaning
target A port, or a process id with --pid
Flag Meaning
--pid Read the number as a process id
--force Kill it outright if it will not stop politely
--yes, -y Do not ask first

warden nodes

List the wardens this one knows about.

Flag Meaning
--forget NAME Remove a node that is not coming back
$ warden nodes --url http://hub:7010
NODE      URL                   POOL       VERSION  STATUS  LAST SEEN
build-01  http://build-01:7010  9000-9099  0.1.0    online  4s ago

warden tui

The dashboard. --interval sets the refresh in seconds, default 2. --all shows the whole fleet instead of one warden.

Key Action
j k Move
tab Switch between services and ports
n Step through one node at a time, with --all
r Reload now
d Release the service, or stop the process
q Quit

The ports view works with no warden running at all: it falls back to reading the machine directly, says so in the subtitle, and d stops a process the same way warden kill does. The services view still needs a registry.

Exit codes

0 on success, 1 on any refusal, with the reason on standard error. The message is the same one the API would return, so a script can act on it:

if ! PORT=$(warden register shop-api --kind backend 2>/dev/null); then
    echo "no port available" >&2
    exit 1
fi

warden doctor

Everything worth checking, in one command, so "why is this not working" does not mean running four others and comparing what they said.

$ warden doctor
ok    warden 0.1.0 answering at http://127.0.0.1:7010, role hub
ok    settings from ~/.config/warden/warden.toml
warn  listening on 0.0.0.0 with no token set - anyone who can reach this
      machine can hand out and release ports
ok    pool 8000-8999, 4 held, 995 free
warn  2 of 6 registrations held by something that is gone - warden reap
ok    build-01 online, last seen 4s ago
note  could not check for updates - github is unreachable
Level Means
ok Checked, nothing wrong
note Worth knowing, or a check that could not run at all
warn Works, but somebody should look at it
fail Broken

Exits 1 only when something failed, and 0 on warnings, so it drops into a health check without an unset token being read as the machine being down.

warden service

Have this machine start warden serve at login.

$ warden service install
systemd user unit
  ~/.config/systemd/user/warden.service
  [Unit]
  Description=warden - hands out ports and says who holds them
  ...
  systemctl --user daemon-reload
  systemctl --user enable --now warden.service
Write it? [y/N]:
Command Does
warden service install Show what it would write, then write it
warden service uninstall Take it away again
warden service status Say whether it is installed, and whether it is up

--yes on install and uninstall skips the question.

System What gets written
Linux A systemd user unit in ~/.config/systemd/user
macOS A launchd agent in ~/Library/LaunchAgents
Windows A command in the Startup folder

Always as the account that ran it, never as root or SYSTEM: a warden started by another user reads that user's settings and hands out ports from a registry nobody else can see.

On Windows it is a Startup command rather than a scheduled task, because creating a task is refused outright on plenty of managed machines. That also means there is no service manager to ask, so status says running when a warden is answering on the configured port, whoever started it.

warden setup

Asks the handful of questions that matter and writes the answers to the config file. It keeps any setting it did not ask about. See Configuration.

In a terminal it is one screen:

Key Action
tab shift+tab Move between fields
space Toggle a switch or a tick box
enter Open a menu, or pick from it
pgup pgdn Scroll without leaving the field you are in
ctrl+t Post a test event to the webhook address on screen
ctrl+s Save
ctrl+q Leave without writing anything

Questions that nothing has earned stay hidden: no token field until the warden is reachable from elsewhere, no upstream address until it reports to one, no webhook shape until events go anywhere, and no signing secret unless the shape is json. A refusal names the field and puts the cursor in it.

It fits an 80 by 24 terminal; under 84 columns the labels move above the fields they name and nothing is cut off.

Flag Meaning
--plain Ask one question at a time instead of a screenful

Without a terminal - a script piping answers in, a job on a build machine - the questions come one at a time on their own. So do they where TERM is unset or dumb, which is what a cron job or a serial console gets; warden tui says so and points at warden ls rather than ending in a traceback.

A warden that is already running keeps the settings it started with. It reads them once, at startup. Restart it to pick up a new webhook or pool.

warden settings

Every setting, its value, and which source that value came from.

warden settings                    # the table
warden settings --json             # the same as data
warden settings set pool_end 4199  # write one to the config file
warden settings unset probe        # take it back out

set types and checks the value before writing it, so the file never holds something warden would refuse to start with, and it says so when the environment will override what you just wrote anyway.

Clone this wiki locally