CLI reference¶
Every verb, with its flags, and the invariant the read backends behind them all obey.
Start here¶
One binary, one grammar. The verbs a new host actually needs:
agentctl init # scaffold the GitOps repo (default /opt/agentctl)
agentctl provision # host bootstrap (idempotent)
agentctl agents add <name> --channel webview --user-email <e> # a new agent
agentctl apply [<agent>] # reconcile, and restart exactly what changed
agentctl status # host readiness + fleet overview
agentctl notify <agent>|self <text> [--file <path>] # message the owner over its channel
agentctl logs <name>|self [-f] # an agent's unit journal
agentctl version # print the build version
Everything else follows the same shape: agents, mcp, tool, secret, login, cron,
the gateway installers, the read-only monitoring verbs and the subsession verbs.
health / doctor exit non-zero on problems, so they double as a watchdog /
host-monitoring signal.
The full grammar¶
agentctl version | --version | -V # print the build version
agentctl init [<dir>] [--git <url>] # scaffold the GitOps repo (default /opt/agentctl): agents.yaml + agents.d/ shards + mcp/skills/agents/hooks/tools overlays + secrets.d/ store — or join an existing fleet with --git <url>
agentctl provision # host bootstrap (idempotent)
agentctl status # host readiness + fleet overview
agentctl queue # daemon reconcile queue: pending / in-flight / last result
agentctl apply [<agents.yaml>] [<agent>] [--dry-run] [--force] [--prune [--yes]] [--json] # declarative reconcile from the repo — RECONCILES everything AND RESTARTS exactly what changed (self-bouncing, as of 2.2.4); --force = reconcile + restart ALL agents+gateways; --dry-run previews the restart set; --prune tears down live agents no longer in the ledger (--prune lists orphans, --prune --yes userdel -r's them) (see ../operating/operations.md#apply-semantics)
agentctl defaults list | add <kind> <name> | remove <kind> <name> # host-level catalog-defaults overlay (defaults.yaml) applied to every agent
agentctl migrate <ssh-target>[:port]|<local-path>|--local <manifest.yaml> [--agent <name>] [--new-name <dest>] [--bot-token <t>] [--owner-id <id>] [-i <ssh-key>] [--cutover] # relocate one agentctl agent (SSH/local); --cutover stops the source
agentctl converge <agent> [--dry-run|--apply] # native Go reconcile of ONE agent's steady state (the engine under apply)
agentctl config normalize # rewrite agents.yaml to the canonical schema (legacy→new, one-shot) + commit
agentctl inject <agent>|self <text> # inject a message into an agent's live session as a user turn (self-scope; admin/root → any agent)
agentctl notify <agent>|self|admin <text> [--file <path>]... [--format md|text] [--silent] # OUTBOUND msg (+attachments) to the owner over its channel (self-scope; admin/root → any; `admin` → fleet admin)
agentctl chrome restart|status # self-scope: restart/inspect your OWN CDP Chrome (browser@<self>) — recover a stuck browser without root
agentctl apps list|validate [<agent>|self] [--json] ; apps bundle <agent>|self <id> # self-scope read plane for user-authored cockpit mini-apps (~/.claude/apps); validate = why one silently doesn't appear
agentctl agents add <name> [--channel webview|telegram|teams|slack] [--user-email <e>] [--bot-token <t>] [--owner-id <id>] [--owner-aad <id>] [--owner-email <e>] [--admin|--no-admin] [--runtime claude|pi] [--api-key <key>] [--provider groq|anthropic|openai|openrouter] [--model <id>] [--workspace-remote <url>] [--json]
# the routing key follows the channel: webview → --user-email (mandatory, no bot token), telegram → --owner-id/pairing + --bot-token, teams → --owner-aad, slack → --owner-email. --user-email is additive on ANY channel and gives that agent a cockpit too. (see ../guides/channels.md)
agentctl agents list | restart <name>|all | logs <name> | trigger <name> <job> | remove <name> --yes
agentctl agents restart-self # self-scope (used by the agent's own post-login restart)
agentctl agents stop-self # self-scope: stop + disable the caller (used by remote migrate --cutover)
agentctl agents admin <name> [--revoke] # grant/revoke the admin ROLE (operator scope); additive — any number of agents may hold it
agentctl agents set <name> <key> <value> # declaratively set a ledger scalar (owner_id|chat_id|model|skill_model|provider|runtime|workspace_remote|user_email|fanout|channel|endpoint_base_url|foundry_resource|foundry_base_url|cost_cap_usd); apply reconciles it (cost_cap_usd needs no apply — see ../operating/operations.md "Cost cap")
agentctl agents remote add <url> <name>|self | remove # git remote + deploy key for an agent's ~/workspace backup repo (prints the pubkey to register)
agentctl agents validate [<name>|--all] [--json] # lint recorded agents vs every add-time invariant (secrets, catalog refs, teams routing/reachability)
agentctl agents skills add|remove|list <skill> <name>|* # add|remove activate for an agent; list = catalog + defaults
agentctl agents skills catalog add <name> [--from <dir>] [--content-stdin] # AUTHOR a new skill INTO the overlay
agentctl agents subagents add|remove|list <sub> <name>|* ; subagents catalog add <name> [--from <file>]
agentctl agents cron add|remove|list <job> <name>|* ; cron catalog [--json] # per-user timers
agentctl agents hooks add|remove|list <hook> <name>|* ; hooks catalog add <name> --event <E> (--script <p>|--command <c>) # Claude hooks (registry + repo overlay)
agentctl mcp catalog list # selectable MCPs + source + pinned version + auth
agentctl mcp catalog add <name> --npm <pkg@ver> | --uvx <pkg==ver> # bridged: pin exact version, vendor lock; lands in the OVERLAY by default (+commit), --into overrides
agentctl mcp catalog add <name> --scope shared|per_user --auth none|env|oauth [--env KEY...] [--scopes URL...]
agentctl tool list|add|remove <tool> <name>|self # wire pi CLI tools (pi's MCP replacement) for an agent
agentctl tool catalog list | tool catalog add <name> [--from <dir>] # pi tools = deb-wrapped MCPs + native tools; add authors a native one into the overlay
agentctl tool wrap-mcp <name> [--cmd '<stdio>'] [--env KEY...] [--description <d>] # author a catalog tool that fronts an MCP via mcptools
agentctl mcp catalog upgrade <name> --to <ver> # the only way a version moves: re-lock (--to required)
agentctl mcp catalog remove <name> # drop from catalog (must be deactivated everywhere)
agentctl mcp add|remove <mcp> <name>|self # activate/deactivate for an agent; socket materializes here
agentctl catalog browse [--json] | catalog remove <kind> <name> # browse every category's items (origin overlay/deb + defaults); remove an OVERLAY item
agentctl catalog add <skill|subagent|mcp|tool|hook> <name> [category flags…] # ONE entry point over the five per-category authoring verbs; dispatches, so every flag above still applies (--from/--content-stdin/--zip-stdin/--force, --event, --npm/--uvx/--url). cron is NOT authorable — a cron catalog item is a shipped unit+timer template pair; use `cron add` to SCHEDULE work
agentctl teams-gateway install|uninstall|status # host Teams channel gateway (webhook :3978 + WS router)
agentctl slack-gateway install|uninstall|status # host Slack channel gateway (Socket Mode + WS router)
agentctl webview-gateway install|uninstall|status [--port 8735] [--force] # host WEB-VIEW channel gateway (WS router; channel=webview agents dial in over the fleet net → `top --web` becomes the live cockpit). CO-INSTALLS the host-operation coordinator; `uninstall` removes both and refuses while non-terminal host operations exist (--force abandons them)
agentctl hostop-coordinator status [--json] | serve # the durable host-operation coordinator: root-owned SQLite (WAL) at /var/lib/agentctl/hostops + a 0600 unix socket at /run/agentctl/hostop-coordinator.sock. Installed BY `webview-gateway install` (one marker, one lifecycle) but runtime-INDEPENDENT of it — a cockpit/gateway restart must never stop it. `serve` is the systemd ExecStart entrypoint
agentctl hostop-coordinator operation [<op-id>] [--json] # one durable operation + its audit trail + its TARGET CONNECTION HISTORY, or the 20 most recent. The gateway records EVERY host operation it relays here (source, target, verb, args digest, authorization snapshot) and every change to the target's dial-in connection (a monotonic "epoch" per connection instance) — informationally, in parallel with the unchanged transient relay, which still carries the actual work. Rows marked `RECORDED ONLY` are retry DECISIONS the coordinator wrote down and did NOT act on: it cannot dispatch or redeliver until the target-side receipt path exists
agentctl hostop-coordinator submit <target> <verb> [args…] # PROVISIONAL/gateway-host only: durably RECORD an operation without running it (proves submit + idempotency end to end). NOT the way to run work: it submits and stops, where `host operation run` submits AND follows AND returns the operation's real exit code. The awkward spelling is deliberate and permanent — a command that records an operation without running it must not be reachable under a name an operator can mistake for the one that runs it
agentctl hostop-worker run <operation-id> # NOT A COMMAND TO RUN BY HAND: the systemd ExecStart of agentctl-hostop@<operation-id>.service, one transient unit per durable host operation. Its ONLY input is the root-owned 0600 spec at /var/lib/agentctl/hostops/target/ops/<operation-id>/spec.json that agentd wrote after persisting the operation's receipt — the verb and its arguments never appear on a command line, so nothing coordinator-supplied is ever interpolated into one. It records the receipt, its own execution identity (unit, pid, cgroup, boot id) and a terminal state; a worker stopped mid-flight records `interrupted`, never `failed`. LIVE since the Phase 4 cutover: agentd starts one per dispatched `apply`/`host upgrade`, and the executor runs the verb for real
agentctl webview-login install|uninstall|serve # host web-view login proxy (generic auth driver → cockpit; needs WEBVIEW_HMAC_KEY + driver cfg in sops; binds :8736)
agentctl auth-broker install|uninstall|status # OPTIONAL host OAuth callback broker (:8734) — makes `login google` hands-free
agentctl login <provider> <name>|self [--code <code>] # claude | google | ms365 (host-side or self-serve)
agentctl secret set|get|list|remove <KEY> [--value V] [--agent <name>] # store; --agent = per-agent (agents.<name>.<KEY>); value on stdin
# an agent's bare `secret get <KEY>` is self-scoped: it returns any secret VISIBLE to it (own per-agent + shared) to stdout — the canonical runtime fetch, no value on disk. writing shared/other, and `secret list`, stay operator-only.
agentctl cred encrypt <name> <cred-id> # systemd-creds for a unit credential (general-purpose)
agentctl host cron add|remove|list <job> # host-scope timers (shipped catalog jobs: load-monitor, runaway-reaper, job-watchdog)
agentctl host rename [<new-name>] [--from <old-name>] # rename this host across the fleet ledger (hosts key, agent-host pins, teams_gateway) after an OS hostname change
agentctl host upgrade # upgrade the agentctl package on THIS host (apt --only-upgrade / dnf upgrade --refresh); run `apply` afterwards
agentctl host operation run <target> <verb> [args…] # run an allowlisted verb on ANOTHER fleet host, as the admin agent, through the gateway. `apply`/`host upgrade` are submitted DURABLY: it prints `operation <id>`, then follows the coordinator's journal and exits with the operation's real exit code. Killing the follow does not touch the operation — resume it with `host operation logs <id> --follow`. RENAMED from `host op`, which now refuses and names this command
agentctl host operation status <operation-id> [--json] # one durable operation's record AND the state of its stored output (coordinator-backed). The exit code is the VIEW's, never the operation's
agentctl host operation logs <operation-id> [--follow|-f] [--after <sequence>] # replay a durable operation's output from the coordinator. `--follow` keeps reading and resumes at THIS host's last delivered cursor (`--after 0` replays from the start; `--after <n>` picks an explicit one). A SIGKILLed follower loses nothing
agentctl cron add <label> --schedule '<OnCalendar>' (--skill <name>|--script <abs>) [--host | --agent <name>] # durable timer: self-scope, --host (admin), or --agent (operator authors FOR a named agent)
agentctl cron remove <label> [--host | --agent <name>] # remove a generated cron
agentctl cron list [--host] # your crons (the admin agent / root sees every agent's; --host for host crons)
agentctl remote add <url> | remove # fleet /opt git remote + deploy key (prints the pubkey to register)
agentctl fleet reachable [--json] | seed | adopt [--force] | sync # cluster-onboarding primitives (the top web wizard sequences these)
# ── monitoring & observability — read-only, route locally ──
agentctl top --json [--watch] # the raw Snapshot for THIS host — one object, or a 2.5s stream
agentctl top --web [addr] --gateway # the headless host service: serve the cockpit + accept agent dial-ins
# THE FLEET VIEW IS A COCKPIT MINI-APP. There is no terminal dashboard and no SSH
# multi-host collector: open the cockpit as an admin and the `fleet` app shows every agent
# on the host, live, including agents on OTHER hosts that dial into this gateway.
# `--json` is the wire format that view is built from (the gateway op-relays it into each
# dialed-in agent), which is why it is the one form of `top` that survives.
# Retired: --once, --snapshot, --width, --no-web, --host, --hosts-file. Each now prints
# what replaced it rather than doing something subtly different.
agentctl agents status [<name>] # per-agent: unit state, uptime, NRestarts, mem/cpu, creds + MCP status
agentctl health # fleet roll-up; non-zero exit on any problem
agentctl doctor [<name>] # actionable diagnostics + suggested fixes
agentctl logs <name>|self [-f] [--since …] [--until …] [--grep …] [--json] [-n N] # an agent's unit journal — delegates to `unit logs` (root, via agentd), so it is admin scope even for your own unit
agentctl timers [<name>] # schedule, next fire, last run + result
agentctl creds [<name>] # per-(agent, provider) login health: valid | expiring | dead
agentctl usage [<name>] [--days N] # token + estimated-cost rollup
agentctl tail|def|session-stream|history|ws … <agent> # read backends the cockpit consumes over the socket / the agent dial-in (session feed, definitions, transcript stream, timeline scroll-back, workspace)
# ── subsessions — headless, web-view-only agent sessions (self-scope) ──
agentctl subsession start --title <name> [--resume <sid>] <agent>|self # spawn a NAMED headless sub (web-view channel; you address it by the name)
agentctl subsession prompt <name> <text> <agent>|self # drive it — deliver a turn to the sub's own ingress
agentctl subsession tail <name> <agent>|self # its transcript as JSON events
agentctl subsession resume <name> <agent>|self # re-launch a stopped/idle sub (re-checks the running cap; refuses a sub with no transcript)
agentctl subsession clear <name> <agent>|self # stop the unit, KEEP the transcript (resumable)
agentctl subsession rename <name> <new-name> <agent>|self # retitle it — the name is the handle; sid, transcript and unit are untouched (refuses a name already taken)
agentctl subsession list <agent>|self [--with-main] # the registry (id/title/kind/status/last_active/resumable) as JSON; --with-main adds the synthesized main-lane entry (the cockpit's shape); cross-agent goes through agentd (root)
agentctl subsession reap [--ttl <dur>] # host janitor (timer): stop idle subs past the TTL
The seam invariant¶
Behind all of the above: append-only files on disk are the source
of truth, frames are the live push of events already in that log, and every durable
frame must be reconstructible from the log. ../concepts/architecture.md states it once,
with the exempt list (typing signals, token-delta previews, browse RPC, handshakes,
client view state), the precise one-writer rule for sends, and why session kind is
recorded at creation rather than inferred.