Skip to content
RouteSpear
Documentation
Documentation menu

Agent CLI reference

Every flag on the routespear-agent binary and every rsctl subcommand, with the defaults and the reasoning behind them.

Most agents are deployed from the install pack the portal generates, which writes a command line for you and wraps it in a service. This page is for the cases where that does not fit: a container image with its own entrypoint, a configuration-management module that owns the unit file, an air-gapped host that will never see the portal, or a foreground run you are watching while you work out why a measurement looks wrong.

Everything here is flags. routespear-agent has no subcommands; it takes flags only, and a positional argument is refused rather than ignored, because a mistyped flag that silently became an argument is a deployment that looks healthy and measures the wrong thing. The companion tool rsctl is the one with subcommands, and it is covered further down.

Worked examples

A foreground run against a control plane. The configuration arrives over HTTP, the signing key is pinned so that terminating TLS as the control plane is not enough to configure this agent, and enrollment supplies the credential the receiver derives this agent’s identity from:

Linux

./routespear-agent \
  -config-url https://ingest.example.com:9080/v1/agent/config \
  -config-signing-key "$(cat /etc/routespear/config-signing.pub)" \
  -enroll-bootstrap /etc/routespear/bootstrap.json \
  -spool /var/lib/routespear/spool \
  -ingest ingest.example.com:9443 \
  -ingest-ca /etc/routespear/ca.pem \
  -log-format text

macOS

./routespear-agent \
  -config-url https://ingest.example.com:9080/v1/agent/config \
  -config-signing-key "$(cat /etc/routespear/config-signing.pub)" \
  -enroll-bootstrap /etc/routespear/bootstrap.json \
  -spool /var/lib/routespear/spool \
  -ingest ingest.example.com:9443 \
  -ingest-ca /etc/routespear/ca.pem \
  -log-format text

Windows

$dir = "$env:ProgramData\RouteSpear\Agent"
.\routespear-agent.exe `
  -config-url https://ingest.example.com:9080/v1/agent/config `
  -config-signing-key (Get-Content "$dir\config-signing.pub" -Raw).Trim() `
  -enroll-bootstrap "$dir\bootstrap.json" `
  -spool "$dir\spool" `
  -ingest ingest.example.com:9443 `
  -ingest-ca "$dir\ca.pem" `
  -log-file "$dir\agent.log" -log-format text

-log-file is worth adding on Windows even in the foreground, because it is required once the same command line is registered as a service: Session 0 has no console for stderr to reach.

A fully local run with no control plane and no ingest tier at all. The configuration is a file, and every record is written as one JSON object per line. This is the shape to reach for when you want to see what the agent produces before deciding where to send it:

Linux

./routespear-agent -config agent.json -ndjson records.jsonl -log-format text

macOS

./routespear-agent -config agent.json -ndjson records.jsonl -log-format text

Windows

.\routespear-agent.exe -config agent.json -ndjson records.jsonl -log-format text

-ndjson - writes to stdout instead, which composes with jq. Neither form needs privilege, a spool, or a network path to anything but the targets.

Finding out what a host will actually give you, before you decide it is worth a placement:

Linux

./routespear-agent -print-capabilities

macOS

./routespear-agent -print-capabilities

Windows

.\routespear-agent.exe -print-capabilities

This assembles the agent as far as opening the preferred probe transport and then exits without measuring, so it needs no configuration and no record destination, but it does need to run as the user the agent will run as, since whether a raw socket can be opened is a property of that user rather than of the host.

Identity and labelling

Flag Default What it does
-agent-id the hostname This agent’s identifier for its own bookkeeping.
-namespace empty Advisory network scope hint (routing, VRF or site) carried on records.
-labels empty Agent-wide record labels, as key=value pairs separated by commas.

The identifier defaults to the hostname rather than to a generated value because a generated one would change on every restart, and the agent’s own logs would then be uncorrelatable across exactly the window you most want to read. A host with no resolvable hostname has to be told who it is, and refuses to start until it is.

The identifier does not reach a record. Identity on the wire is derived by the receiver from the credential the connection authenticated with, which is what stops one agent from filing records as another. -agent-id is what the agent calls itself locally, and what -enroll-name and -enroll-fingerprint default from.

-namespace is a hint and is labelled as one. Two agents in different VRFs can legitimately measure the same address and get different answers, and the network scope is how a reader tells those apart; nothing in the agent enforces it.

Configuration

Flag Default What it does
-config empty Path to the configuration document; watched for changes and reapplied atomically.
-config-url empty Control-plane configuration endpoint, for example https://ingest.example:9080/v1/agent/config.
-config-poll 5s How often the configuration source is checked for a change.
-config-signing-key empty Base64-encoded ed25519 public key the served configuration is verified against. Required with -config-url.
-config-cache a file beside -enroll-state Where the last verified control-plane configuration is cached. Only meaningful with -config-url.

One of -config or -config-url is required, and giving both is refused. An agent with no configuration document has nothing to measure, and defaulting to an empty one would produce a process that starts cleanly, logs nothing unusual, and does no work.

-config-url without -config-signing-key is also refused. An unpinned key would mean anything able to terminate TLS as the control plane could configure this agent, and the signature is what reduces the control plane to a delivery mechanism for a document you can verify independently of the transport that carried it. The key comes from whoever ran routespear-ingest -generate-config-signing-key.

-config-cache exists for the restart during an outage. Without it, an agent that restarts while the control plane is unreachable has no configuration and measures nothing; with it, the last document that passed signature verification is reapplied and the agent carries on. Only verified documents are ever cached, so the cache cannot become a way to persist something the agent would have refused.

The five-second poll default is a compromise. A local file edit should take effect while you are still looking at the terminal, and a stat plus a hash of a document this size costs nothing, while still being long enough that a whole fleet polling a control plane does not need a separate interval.

Enrollment

Flag Default What it does
-enroll-bootstrap empty Path to the bootstrap file the portal Enrollment page (or routespear-admin secret create -bootstrap) writes. The preferred way to supply an enrollment secret.
-enroll-secret empty Enrollment secret inline, as clientid:secret.
-enroll-client-id empty Public half of the enrollment secret, when the private half comes from the configured secret backend under the reference routespear-enrollment.
-control-plane from the bootstrap file Control plane base URL to enroll against. Overrides the endpoint recorded in -enroll-bootstrap.
-enroll-state an enroll directory beside -spool Directory holding the issued credential.
-enroll-name -agent-id Operator-facing label for this agent’s control-plane record.
-enroll-fingerprint a hash of -agent-id Stable local identity the control plane keys this agent’s record on.
-enroll-insecure-skip-verify false Do not verify the control plane’s TLS certificate.

-enroll-bootstrap is preferred over -enroll-secret for one concrete reason: on Linux any local user can read another process’s argument list from /proc, so a secret on the command line is a secret shared with every account on the box. The agent warns rather than refuses, because an inline secret is genuinely the right shape for a systemd unit using LoadCredential.

Enrollment flags without -ingest are refused, and so is combining them with -ingest-cert. The first is a half-finished command line: enrollment exists to authenticate shipping, so a credential that is never presented to anything is almost certainly not what was meant. The second is worse than redundant, both would work, and which one authenticated a given push would depend on the receiver’s TLS policy rather than on anything you wrote down.

Enrollment is lazy, and that is the point

The agent does not block at startup waiting for the control plane. It constructs its enrollment session during assembly, performing no network I/O at all, and the first real enrollment happens inside the credential callback on the shipper’s first push. Everything in between (opening the transport, applying the configuration, scheduling tests, measuring, spooling) proceeds regardless of whether the control plane is reachable.

The alternative would be to enroll at startup and refuse to start if it failed, which for a process whose whole purpose is to measure networks that break is backwards. The host that most needs an agent running is the one that cannot currently reach anything. An agent started in the middle of a partition measures, spools, and enrolls when the partition heals, with no operator intervention and no special case anywhere in the code.

The cost is that a bad enrollment secret is discovered on the first push rather than at startup, so everything checkable locally is checked early instead: the bootstrap file parses, the endpoint is a URL, and an already-expired secret is reported as an error in the startup log rather than left for later.

The issued credential lands in -enroll-state, which defaults to an enroll directory beside -spool, next to the spool rather than inside it, since the spool directory is scanned for segment files and an unrelated file in it is an invitation for a future reader to mistake one for the other. It is written with owner-only permissions, because that credential is this agent’s identity. With no -spool at all, -enroll-state becomes required: the credential has to persist somewhere, or the agent would enroll afresh on every restart and exhaust its tenant’s enrollment rate limit.

-enroll-fingerprint is worth setting deliberately in one situation. It defaults to a hash of the agent identifier, which defaults to the hostname, and the control plane keys the agent record on it, so two hosts presenting the same value are one agent whose records interleave. A cloned VM image that kept its hostname is exactly that case, and a cloud instance id or a TPM-derived value is a better answer where one is available.

-enroll-insecure-skip-verify presents a fleet-wide enrollment secret to whoever answers, which is why it is for a local stack and tests only. A secret captured that way enrolls an attacker’s agent into your tenant.

Shipping to ingest

Flag Default What it does
-ingest empty Ingest tier address in gRPC target syntax. Empty disables shipping entirely.
-ingest-insecure false Ship without transport security.
-ingest-ca empty PEM bundle the receiver’s certificate is verified against.
-ingest-cert empty Client certificate for mTLS.
-ingest-key empty Private key for -ingest-cert.
-ingest-server-name empty Override the name verified in the receiver’s certificate.

-ingest requires -spool. The shipper drains the write-ahead spool rather than sending records as they are produced, and that indirection is what makes a delivery outage survivable instead of lossy.

-ingest also requires one of -ingest-ca, -ingest-cert, or an explicit -ingest-insecure. Measurement data describes the topology of your network, and shipping it in the clear is a decision rather than a default, so the flag exists, and you have to write it down. -ingest-cert and -ingest-key go together, and neither is accepted alone.

-ingest-server-name is for the case where the address you dial and the name in the receiver’s certificate legitimately differ, such as a load balancer address or a hostname resolved differently inside the network being measured.

Spool and local output

Flag Default What it does
-spool empty Durable write-ahead spool directory. Required to ship, and survives a crash.
-spool-max-bytes 536870912 (512 MiB) Cap on the whole spool.
-spool-fsync interval Durability policy: always, interval, or never.
-ndjson empty Optional JSON debug copy: 32 MiB per file, two backups (96 MiB total), or - for stdout.

At least one of -spool and -ndjson is required, and an agent given neither is refused at startup rather than defaulted to a discard sink. An agent that measured correctly and threw the results away looks healthy in every log line it emits, and its absence would be discovered downstream as missing data with nothing pointing back at the host.

The spool cap is not optional and not unbounded, because an unbounded spool turns a shipping outage into a full disk, and a full disk is a worse incident than a gap in a time series, since it takes the rest of the host with it.

-spool-fsync interval is the default because per-record fsync costs more than the durability it buys at this scale: the records at risk are the last fraction of a second’s worth, and an agent that syncs on every record is an agent whose measurement cadence is bounded by disk latency. always is there for a host where losing a second of records genuinely matters, and never for a throwaway container where the spool is a buffer rather than a durability mechanism.

-spool and -ndjson compose, but install packs and the development Compose agent enable only the spool by default. NDJSON is a duplicate debugging stream, not an upload queue: successful uploads do not remove its records, and the agent does not replay it.

Updated agents bound file-based NDJSON output to 32 MiB plus two numbered backups (.1 is newest). Agent diagnostic logs use 10 MiB plus three backups. Each write stays whole; a diagnostic write larger than its file limit is refused without suppressing the separate spool write. Restart appends to a file within its limit. An oversized file or numbered backup from an older version is reduced to complete recent lines within the limit when opened. Older local diagnostic history is discarded, including when NDJSON is the only configured sink.

Existing installations must remove -ndjson from their launch arguments to stop writing the copy. Stop the agent before removing an old debug file; retain its spool and enrollment directories. The rsctl spool -drain -ndjson command is an explicit offline export and does not rotate away exported records.

For stdout/stderr, configure retention in the service manager. The development Compose agent definitions use four Docker log files of 10 MiB each; existing containers need recreation for that setting to apply. These diagnostic limits are separate from the spool cap and do not include binaries, browser caches, managed runtimes, or other application files.

Probe transport

Flag Default What it does
-transport auto Probe transport: auto, none, raw_icmp, linux_udp_recverr, unpriv_dgram_icmp, tcp_ttl, or windows_iphelper.
-families ipv4,ipv6 Address families to open transports for.

auto opens the best transport this host and this user will allow, and reports what it settled for rather than pretending. Naming a kind explicitly opens that one and fails if it cannot be opened, which is usually what you want on a host you care about: auto quietly succeeding with a worse transport is convenient during evaluation and undesirable in production, where finding out at startup is better than finding out from a measurement that could not resolve the difference you were asking about.

none runs everything that needs no probe socket (the check types, the self-reporting types) which is the right setting for an agent whose job is HTTP and DNS from a particular vantage point and which has no business holding a raw socket.

An address family the host does not have is not an error. The transport for it is simply absent, and what that costs is visible in -print-capabilities and in the capability report the control plane receives. See capabilities.

Rate limits and target policy

Flag Default What it does
-max-pps 200 Agent-wide probe rate ceiling, in the runner’s abstract cost units.
-max-per-target-pps 20 Per-target pacing rate.
-max-concurrency 256 Cap on simultaneous test executions.
-max-tests 2000 Cap on accepted test definitions.
-min-interval 1s Shortest measurement period this agent will accept.
-allow-targets empty Exhaustive CIDR allow list for measurement targets. Empty means anything not denied.
-deny-targets empty CIDRs refused even when allowed, added to the built-in list.

These are the ceilings a served configuration is checked against, and the asymmetry is deliberate: a configuration may lower any of them and can never raise one. A control plane throttling a single agent from the server is a legitimate instruction; a control plane raising an agent’s rate ceiling would be the first move in turning a well-placed measurement fleet into a distributed packet generator.

The -max-pps default of 200 cost units per second is roughly what a few dozen path tests at a one-minute cadence consume. It is well below what would be noticed as traffic anywhere, while still being high enough that shedding is unusual rather than constant.

The ceiling is checked twice, and the reason is worth knowing. A configuration whose declared aggregate demand exceeds the ceiling is refused up front, which is a failure you can act on; the runner’s own budget then holds at admission time regardless of whether the declaration was honest, which is what makes the ceiling safe against a configuration that under-declares its cost. Refusing up front matters because the alternative is an agent that accepts the configuration and then sheds continuously, so every series runs at some unpredictable fraction of its configured rate, which is indistinguishable in stored data from intermittent loss.

-max-per-target-pps exists so that the agent does not trip the ICMP rate limits it is measuring around. A hop that rations the ICMP it generates looks lossy to TTL-limited probes and flawless to a direct echo at the same instant, and an agent probing hard enough to cause that is reporting its own behaviour as a network fault.

-allow-targets is empty by default because a mandatory allow list cannot be pre-populated, the addresses an agent should measure are exactly what you are about to configure. It is worth setting on an agent whose control plane you treat as less trusted than the host, or on a public-facing agent.

-deny-targets is appended to the built-in deny list rather than replacing it. Deny is evaluated first and wins, so a broad allow list can be carved out rather than enumerated around, and narrowing the allow list cannot silently re-permit something the defaults refuse. The built-in list covers only ranges that are never a legitimate measurement target:

Prefix Why
224.0.0.0/4, ff00::/8 Multicast is not a measurement target, and a probe to one is an amplification vector.
0.0.0.0/8, ::/128 This-network and the unspecified address.
255.255.255.255/32 Limited broadcast: one probe, every host on the segment replies.
169.254.169.254/32, fd00:ec2::254/128 Cloud instance metadata. Never a network measurement, and a control plane able to aim an agent here would be asking it to fetch credentials on its behalf.

Private ranges are deliberately not denied. Measuring the local gateway, the local subnet, and internal infrastructure is the primary use of this product, and a default that broke it would be turned off wholesale rather than narrowed.

Both lists apply to a resolved address as well as to a configured literal. A hostname cannot be decided when the configuration is read (its addresses are not known until it resolves, and they change afterwards) so the guarded resolver applies the same policy to whatever it actually resolves to. Without that second check, naming a host instead of an address would bypass the allow list entirely.

Adaptive probing and coalescing

Flag Default What it does
-adaptive true Run adaptive probing: burst a hop whose statistics departed from their own baseline.
-burst-share 0.25 Fraction of the rate ceiling adaptive probing may draw.
-coalesce-window 2s How long one measurement may be reused by another test presenting the same coalescing key. 0 disables sharing entirely.

Adaptive probing spends extra packets on the hop that just became interesting, so a degradation is sampled densely while it is happening rather than at the ordinary cadence that was chosen for a healthy path. Every sample taken inside a grant is marked as burst on the wire, so a consumer reads the density change as sampling rather than as a change in the network.

-burst-share is composed under the rate ceiling rather than added beside it, and cannot raise it. A quarter is a deliberate minority share: at a half, one fleet-wide event would halve every ordinary series on the box at the moment those series are the evidence, and the agent would be reporting a network problem that is partly its own scheduler.

Coalescing lets dns_check, http_check, tcp_connect, ssh_check, dhcp_check, pmtu_check and api_flow share one measurement when two tests present the same coalescing key within the window, instead of measuring twice. The two-second default is well under every check type’s own minimum interval, so sharing only ever catches jitter-staggered due times inside the same round and never spans a meaningful slice of either test’s cadence. Zero disables sharing, which is always safe and always costs more packets. rsctl check prints the coalescing key, which is the only place a human can see it.

Secrets

Flag Default What it does
-secrets-dir empty Directory holding one file per credential, named after its reference.
-secrets-env false Also resolve credentials from ROUTESPEAR_SECRET_* variables.

A credential reference in a configuration resolves from somewhere you nominate, never from the configuration itself. That is what keeps a database password out of anything the control plane serves and out of anything that gets logged.

-secrets-dir is the stronger of the two. Environment variables are inherited by every plugin child process the agent starts, so -secrets-env widens the blast radius of a credential to processes that have no business holding it; it exists because some deployment systems have no good way to place files.

With neither configured, every credential reference fails at test construction, so a test type that requires a secret genuinely cannot run on that agent, and says so in its capability report rather than failing per execution.

Capability grants

Flag Default What it does
-allow-insecure-tls false Permit an http_check or api_flow that asks to skip certificate verification.
-ca-bundle empty PEM bundle certificates are verified against instead of the platform store.
-allow-skip-host-key-verify false Permit an ssh_check that asks to skip host key verification.
-known-hosts empty Agent-local host key store, as a JSON object mapping host, host:port or [host]:port to accepted SHA256 fingerprints or public key lines.
-allow-dhcp-lease-consumption false Permit a dhcp_check in full_dora mode, which takes a real address out of the pool on every execution and releases it immediately.
-dhcp-lease-grant-reason empty Recorded with the DHCP grant, so an agent found consuming leases can be traced to the decision that allowed it.
-browser-plugin empty Path to the routespear-browser executable. Empty means no browser test types.
-browser-chrome-auto-download false If the plugin finds no Chrome, Chromium, or Edge on the host, fetch and cache a pinned Chrome for Testing build instead of failing. Ignored without -browser-plugin.
-browser-plugin-user empty Run the browser plugin as this local account or uid instead of the agent’s own. Required on Linux whenever the agent runs as root; on Ubuntu 23.10+ it also needs an AppArmor profile, see below. Refused on Windows.
-browser-allow-unsandboxed false Permit Chromium to run with its own sandbox disabled.
-browser-allow-insecure-tls false Permit a browser navigation to a host whose certificate does not verify.
-script-plugin empty Path to the routespear-script executable. Empty means no script test types.
-script-execute false Permit the script plugin to run signed bundles.
-plugin-sandbox-disabled false Run plugins unconfined, for a deployment that has already confined the agent itself.

-browser-plugin and -browser-chrome-auto-download can also be set remotely

Every other grant on this page is fixed for the life of an install: the only way to change it is to edit the command line and restart. The browser plugin is the one exception, and only through the portal’s Reconfigure dialog on an already-running agent, which fetches routespear-browser and writes a local override file next to the agent’s own binary before restarting it, not through the config poll every agent already refuses to take a grant from. This works because a Reconfigure browser change is dispatched the same way “Update agent” and “Restart agent” already are: a one-shot, operator- authenticated command for one named agent, at the same trust tier those two already sit at, not the fleet-wide document every agent polls forever. See the browser plugin’s RUNTIME.md for the override file’s exact location.

An explicit -browser-plugin on this process’s own command line always wins over that override file, checked before it is ever read. The one asymmetry this creates: an agent installed with -browser-plugin baked into its service definition cannot be switched to no browser plugin from Reconfigure alone; that flag has to come out of the service definition by hand. An agent installed through the download wizard’s browser picker, which never puts the flag on the command line, does not have this problem.

A root agent needs -browser-plugin-user

Chromium will not start its sandbox as uid 0; it prints Running as root without --no-sandbox is not supported and exits. That matters because a bare-metal agent under systemd normally is root: raw sockets, ebpf_flow, and ebpf_host all need privilege the browser does not. Left alone, every browser check on such a host fails at launch even though the plugin started and Chromium is installed.

-browser-plugin-user drops just that one plugin to an unprivileged account, which is what the plugin’s own container image already does by running as uid 10001:

routespear-agent \
  -browser-plugin /opt/routespear/routespear-browser \
  -browser-plugin-user routespear-browser

Either a local account name or a bare uid. A uid with no /etc/passwd entry is accepted, since that is a normal shape for a hardened deployment. 0 is refused: this flag exists to give up privilege, and root is the state that makes Chromium refuse in the first place.

The agent handles the consequences rather than leaving them to you, the plugin’s callback socket is chowned to that account (keeping mode 0700 rather than being widened, because anything that can reach that socket can forge records), root’s supplementary groups are dropped, and the auto-download cache is created and handed over. See the browser plugin’s RUNTIME.md for the reasoning in full.

Windows refuses this flag rather than ignoring it. Chromium’s sandbox has no objection to the account a Windows service runs as, so it is not needed there, and a plugin quietly running with more privilege than you asked for is the outcome worth failing over.

On Ubuntu 23.10 and later, that is only half of it

These releases set kernel.apparmor_restrict_unprivileged_userns=1, which stops an unconfined binary from creating the unprivileged user namespace Chromium builds its sandbox out of. Adding -browser-plugin-user on such a host does not make browser checks start working; it changes the message they fail with:

FATAL:zygote_host_impl_linux.cc] No usable sandbox!

That is not a regression and not a second unrelated problem. Chromium needs a sandbox it can construct, and neither uid 0 nor a uid barred from unshare can construct one; you are seeing the same requirement from the other side.

Grant that one capability with the AppArmor profile the repository ships:

install -m 0644 deploy/apparmor/routespear-browser-chrome \
    /etc/apparmor.d/routespear-browser-chrome
apparmor_parser -r /etc/apparmor.d/routespear-browser-chrome

The profile is declared flags=(unconfined), so it adds no confinement of its own: the browser is left exactly as restricted as before, plus the ability to build its sandbox. Prefer it to clearing the sysctl, which grants unprivileged user namespaces to every binary on the host in order to fix one, and to -browser-allow-unsandboxed, which turns the sandbox off for the one process whose entire job is loading pages you do not control.

Only a host using -browser-chrome-auto-download needs the profile. Point the plugin at a distro-packaged browser instead and the package’s own AppArmor profile already covers it.

Grants can never come from served configuration

This is the single most important behaviour on this page, and it is a structural property of the binary rather than a policy that could be relaxed by a setting.

Each of the behaviours above is expressed as two independent halves. A request lives in a test definition, and is therefore fully controlled by whoever controls the control plane. A grant is a value supplied by the code that builds the test registry, and every grant in this binary comes from the command line above. A request without its grant does not construct a test at all: it is refused with a message naming the grant it needed. There is deliberately no code path from a configuration document, a test definition, or a test’s parameters to any grant, and that invariant is asserted by tests at the assembly level as well as per package.

What this buys you is concrete. A compromised control plane cannot turn your agent fleet into a remote execution service, because switching on script execution requires editing the command line on each host. It cannot make your agents skip certificate verification and thereby make the verified flag on every HTTPS record meaningless. It cannot start consuming DHCP leases from a production pool. It cannot point the browser or script plugin at a different executable, which would be remote code execution written as a configuration field. Everything dangerous is decided by whoever administers the host, and a fleet of measurement agents sitting inside private networks is exactly the sort of instrument where that distinction matters.

-ca-bundle and -known-hosts are grants in everything but name, which is why they are in this section. A control plane able to supply a trust anchor could make any certificate or any host key verify, and the verified flag on every record would then mean nothing. Both are loaded at startup, so an unreadable bundle is a refusal to start rather than a per-execution failure that would look like the destination’s fault. A -known-hosts file with no entries is also refused: an empty store answers “this host is unknown” for everything, which reads in a log exactly like a store that loaded and silently verifies nothing.

Each flag is spelled as an allowance rather than as a mode, so reading a service unit’s arguments answers “what has this agent been permitted to do” without cross-referencing anything. The allowances that are on are also named in the startup log and in the capability report, which means most of an agent’s grant configuration can be audited from its own telemetry rather than only by logging into the host. -print-capabilities prints that list, and prints an explicit line saying none of them is reachable from a served configuration. Two things are not in that list: the plugin executable paths, which show up instead in the report’s plugins section, and -plugin-sandbox-disabled, whose effect is visible as the sandbox kind in rsctl capabilities.

-browser-allow-unsandboxed deserves its own warning. Chromium’s sandbox is load-bearing, and disabling it is only defensible in a throwaway container with a read-only root filesystem, dropped capabilities, and no secrets on disk.

Logging

Flag Default What it does
-log-level info debug, info, warn, or error.
-log-format json json or text.
-log-file empty (stderr) Rotate logs at 10 MiB per file, retaining three backups (40 MiB total).

Logs go to stderr or to a file and never into the record stream. An unreachable target is a record, not a log line, and an agent that logged measurements would have its log volume track the health of whatever it watches, so the noisiest possible moment would also be the moment the logs became useless.

JSON is the default because these lines are read by a log pipeline far more often than by a person. -log-format text is there for the person, and is worth setting for a foreground run.

-log-file is required when running as a Windows service, because Session 0 has no console. -install-service fills it in automatically when it is missing.

Windows service

Flag Default What it does
-install-service false Register this command line as the RouteSpear Agent Windows service, start it, and configure it to run at boot. Requires administrator.
-uninstall-service false Stop and remove the RouteSpear Agent Windows service. Files on disk are left in place.

Both flags are Windows-only. On Linux and macOS they parse and then refuse with a message pointing at the install-service and uninstall-service scripts in the install pack, which write a systemd unit or a launchd job. There is no -install-service behaviour on those platforms to configure.

Giving both flags at once is refused. -uninstall-service needs nothing else on the command line; -install-service records the rest of it.

Installation stores the current command line as the service’s ImagePath, with two modifications. The install and uninstall flags are stripped, so the service does not re-enter the installer on every start. And every flag whose value is a filesystem path (-config, -config-cache, -ndjson, -spool, -log-file, -ingest-ca, -ingest-cert, -ingest-key, -enroll-bootstrap, -enroll-state, -secrets-dir) is rewritten to an absolute path. A Windows service’s working directory is System32, so a relative -config would open the wrong file and the agent would fail at boot with nothing on a console to say why. If -log-file was not given, one is added beside the executable, for the same reason.

The service is created with automatic delayed start and with restart actions at five, fifteen and sixty seconds. Installation also adds an inbound ICMPv4 Time Exceeded firewall rule, because that is the one moment the process is known to be elevated and because a measurement agent that rewrote the host firewall on every start is not something you should have to discover. A failure to add the rule is reported and not fatal: the agent still runs, but Windows drops the message that TTL-limited probes depend on, so it falls back to IcmpSendEcho2 and cannot sample ECMP. Uninstalling removes the rule again and leaves the host’s firewall as it was found.

Diagnostics

Flag Default What it does
-version false Print version information and exit.
-print-capabilities false Print the capability report this agent would advertise, then exit without measuring.

-version prints one line carrying the build version, the platform, and the record schema version this build speaks. The build version also rides on every record, so a fleet running a mix of builds can be told apart in the data rather than only in a deployment tool. A local build honestly reports itself as dev with no commit.

-print-capabilities is the flag to run before deciding a host is a poor vantage point. It needs no -config and no record destination, and it prints:

  • the version, platform, Go version and build commit;
  • the preferred transport, the timing resolution actually achieved on this host, a degradation reason if there is one, and every transport that was graded;
  • the per-capability grading;
  • every test type this build registered, with whether it is available here, degraded here, or unavailable, and why;
  • each plugin and its version, or the reason it is unavailable;
  • the agent-local grants that are on, or an explicit statement that none is.

Nothing in that report re-detects anything. The transports, per-family support, privilege, flow-identifier control and timing profile all come from the single detection performed at startup, and the test types come from the registry this build actually populated. Two opinions about whether this host holds cap_net_raw is exactly the sort of disagreement that would make a capability report untrustworthy, so there is only ever one.

Exit codes

Code Meaning
0 Clean shutdown after a signal, or a diagnostic flag that completed.
1 The agent could not start, or a component failed unrecoverably.
2 The command line, or the local policy on it, is unusable.

The difference between 1 and 2 is the difference between “restart me” and “a human has to edit something”, and a supervisor unit is entitled to act on that. A flag the binary does not recognise, a flag combination that cannot produce a working agent, and -help all exit 2. A control plane that cannot be reached is neither: enrollment is lazy and shipping retries, so an unreachable control plane is not a startup failure.

The agent handles SIGINT and SIGTERM, and SIGTERM specifically because that is what a container runtime and a service manager send. An agent that only handled Ctrl-C would be killed hard on every ordinary restart and lose whatever the spool had not yet synced.

Running the binary with no arguments at all exits 2 and prints a short note saying that this is the agent rather than an installer, and pointing at the install pack’s scripts. That exists because the most common way to encounter this binary is to double-click it.

rsctl

rsctl is the companion CLI. It is how you verify what a host can measure, and what a particular test definition does, without privileges you do not need, a control plane, or the ingest tier. It is not a client for a running agent: it answers questions about the binary and the host it is invoked on.

rsctl -version prints the version and schema version. An unrecognised subcommand exits 1.

Two of the subcommands (view and adapt) run against a scripted network rather than a real one, and that is worth being blunt about before you read their output. They are for understanding what the tooling does with a diamond, a tunnel, a silent hop, a policing router or a route change. They are not measuring your production network and their numbers are not about it.

rsctl capabilities

What this host can measure and how well: the timing source and the resolution actually achieved here, every transport graded best-first, the preferred one, the per-capability grading, and every test type this build registered with its requirements and its placement verdict.

Flag What it does
-json Print the AgentCapabilities message as JSON.
-v Include capabilities and test types that are fully held.

By default only the degraded, absent and undetermined capabilities are printed, because someone running this is usually asking what is wrong, and a list where eight held capabilities push the one degraded capability off the screen answers a different question. The report comes from the same code path the agent puts on the wire, so rsctl cannot agree with your expectation while the agent reports something else.

rsctl has no secret backend and no plugin supervisor, and says so rather than inheriting an answer it cannot see. For the grants and plugins of a real agent, use routespear-agent -print-capabilities.

rsctl context

This host’s own network position: hostname and platform, gateways, the egress address and interface the stack would actually use per family, every interface with its addresses, resolvers and search domains, the associated wireless link, and the connected subnets that are the evidence behind a network scope.

Flag What it does
-json Print the LocalContext message as JSON.
-records Print every emitted record as JSON, including events.
-rounds Collections to take, so change detection is observable. Default 1.
-interval Wait between collections. Default 2s.

Every field this host declines to answer is listed at the end with the mechanism that refused it, which is the point of the command: “this laptop has no gateway” and “this laptop’s route table could not be read” look identical once either is rendered as a blank cell, and they lead to opposite conclusions.

-rounds exists for the vantage change detector. One snapshot cannot demonstrate anything about change detection, and a genuine change is only reported on the second consecutive observation of it, so watching two or three rounds is what makes the pending-versus-confirmed distinction visible.

rsctl health

The self-telemetry an agent ships as its own agent_health record: uptime, goroutines, heap, timing and clock quality, scheduler counters and lag percentiles, per-transport probe accounting, the rate ceiling and the draw against it, the burst budget, spool depth and shipper state, and the configuration generation in effect.

Flag What it does
-json Print the AgentHealth message as JSON.
-records Print the emitted record as JSON.

Run from rsctl there is no spool and no shipper, and the output says undeterminable for them rather than zero. That is the record’s central property rather than a limitation of the command: a spool depth of zero is the healthiest possible durability reading, an unwatched spool is a blind spot, and charting the second as the first would make an unmonitored agent look like the best one in the fleet.

The reason this record matters at all is that without it, “the network degraded” and “the agent was starved” are indistinguishable in stored data. A scheduler running 40% behind produces sparse samples with a widened RTT distribution, which is exactly what a congested link produces.

rsctl run

One path_health definition, executed for the number of cycles you ask for, printing the hop table per flow, the flow-consistency accounting read back off the transport, the path change if one occurred, and the Track A metrics.

Flag What it does
-transport fake for the scripted network (the default), or auto, raw_icmp, linux_udp_recverr, unpriv_dgram_icmp, tcp_ttl or windows_iphelper for a real one.
-target Destination to trace to.
-family ipv4 or ipv6. Default ipv4.
-flows Concurrent runs per cycle, each on a distinct flow id.
-first-ttl, -max-ttl Lowest and highest hop limit to probe.
-probes-per-hop Probes per TTL position per run.
-gap-limit Consecutive silent hops that end a run.
-flow-seed Seed for flow id allocation; 0 selects a random one.
-cycles Discovery cycles to run. Default 2.
-no-flow-control Make the scripted transport report flow_id_control=none, as windows_iphelper does.
-records Also print the envelopes written to the sink, as JSON.
-v Log the engine’s own diagnostics to stderr.

Unlike view and adapt, this one takes a real transport. -transport auto traces to a live destination through the host’s own resolver, with the same registry, the same dependency set and the same printing as the scripted run, so the two outputs are comparable line for line. What it is not is an agent: there is no spool, no shipper, no configuration source, no adaptive stage and no local target policy. It runs the cycles it was asked for and exits, which is the difference between a diagnostic and a daemon.

The scripted default is a deliberate choice rather than a limitation. It carries ECMP fan-out from hop 3, an MPLS tunnel at hop 5 with RFC 4950 labels, RFC 5837 interface identification at hop 4, a silent hop at 6, and a route change that lands in the second cycle, every one of them a real behaviour that a naive implementation misreads as a fault. Nothing about a real network is that cooperative.

The flow-consistency block is read back off the transport rather than off the output, because it is the property that separates this from a traceroute and the hop table cannot evidence it: a run whose every probe carried one flow identifier is what makes its hops a path rather than a list of routers.

rsctl check

One execution of an in-process check type against a real target, with the parameters you would put in a configuration. It prints what was constructed, the coalescing key, the plan for the types that have one, and then the metrics, optionally with the Track B detail payload and the published envelope.

Flag What it does
-type http_check, dns_check, tcp_connect, api_flow, ssh_check, dhcp_check or pmtu_check.
-target A URL, a host, or host:port, per the type.
-family ipv4, ipv6, or empty where the type sends no packets of its own. Default ipv4.
-params Type parameters as a JSON object.
-params-file Read -params from a file instead.
-timeout Deadline for the whole command. Default 30s.
-repeat Run this many times, one after another.
-validate Construct the check and print what it would do, without measuring anything.
-detail Print the Track B detail payload as JSON.
-records Print the published envelope as JSON.
-coalesce-window With -repeat, reuse a measurement across repeats within this window. 0 measures every repeat.

-params-file exists because an api_flow parameter document is several steps long, and quoting one through a shell (PowerShell in particular, which strips the outer quotes) is where an afternoon goes.

Construction goes through the same registry the agent uses, so a configuration rsctl accepts is a configuration the agent accepts, and -validate is the cheap way to find out that a parameter document would be refused. That symmetry runs the other way too: rsctl grants itself nothing. It refuses insecure_skip_verify and insecure_skip_host_key_verify for the same reason the agent does, it refuses a dhcp_check in full_dora mode because that mode consumes a real lease and the grant for it belongs to an agent’s own command line, and an ssh_check has to carry its own host_key_fingerprint because there is no agent-local known-hosts store behind this command.

The answer a check gives here is genuinely local, which is why the command is worth running from the host in question rather than from CI: the resolver, the egress path, the proxy and the trust store are all this host’s.

Two of the plans are worth reading before you run the check at all. A dhcp_check prints its mode, the client hardware address it will present, and whether the mode takes a real address out of the pool. A pmtu_check prints whether this transport can set the do-not-fragment bit, because that is the one check with a failure mode where it works, publishes a number, and the number is wrong.

rsctl view

Per-hop health across two windows against the scripted network: loss, latency percentiles, jitter, queueing delay above each hop’s own baseline, the link-delay estimates derived from adjacent hops, the confidence verdict on each row, and a path-change timeline.

Flag What it does
-target Destination to measure.
-family ipv4 or ipv6. Default ipv4.
-flows Concurrent runs per discovery cycle. Default 2.
-rounds Per-hop probe rounds per window. Default 15.
-flow-seed Seed for flow id allocation.
-records Also print the health reports and the raw sample batch, as JSON.
-v Log the engine’s own diagnostics to stderr.

This is a teaching tool and a way to reason about the confidence model, not a measurement of anything you operate. It runs on a controlled clock, because every quantity in the per-hop stage is defined over intervals (a probe every two seconds, a window every thirty, a baseline over fifteen minutes) and the alternatives are to sleep for several minutes or to print statistics gathered over no time at all.

What it is genuinely useful for is the distinction the confidence column encodes. The scripted path has a hop that is really dropping traffic and a hop that forwards perfectly while rationing the ICMP it generates. Those two look identical in a loss column and lead to opposite actions, and seeing the verdict next to the numbers that produced it is faster than reading about it. See the trust model.

rsctl adapt

What adaptive probing decided, again against the scripted network: which hops it bursted, which it refused to burst and why, the trigger and resolve timeline, the probes actually handed to the transport per hop, and the burst budget’s accounting.

Flag What it does
-target Destination to measure.
-family ipv4 or ipv6. Default ipv4.
-rounds Per-hop probe rounds per phase. Default 15.
-pps Agent-wide probe rate ceiling, as local policy would set it. Default 200.
-burst-share Fraction of that ceiling adaptive probing may draw. Default 0.25.
-records Also print the granted trigger event, as JSON.
-v Log the detector’s own diagnostics to stderr.

The run has three phases (clean, then one hop queueing, then recovered) because an anomaly is a departure from a history, and a run that started at the degradation would have nothing to call it a departure from.

Two things in the output are worth understanding before you tune anything. A trigger is never a claim that a hop is broken; it is a record that the detector looked, and what it saw. And a hop whose two measurement channels disagree is never bursted, because that reading says a router is rationing the ICMP it generates, and more probes into a rationed limiter buy a worse number rather than a better one. -pps and -burst-share here mirror -max-pps and -burst-share on the agent, so this is also the cheap way to see what a particular share would actually permit.

rsctl spool

A spool directory’s depth, segments, oldest owed record and corruption; and with -drain, delivery of what it holds.

Linux

./rsctl spool /var/lib/routespear/spool

macOS

./rsctl spool /var/lib/routespear/spool

Windows

.\rsctl.exe spool "$env:ProgramData\RouteSpear\Agent\spool"
Flag What it does
-json Print the report as JSON.
-v Print per-segment detail for clean segments too.
-drain Deliver the spool’s contents. Requires -to or -ndjson.
-to gRPC target of an IngestService to drain to.
-insecure Permit a plaintext connection to -to.
-ndjson Drain to an NDJSON file, or - for stdout.
-max Stop after this many records. 0 drains everything.
-batch Records per batch. Default 256.
-timeout Overall deadline for a drain. Default 2m.

Inspection is read-only and stays read-only. The command you reach for when an agent is misbehaving must not be the thing that repairs (or destroys) the evidence, so inspection describes what a recovery would do rather than doing it, and it is safe to run against a spool a live agent is writing.

The number to diagnose with is usually “oldest owed”: a spool whose oldest undelivered record is four hours old is an agent that has been unable to deliver for four hours, whatever its depth happens to be.

Draining requires the agent to be stopped. Two processes appending to one active segment will corrupt it, and nothing in the command can detect a second writer; there is no lock file, because a lock the agent does not take is a lock that lies. So the command prints the requirement rather than assuming you know it. Draining opens the spool for real, which performs recovery, and it deletes records as they are delivered; a drain that stops early leaves the spool intact and re-drainable, and says so, because reading “done” and then deleting the directory would be the tool misleading you rather than the agent.

-to drains through the real shipper rather than a simplified copy of it. A drain that reimplemented batching and rejection handling would be a second delivery path that could disagree with the agent’s, and it would disagree exactly on the cases you are running the command to understand.

Next

Screenshot