Skip to main content
Docs navigation

Runner CLI

The emisar binary on each host is two things: the daemon that executes gated actions, and the operator's local toolbox for inspecting packs, reading the journal, verifying the audit chain, and setting up signed dispatch. This page is the reference for its operator verbs — grouped the way the binary groups them — with the key flags and whether each command only reads or changes the host.

web-01
$ sudo emisar doctor          # preflight: config · credential · packs · binaries · service
$ sudo emisar events tail -f  # the host-side security log, live
$ sudo emisar pack list
ID            VERSION  ACTIONS  HASH                 DESCRIPTION
debugging     0.2.10   31       sha256:ae3b9a7c8f11  General-purpose Linux diagnostics + remediation…
linux-core    0.3.19   34       sha256:8d0ba17ca0e4  Read-only Linux diagnostics + narrow service control…
systemd-deep  0.1.10   24       sha256:84f814476179  Deeper systemd state + operator surface…

Running the CLI#

On an installed host the binary lives at /usr/local/bin/emisar and every subcommand auto-discovers its configuration, so you rarely pass --config. The config and the cached token are root-owned, and the packs directory is root-owned too, so run commands with sudo. Running emisar with no arguments prints a Paths footer showing exactly where this host keeps its config, packs, token, and journal.

  • --config <path> — the config.yaml to load. Defaults to $EMISAR_CONFIG, then the /etc/emisar/config.yaml install location, then a per-user ~/.config/emisar/config.yaml.
  • --packs-dir <dir> — extra pack search directories, overriding the config's paths.packs (repeatable). Lets a read command work without a full config.
  • --json — machine-readable JSON where a command supports it: action list/describe, pack list/info, state, and the signing verbs.

Where an installed host keeps things — the installer's defaults; config keys or the install flags below move them:

What Default path
Config /etc/emisar/config.yaml
Packs /etc/emisar/packs
Data dir (identity, dispatch log, signing nonces) /var/lib/emisar
Cached runner token /var/lib/emisar/token
Events journal /var/log/emisar/events.jsonl

The connect daemon#

emisar connect is the long-running daemon: it dials out over a TLS websocket, advertises this host's actions, executes incoming dispatches, streams redacted output back, and journals every attempt locally. There is no inbound listener on the host. On the first connect it trades the enrollment key (from the env var named by cloud.enrollment_key_env, by default EMISAR_ENROLLMENT_KEY) for a cached per-runner token, so the key can be unset afterward.

You normally don't run connect by hand — the installer registers it as a supervised service (emisar.service under systemd, a LaunchDaemon on macOS) running as the emisar user. Manage it through the init system. reload sends SIGHUP, which re-reads packs and rebuilds the signing verifier and re-advertises the catalog on the live connection — in-flight runs keep executing across it.

shell
$ sudo systemctl status emisar     # is the daemon up?
$ sudo journalctl -u emisar -f      # stream its logs
$ sudo systemctl reload emisar      # re-read packs + signing config (SIGHUP)
$ sudo systemctl restart emisar     # full restart

On a --no-service host (a container, CI box, or cloud shell) there is no unit; run the daemon yourself with emisar connect --config /etc/emisar/config.yaml. The full fleet lifecycle — groups, enrollment keys, pack credentials — is in the runner fleet guide.

Actions#

Inspect the catalog this host advertises, and run one action locally while you're debugging a pack. emisar actions is an accepted alias for the group.

Command What it does Mode
emisar action list Every action this host advertises — id, pack, kind, risk, title. Reads only
emisar action describe <id> The full JSON definition of one action, including its argument schema. Reads only
emisar action run <id> Execute one action on this host now, for debugging a pack. Runs on the host

Flags on action run:

  • --arg key=value — one argument, repeatable. Numbers, true/false, and JSON arrays/objects are decoded; everything else stays a string.
  • --reason "…" — free-text reason recorded on the local event.
  • --timeout 30s — override the action's timeout, clamped to its own min/max.
  • --stream — stream stdout/stderr live instead of returning buffered JSON.
action run bypasses the gate.
It executes on the host immediately, without cloud policy, approvals, or signature checks — it's for debugging a pack you're authoring, not the dispatch path. The governed path is a run dispatched from the control plane or an MCP client, where admission, policy, and audit all apply.

Packs#

emisar pack (alias packs) manages this host's action catalog — the versioned bundles of action schemas it may execute.

Command What it does Mode
emisar pack suggest Recommend packs for what's running on this host (binaries, processes, ports). Reads only
emisar pack list Installed packs with version, action count, and content hash. Reads only
emisar pack info <id> A pack's setup: required binaries, the env vars its actions read, a verify command. Reads only
emisar pack validate <path> Validate a pack directory on disk without loading it into the runner. Reads only
emisar pack install <name|path|url> Fetch, validate, hash-verify, and install one pack into the packs dir. Changes the host
emisar pack update [id...] Update installed packs to the registry's current versions. Changes the host
emisar pack uninstall <id> Remove an installed pack (aliases: rm, remove, delete). Changes the host

Flags on pack install:

  • --hash sha256:… — pin the exact content hash the portal advertises; the install aborts on any mismatch, so a tampered or mismatched copy never reaches the runner.
  • --force — overwrite a pack already installed under the same id.
  • --registry <url> — install from your own registry (or set EMISAR_PACKS_REGISTRY); defaults to the public registry at emisar.dev.
  • --dest <dir> — the packs directory to install into; defaults to the config's first paths.packs entry.
shell
# install one pack, pinned to the hash the portal advertises
$ sudo emisar pack install redis --hash sha256:9f2c…
# update every installed pack (or name a few); preview with --dry-run
$ sudo emisar pack update
$ sudo emisar pack update redis postgres --dry-run
# validate a pack you are authoring, before you trust it
$ emisar pack validate ./my-pack

install, update, and uninstall signal a running daemon to reload — the same SIGHUP as systemctl reload emisar — so changed actions re-advertise without a restart or a dropped run; when no live daemon is reachable the CLI prints the manual reload command instead. Installing bytes doesn't trust them: a changed hash lands as pending on the Packs page until someone trusts it. Names accept name=version to pin a specific published version. See the pack reference for what goes in a pack.

Diagnose and audit#

When a runner won't connect or an action fails, these read-only verbs answer why without opening a cloud session.

Command What it does Mode
emisar doctor Offline preflight: config, credential, packs, action binaries, service, and control-plane reachability. Exits nonzero if any check fails. Reads only
emisar state Print the runner_state JSON this host would advertise to the control plane. Reads only
emisar events tail Print recent journal events; -f follows new ones (--lines sets the count, default 50). Reads only
emisar events cat / grep Dump the whole journal, or filter it by --action, --caller, or --event. Reads only
emisar audit verify [path] Re-derive a journal file's SHA-256 hash chain and report breaks; --all walks rotated siblings. Reads only
shell
$ sudo emisar doctor              # offline preflight — run this first
$ sudo emisar events tail -f      # the local journal, live
$ sudo emisar audit verify --all  # re-derive the journal hash chain

doctor never opens a cloud session and one failing check never aborts the rest, so a single run surfaces every problem at once. audit verify exits 0 when the chain is intact and 1 on the first break, naming the line and event id; --all walks the active file plus every rotated .N sibling, each chain verified independently. There's also emisar state check-dispatch-log, which the installer runs before an upgrade to confirm the durable dispatch log still loads.

Tamper-evident, not tamper-proof.
audit verify proves the on-host journal hasn't been edited in place, but root on the host can truncate the tail and re-chain it. The record that can't be forged from the host is the off-host cloud audit — stream both and cross-reference them. See Audit & SIEM.

Signed-dispatch keys#

emisar signing sets up client-attested dispatch: an enforcing runner then requires a CA-signed certificate on every action, so a compromised control plane can relay a dispatch but never mint one. Everything here is offline — keys are printed to your terminal and sent nowhere.

Command What it does Mode
emisar signing init One-shot on-ramp: mint a CA, a leaf key, and a cert; print the runner config, the offline CA private key, and the two MCP env vars. Prints key material
emisar signing new-ca Mint just the offline CA keypair — the root of trust (rarely). Prints key material
emisar signing new-cert Sign a short-lived operator cert against the offline CA (routinely, as certs expire). Prints key material
  • --ttl 24h — the cert's validity window; accepts Go durations plus long-form 30d/1y (default 24h). Short is better — a long TTL trades away revocation granularity.
  • --scope group=prod,env=edge — bind the cert to matching runners; empty means any runner that trusts the CA.
  • --ca-id and --ca-key (both required on new-cert) — the CA id, which must match the runner's trusted_cas, and the offline CA private hex seed from new-ca. The key is read locally and never transmitted.
  • --key-id and --pubkey — label the leaf key, or supply your own leaf public key instead of minting one.
shell
# one-shot: prints the runner config block, the offline CA key, and the MCP env vars
$ emisar signing init --ca-id acme-2026 --scope group=prod --ttl 24h
# later, renew a cert against the offline CA (no runner reload needed)
$ emisar signing new-cert --ca-id acme-2026 --ca-key <offline CA key> --scope group=prod

The generated EMISAR_SIGNING_KEY and EMISAR_SIGNING_CERT are the env vars your MCP client carries; the CA private key stays offline, never on a runner or the control plane. The full model — enforcement, scoping, rotation, and revocation — is in Signed dispatch.

Install, update, and removal#

The binary is installed and upgraded by the same script; it isn't self-updating. Re-running the installer upgrades in place and preserves config and packs. It requires systemd on Linux (use --no-service for hosts without a real init).

shell
# install or upgrade in place (config + packs preserved)
$ curl -sSL https://emisar.dev/install.sh | sudo bash
# pin a version, or provision unattended with a fixed pack set
$ curl -sSL https://emisar.dev/install.sh | sudo bash -s -- --version runner-v0.3.0
$ curl -sSL https://emisar.dev/install.sh | sudo bash -s -- --yes --packs linux-core,postgres
  • --version TAG — install a specific runner release (runner-vX.Y.Z, vX.Y.Z, or bare X.Y.Z); default is the latest.
  • --yes — skip prompts; requires an explicit --packs (or EMISAR_PACKS) so an unattended run never guesses.
  • --packs LIST — install exactly these packs, comma- or space-separated, with no host detection (for cloud-init, Packer, Ansible).
  • --no-start — install and enable the service but leave it stopped; --no-service — binary only, with no service user, unit, or activation.
  • --uninstall — stop the service and remove the binary, unit, and cached token, keeping config, data, and logs; --purge with it also deletes config, data, logs, and the service user.
  • --bin-dir · --etc-dir · --data-dir · --log-dir · --user — override the install locations and the service account.
shell
# remove the binary, service, cached token, and identity — keep config + journal
$ sudo bash install.sh --uninstall
# …or delete everything, including config, data, and logs
$ sudo bash install.sh --uninstall --purge

After uninstalling, delete the runner in the dashboard — its run history and audit events survive the host. Removing a runner cleanly, including manual identity resets, is covered in the runner fleet guide.