Skip to content

App CLI Reference ​

The binary produced by wick build registers its own command tree. Run ./bin/<app> --help for the live list — this page documents every subcommand.

For the wick host CLI (wick init, wick build, wick doctor, etc.) see CLI Reference.


Default behavior — no args ​

./bin/<app> with no arguments routes by environment:

  • GUI desktop (Windows / macOS / Linux with $DISPLAY or $WAYLAND_DISPLAY) → launches the system tray. Tray internally starts the HTTP server and the background worker as goroutines and exposes MCP install / uninstall, preferences, and self-update from the menu.

  • Headless (Termux, SSH session, Linux server without $DISPLAY) → prints a hint and exits:

    Headless environment detected — tray needs a desktop session.
    Use `<app> start` for background daemon mode, or `<app> all` to run in foreground.

Headless builds (wick build --headless) strip the tray symbols entirely; this command exits non-zero with the same hint regardless of $DISPLAY.


Run (foreground) ​

These commands block the calling shell — Ctrl+C exits cleanly. Use them for dev runs, Docker CMD, or supervisors like systemd / pm2.

<app> server ​

Start the HTTP server only. Useful for splitting server / worker across containers, or running behind an external process supervisor.

bash
./bin/myapp server
./bin/myapp server --port 9000              # override the resolved port
./bin/myapp server --host 127.0.0.1         # bind specific interface
./bin/myapp server --host 192.168.1.42      # bind one NIC only (multi-network host)
./bin/myapp server --localhost              # shortcut for --host 127.0.0.1
WICK_HOST=127.0.0.1 ./bin/myapp server      # same, via env

--host <addr> / --localhost / WICK_HOST — pin the listen interface. Default (all empty / unset) binds 0.0.0.0 so Docker, systemd, and remote-VPS deploys keep working without any flag. Setting 127.0.0.1 makes the kernel drop SYN packets from any non-loopback source — required on Termux phones where unrooted Android has no firewall to keep :9425 private. Pair with ssh -L 9425:localhost:9425 -p 8022 user@phone-ip for remote access.

Precedence when multiple are set: --host wins over --localhost; both env-overridable via WICK_HOST (env only takes effect when no flag is passed).

<app> worker ​

Start the background job worker only. Pair with server in a separate process / container when you want to scale them independently.

<app> tray ​

Explicit tray subcommand — same as <app> with no args on a GUI host. In headless builds this prints tray not available in headless build and exits non-zero.

<app> all ​

Run the HTTP server and the cron scheduler in the same process, sharing one manager.Service. Use this for single-node deployments where server and worker can't share a volume (single-container Docker, simple VPS, etc.) — without a shared filesystem a separate worker pod can't see provider-storage files restored on the API pod, so jobs that touch those files silently no-op.

bash
./bin/myapp all
./bin/myapp all --port 9000
./bin/myapp all --host 127.0.0.1   # bind specific interface
./bin/myapp all --localhost        # shortcut for --host 127.0.0.1

Trade-offs vs. running server + worker as separate processes:

Aspectall (single-node)server + worker (multi-pod)
FilesystemOne pod, no volume sharing neededNeeds shared volume (or per-pod restore)
ScalingVertical only (one process)Horizontal (scale worker independently)
Cron double-fireImpossible (one manager.Service)Possible if you accidentally run two workers
Failure isolationHTTP crash kills cron tooIndependent

The scheduler goroutine auto-respawns with exponential backoff (2s → 30s cap) if it exits unexpectedly. Clean shutdown via SIGTERM / Ctrl+C stops both.


Daemon (background) ​

The daemon commands spawn the binary detached from the calling shell so the parent terminal can exit without killing it. A PID file under ~/.<app>/run.pid tracks the running instance; output goes to a dated log under ~/.<app>/logs/daemon-YYYY-MM-DD.log — a fresh file per day, swept by the same retention as the other runtime logs (see Logs).

<app> start ​

Spawn the binary in the background. Mode is chosen at runtime:

  • GUI host → spawns <app> tray detached (the interactive icon, with its own autostart / server / worker toggles)
  • Headless host → spawns <app> all detached (server + worker, no UI)
bash
./bin/myapp start
./bin/myapp start --host 127.0.0.1   # bind specific interface — child inherits WICK_HOST
./bin/myapp start --localhost        # shortcut for --host 127.0.0.1
# started myapp as `tray` (pid 12345)
#   log: ~/.myapp/logs/daemon-2026-07-13.log
#   pid: ~/.myapp/run.pid

--host / --localhost (and WICK_HOST=...) propagate to the spawned child via process env, so both tray (GUI) and all (headless) modes honor the chosen bind interface. See server --host for the full security rationale.

Re-running start while a live daemon is recorded is a no-op — prints already running (pid N) and exits 0.

<app> stop ​

Send SIGTERM to the daemon, wait up to --timeout (default 5s) for graceful exit, then SIGKILL if still alive. Removes the PID file even after a force-kill.

bash
./bin/myapp stop
./bin/myapp stop --timeout 15s

Stale PID files (process no longer alive) are silently cleaned up so the next start has a fresh slot.

<app> restart ​

stop + start in one command. Uses the same mode selection as start.

bash
./bin/myapp restart
./bin/myapp restart --timeout 15s
./bin/myapp restart --host 127.0.0.1   # rebind specific interface on new daemon
./bin/myapp restart --localhost        # shortcut for --host 127.0.0.1

<app> reload ​

Replace the binary without downtime. The running daemon starts a successor, hands it the listening socket, and only then drains its own work and exits — the port is never closed, so nothing in front of wick (nginx, a load balancer, a browser) sees a refused connection, and no in-flight workflow run or cron job is cut off.

This is the difference that matters against restart: restart stops first, so the port is closed for the successor's whole boot — tens of seconds once a registry restore, a database and connectors are involved — and everything in flight is killed.

bash
# check the candidate, install it, hand over, verify — one command
./bin/myapp reload --binary ./bin/myapp     # --sudo when the target directory is root-owned

It checks the candidate, installs it, signals the handover and returns — about a second, not a minute and a half:

installed /usr/bin/myapp (previous kept at /usr/bin/myapp.prev)
handover started: pid 319449 is booting the successor (0.1.155 -> 0.1.156)
not waiting for it; the old process keeps serving until the new one is ready

It does not block, because there is nothing to block for: the old process answers every request for the whole boot. A refusal IS caught before it returns — the daemon records one within milliseconds — and that path rolls the binary back. Add --wait when a script wants the confirmation instead:

handover done: pid 618434 -> 668241, 0.1.113 -> 0.1.114
Flag
--binary <path>install this build at the daemon's exec path, then hand over to it
--sha256 <sum>verify the file's checksum before anything else reads or copies it
--yes / -yskip the confirmation prompt — required when stdin is not a terminal
--forceproceed despite a blocking finding; never overrides OS/arch or a non-wick binary
--sudorun the file swap through sudo, for a root-owned target directory
--waitblock until the successor is serving, and roll back if it never gets there. Off by default — the old process serves throughout the boot, so waiting blocks the caller and helps nobody
--wait-drainalso wait for the previous process to finish its work and exit (implies --wait)
--timeout <dur>how long to wait for the successor to take over (default 5m)

The candidate is checked before the file is touched. Identity comes from its embedded build info, never from running it — executing an unknown binary is self-defeating when the whole question is whether it is what you think it is:

  • FATAL, no override — built for another OS or architecture, or nothing in its module graph depends on wick. A wrong-architecture binary in place is a crash-loop the service manager retries forever. Also the SAME version as the running binary: a different build wearing the same number makes the update card, version and any rollback lie about what is serving — bump the version instead.
  • BLOCK, --force to proceed — a different main module, a different BuildAppName (that app has its own data dir, unit and paths), or a version older than the one running.
  • WARN — a wick resolved through a local replace tree instead of a released tag.

If the successor does not take over within --timeout, the previous binary is restored and the command exits non-zero. Nothing is down while that happens — the old process only steps aside once a successor reports ready.

By hand, which is what the command does for you and the fallback on a host whose binary predates the flag:

bash
cp ./bin/myapp /usr/local/bin/myapp.new && chmod +x /usr/local/bin/myapp.new
mv -f /usr/local/bin/myapp.new /usr/local/bin/myapp   # atomic rename
./bin/myapp reload

The rename is not a detail: copying onto a running binary fails with ETXTBSY ("Text file busy"), while a rename swaps the directory entry and lets the running process keep its old inode until it exits.

Install it at the path the successor will exec: the handover re-execs os.Args[0] resolved through PATH, not the inode currently running — that one is gone as soon as the file is replaced. If the unit says ExecStart=/usr/local/bin/myapp all, that file is the one that must hold the new bytes; anywhere else gives a reload that reports success and brings the old version back. --binary resolves that path from the running process instead of assuming it.

Requires the daemon to run with WICK_GRACEFUL_UPGRADE=1; without it the signal is logged and ignored, and you should use restart. Supported on Linux, macOS and BSD, as a plain daemon or under a service manager. On Windows — and when wick runs inside the tray's in-process supervisor — a socket cannot be handed to another process, so reload reports that and you fall back to restart.

reload routes itself: an installed systemd unit gets systemctl --user reload, a unit without ExecReload is signalled by its MainPID, and a PID-file daemon is signalled directly. kill -HUP <pid> does the same thing.

What the drain waits for, because interrupting them costs different things:

WorkWaited forWhy
Agent turns, workflow runs, cron jobs, connector calls, plugin requests, delegations, scheduled deliveriesuntil every subsystem is at zero and has stayed there for WICK_DRAIN_QUIET (default 15s)Interrupting them loses work, or cuts a reply off mid-sentence

There is no default deadline, on purpose: a ceiling on a drain does not stop work, it kills it. Whatever is running is waited for however long it takes, and the process exits the moment the last of it settles. An idle agent subprocess holds nothing and never blocks the handover. WICK_DRAIN_TIMEOUT adds an optional hard cap for an unattended deploy that must finish inside a known time, and is unset by default. It is read at process start, so changing it needs a restart rather than a reload — a successor inherits its parent's environment.

Every 15 seconds a drain logs what is still outstanding, by name, so a long wait reads as "finishing a run" instead of a hang. The same list is on the System page (/admin/advanced/software-update) as Running now, next to a Force swap control for an operator who has read it and decided not to wait — the only thing on a default configuration that interrupts running work.

In-flight HTTP requests are waited for like any other work. Streams (SSE, hijacked websockets) are excluded — they do not end while a client is watching — and are dropped at the very end, after everything else has settled.

Self-update takes the same path. With graceful upgrade armed, applying an update from the admin UI swaps the binary and then hands over exactly like reload — no separate step, and no window where the port is closed. Without it, the update re-execs in place as before.

Proving it

Claiming zero downtime from the server's own logs proves nothing. Probe from outside, through whatever sits in front of wick, and count the failures:

bash
while true; do
  printf '%s %s\n' "$(date -Is)" "$(curl -s -o /dev/null -m 5 -w '%{http_code}' https://wick.example.com/health)"
  sleep 0.5
done | tee probe.log

grep -v ' 200$' probe.log     # anything here is real downtime

Then check that MainPID changed while the unit never left active.

<app> status ​

Report whether the daemon is alive, its PID, approximate uptime (PID file mtime), and the log / PID file paths. --log N tails the last N bytes of the daemon log — the newest daemon-YYYY-MM-DD.log in logs/, so a daemon that's been running since a previous day still tails correctly instead of showing an empty today-file.

bash
./bin/myapp status
# myapp: running
#   pid:     12345
#   started: 2026-05-30T13:02:28+07:00 (37s ago)
#   log:     ~/.myapp/logs/daemon-2026-07-13.log
#   pidfile: ~/.myapp/run.pid

./bin/myapp status --log 1000
# ... last 1000 bytes of the daemon log appended

Auto-start service ​

<app> service registers (or removes) the binary as an OS-level auto-start. Routing depends on the runtime environment:

HostBackendPath
Windows desktopHKCU Run registryHKCU\Software\Microsoft\Windows\CurrentVersion\Run\<app>
macOS desktopLaunchAgent plist~/Library/LaunchAgents/<app>.plist
Desktop Linux (DISPLAY set)XDG autostart~/.config/autostart/<app>.desktop
Headless Linux server / Raspberry Pisystemd-user unit~/.config/systemd/user/<app>.service
Termux on AndroidTermux:Boot script~/.termux/boot/<app>-start

GUI hosts share the same registration the tray's Auto-start app at login toggle uses, and service install / service uninstall also flip userconfig.AutoStartApp so the tray checkbox stays in sync. Headless hosts get a daemon-style unit that runs <app> all directly.

All backends install into user scope — no sudo / admin required.

<app> service install ​

bash
./bin/myapp service install
# installed myapp service
#   backend: autostart-gui
#   path:    HKCU\Software\Microsoft\Windows\CurrentVersion\Run\myapp

On headless Linux (systemd-user backend), service install also attempts to auto-enable systemd lingering for the current user so the daemon survives logout and starts at boot with no one logged in. If the host's polkit allows self-linger (most modern distros do), this happens silently. If it fails, service status reports the exact command to run manually (see below).

Re-running over an existing install rewrites the unit / entry — handy after the binary moves.

<app> service uninstall ​

bash
./bin/myapp service uninstall
# uninstalled myapp service

Returns service not installed if nothing was registered.

<app> service status ​

bash
./bin/myapp service status
# myapp service: installed
#   backend: systemd-user
#   path:    ~/.config/systemd/user/myapp.service
#   active:  true
#   note:    linger enabled — service survives logout and starts at boot

When lingering is off (e.g. the host denied the auto-enable during install), the note reads:

note:    linger DISABLED — service stops at logout; run: sudo loginctl enable-linger <user>

The username shown is the real login name detected at runtime — never a hardcoded value.

On GUI hosts, status also flags drift between the OS entry and userconfig.AutoStartApp so you can spot a tray checkbox that's out of sync with reality and re-run install / uninstall to re-align.

Headless vs. GUI on the same machine

On Termux or a Linux box without $DISPLAY, service install writes a <app> all unit / Termux:Boot script. The moment you SSH into a Mac, the same command refuses with a hint pointing back to the tray's autostart toggle — daemon-style auto-start only makes sense without a usable GUI session.


Config ​

Manage the app-level runtime configs that drive the manager (app URL, allowed origins, etc.). The same rows are editable from the admin UI; the CLI is here for headless setup and scripting.

<app> config list ​

Dump every config row (secrets masked). Env overrides are flagged.

bash
./bin/myapp config list
# allowed_origins          = http://localhost:9425
# app_url                  = http://localhost:9425
# default_admin_password   = ********  (overridden by ADMIN_PASSWORD)

<app> config get <key> ​

Print one value — secrets are not masked.

bash
./bin/myapp config get app_url

<app> config set <key> <value> ​

Update one value. Rejects locked rows and rows currently overridden by an env var.

bash
./bin/myapp config set app_url https://my-deployment.example.com

<app> config profile <full|agent|lite> ​

Set the connector build profile this instance registers at boot. The value is stored as a profile config row in the database; a restart is required for the change to take effect.

bash
./bin/myapp config profile full    # all builtin connectors (default)
./bin/myapp config profile agent   # curated subset: HTTP REST, Slack
./bin/myapp config profile lite    # no builtin connectors registered
# profile = agent (restart to apply)
ProfileBuiltin connectors registered at boot
full (default)All 4: HTTP REST, Slack, Loki, Phoenix
agentHTTP REST, Slack
liteNone

GitHub, Bitbucket, and Google Workspace are no longer builtin connectors — they ship as downloadable plugins and are not profile-eligible. Install them separately with <app> plugin install github / bitbucket / google_workspace.

The four runtime connectors (Wick Manager, Workflow, Notifications, Custom Connector) are never profile-gated and always register regardless of this setting.

The active profile can also be read and written via the admin Advanced page (/admin/variables, key profile).

<app> config allowed-origins ​

A second-level group for managing the host allowlist (URLs that may reach the manager beyond app_url).

bash
./bin/myapp config allowed-origins list
./bin/myapp config allowed-origins add http://192.168.1.42:9425
./bin/myapp config allowed-origins remove http://192.168.1.42:9425
./bin/myapp config allowed-origins autodetect   # interactive LAN whitelist picker

autodetect scans the host's private LAN IPv4 addresses, prints each as a candidate URL, and prompts which ones to whitelist:

LAN access — detected 2 IPv4 address(es) on this device:
    [1] http://192.168.1.42:9425
    [2] http://10.0.0.5:9425

  Whitelist for browser access from other devices?
    a = all       n = none (default)       1,2,3 = pick by number
  Choice [n]:

Plugin ​

Manage external connector plugins — binaries distributed separately from the main app and installed without a rebuild. A hot-reload poller running inside the app detects changes and wires plugins into the connector registry automatically — no restart needed for install, enable, disable, or remove (changes take effect within a few seconds).

<app> plugin search [query] ​

List connector plugins available in the marketplace catalog. Optionally filter by name or description keyword.

bash
./bin/myapp plugin search
./bin/myapp plugin search slack

The catalog is a single plugins.json file fetched raw from the wick repo's default branch (plugins/plugins.json, not the GitHub API — so no rate limit and no token needed). Override the catalog URL with WICK_PLUGIN_CATALOG=<url>.

<app> plugin install <name|url|path> ​

Install a connector plugin.

bash
./bin/myapp plugin install slack              # resolve from the marketplace registry
./bin/myapp plugin install ./slack-plugin/   # local directory {binary, plugin.json}
./bin/myapp plugin install https://…/slack-0.1.0-linux-arm64.zip   # direct URL

The three install sources:

ArgumentBehavior
<name>Looks up the latest matching release in the registry, downloads the arch-matching zip, verifies the manifest, and installs
<path>Installs from a local directory containing the binary and plugin.json
<url> or <file>Downloads / extracts a .zip or .tar.gz archive, verifies, and installs

<app> plugin list ​

List installed connector plugins with their version, arch compatibility, signature status, and enabled/disabled state.

bash
./bin/myapp plugin list
# slack                0.1.0        arch:yes  signed:valid   enabled
# googleworkspace      0.2.1        arch:yes  signed:valid   enabled

<app> plugin enable <key> ​

Enable a previously disabled plugin. The hot-reload poller picks it up without a restart.

bash
./bin/myapp plugin enable slack

<app> plugin disable <key> ​

Disable a plugin without removing it. The change takes effect on the next reload cycle.

bash
./bin/myapp plugin disable slack

<app> plugin remove <key> ​

Uninstall a plugin by deleting its directory under the plugins folder.

bash
./bin/myapp plugin remove slack

Plugins are stored under ~/.<app>/plugins/ by default.


MCP ​

The binary ships an MCP server over stdio so Claude Desktop / Cursor / Gemini / Codex / Claude Code can call wick tools as a model context provider.

<app> mcp serve ​

Run the MCP server over stdio. Spawned by the AI client based on the entry written by mcp install. Rarely invoked by hand.

<app> mcp install ​

Write the binary's MCP entry into the chosen client's config file. Resolves os.Executable() so the entry points at the actual built binary, not at wick.

bash
./bin/myapp mcp install                          # all detected clients
./bin/myapp mcp install --client claude          # Claude Desktop only
./bin/myapp mcp install --client claude-code     # writes ~/.claude.json
./bin/myapp mcp install --name custom-server     # override server name

--client accepts: claude, cursor, gemini, codex, claude-code, all. The default server name is the basename of the current directory.

<app> mcp uninstall ​

Remove the entry written by install. Same flags as install.


Version ​

Three equivalent forms print the embedded release string in one line and exit. The format is stable — scripts/install.sh's probe greps it against the resolved release tag to decide whether a re-install should be skipped.

bash
./bin/myapp version
./bin/myapp --version
./bin/myapp -v
# myapp version v0.14.13 (wick v0.14.13)

The gate sidecar (<app>-gate) accepts the same three forms and prints its own version line:

bash
./bin/myapp-gate version
./bin/myapp-gate --version
./bin/myapp-gate -v
# myapp-gate version v0.14.13

The version handler on <app>-gate returns before the binary reads stdin, so probing it from a script does not interfere with its normal PreToolUse hook path.


Uninstall ​

<app> uninstall ​

Clean up per-user OS integrations before removing the binary. By default removes the autostart entry and every MCP client entry the app installed.

bash
./bin/myapp uninstall                # autostart + MCP entries
./bin/myapp uninstall --mcp=false    # autostart only, leave MCP entries

Run this before deleting the binary so login items, autostart entries, and orphan MCP config rows don't pile up on the user's machine.

Built with ❤️ by a developer, for developers.