Skip to content

The CLI ​

Every capability is a command, and every command follows the same rules. Once you know the rules, you know the whole surface — including the parts added by plugins you install later.

rta <plugin> <capability> [arguments] [--flags]
bash
rta sys cpu --cores
rta net dns github.com --type MX
rta fs usage ~/Downloads --depth 2

Output formats ​

--output (or -o) is on every command:

FormatFor
prettyA person at a terminal. Tables, colour, charts. Default
jsonScripts and jq — the one exact format
yamlA config file, and diffing
csvSpreadsheets and appending to a file
mdDropping into a report or an issue

-o json is the one exact format: every string reads back exactly as it was. The others are cleaned the way pretty is, because they are read on a screen as often as by a program: control characters are removed, and the nine characters that reorder text (U+202A to U+202E, U+2066 to U+2069) are spelled out as a backslash, a u and the four hex digits of the code point, so a file named with one reads as what it is rather than as the name it imitates. That spelling is a display, not an encoding, and a parser reading it gets the spelled-out text. Read any value a script acts on — a file name, a key, a host — from json.

bash
rta sys overview -o json | jq '.pairs'
rta audit web example.com -o md >> security-review.md

RTA_OUTPUT sets the default, so a machine that is always scripting can say it once:

bash
export RTA_OUTPUT=json

The config file's output: key sets it too, under RTA_OUTPUT, and a typed -o wins over both. A default that names no format stops every command that renders, before it runs, as core.output.invalid: the error names RTA_OUTPUT or the key and its file, lists the formats, and is written in pretty, since the format asked for is the broken thing. rta doctor still runs and reports it as a failing output row, and answers in pretty; rta mcp serve, which renders nothing, is unaffected.

A command whose answer is one value prints that value into a pipe. rta gen password | pbcopy copies the password and not the table drawn around it, rta gen uuid and rta gen token print the identifier and the token, and rta http get URL | jq . is handed the response body. One value to a line, so --count 3 is three lines. What else the answer says, such as the entropy of a password or the status and size of a response, goes to standard error, each line marked #, so a script keeps the data and a person still sees whether the request was a 404. A terminal, -o json and the other formats are unchanged, and so is a response with no body, such as a HEAD.

Say what you want in a script. pretty is a rendering choice made for humans, and it is the one format whose shape is allowed to change.

The shape of a result ​

Every capability returns one of a closed set of shapes, named by its type: a table of columns and rows, keyvalue pairs, text, a tree, a chart, or sections composing any of them. That is why one --output flag works everywhere, and why a plugin cannot invent a format your tooling has not seen.

bash
rta net dns github.com -o json
json
{
  "columns": [{ "name": "Type" }, { "name": "Value" }],
  "rows": [["A", "140.82.121.4"]],
  "total": 1,
  "type": "table"
}

An empty listing is still its table — the columns, and rows an empty array — so jq '.rows[]' yields nothing and exits 0, and -o csv prints the header. An empty tree is the same, roots an empty array rather than null, and so is an empty page of sections, items an empty array. The sentence saying what would fill it is drawn only for a person: in pretty on a terminal, in -o md, and in the TUI. pretty redirected to a file or a pipe draws the headings instead, so the sentence never lands in a file as though it were a row. A text that is itself the answer is empty in the same way: rta git diff on a clean tree says there are no uncommitted changes on a terminal, answers "body": "" to -o json, and rta git diff > x.patch writes an empty file. Under -o csv every empty result is its header row alone — a text with no body, a tree with no roots, a chart with no series, a page with no sections — so a script that skips the header never meets a row about nothing.

-o csv writes a table as its rows and any other result as rows of its own: key/value pairs under key,value, text as one cell under text, and a tree, a chart or a page of sections as every value it holds under path,value, the path being where -o json puts it. A result that is not a table is never a reason for csv to fail — a write has already happened by the time its result is written.

A column may carry a kind — a semantic hint the renderers style by, never styling itself. Two of them are graded rather than merely aligned: a status column colours its own vocabulary (ok, warn, expired, denied…), and a usage column is a percentage of something with a ceiling, drawn green below 80%, amber from 80 and red from 90. That is what makes a full volume in rta kube pvc usage, a throttled container in kube metrics pod and a namespace out of quota findable by glance rather than by reading every row.

Not every percentage is one. fs usage's Share is a directory's proportion of a tree and 95% is the answer you ran it for; sys ps's CPU% passes 100 on a second core; kube metrics pressure reports stall time, where a node is already in trouble far below 80. Those stay percent — ungraded, and deliberately so.

Colour is never the only signal. sys disk states the same band as a word in a Status column beside its Use%, which is the half that survives --no-color, a pipe and -o json.

A machine-readable format means machine consumption, and rta treats it that way. With -o json|yaml|csv|md, stdout carries the view and nothing else — errors go to stderr, as json, yaml or md when that is what you asked for (under csv an error is the plain ERROR and HINT lines: one coded fact is not rows a spreadsheet can use), and the startup notice about untrusted artifacts is suppressed entirely. So the output on your screen is the output a parser accepts, which is where copy-and-paste gets it from:

bash
# Approve every artifact rta found and refused to run. With nothing waiting
# `rows` is empty, and xargs -r has nothing to run.
rta plugin trust -o json | jq -r '.rows[][0]' | xargs -rn1 rta plugin trust

# Every profile, and what each one covers.
rta profile list -o json | jq -r '.rows[] | "\(.[0])\t\(.[1])"'

# Ship the record onward from where you left off.
rta agent log --after "$cursor" -o json | jq -c '.rows[]'

Exit codes make the loop safe to write, and they are the next thing worth knowing.

Exit codes ​

CodeMeaning
0Success
1rta refused, or the operation failed — a structured error with a code and a hint
2The command line was wrong — an unknown command or flag, a missing or extra argument, a value a flag cannot take — so nothing ran. The error's code is core.usage, or core.output.invalid when the format is RTA_OUTPUT's or output:'s and names none. A failure rta has no code for exits 2 as well, written as plain text whatever -o says
3Confirmation required: --yes was not given where nobody could be asked, or the answer to the question was no
130, 143A signal stopped rta: SIGINT (^C) or SIGTERM. The command has three seconds after the first one to stop on its own, and exits with its own code if it does; past that, rta exits without it, with the error core.signal, once a write under way has finished — a store's, or the whole of a plugin install or remove, of one version a prune takes out, or of a key pair — ends the plugins it started, one still starting included, gives back the locks the command held, and puts back the terminal mode a prompt changed. A second signal exits at once. rta mcp serve and the TUI stop on their own terms and the three seconds are not theirs: mcp serve cancels the calls in flight and waits ten seconds for any that do not end, then exits 0, and the TUI hands the terminal back

Code 3 is the one worth handling in scripts. It means the command was destructive, or changes another tool's configuration as rta init does, and nobody confirmed — not that anything failed.

rta doctor is a check, and exits like one: 1 when any row of its report is an error, and with --strict when any is a warn too. The report is printed in full before it exits, in the format asked for, so a CI step or a pre-commit hook can gate on rta doctor --strict and the log still says which rows failed.

bash
rta note rm 4 || case $? in
  3) echo "needs --yes" ;;
  2) echo "the command line is wrong" ;;
  1) echo "refused or failed" ;;
esac

Errors carry a stable code and an actionable hint, in the format you asked for — csv excepted, as above:

bash
rta kv get missing-key -o json
json
{
  "code": "kv.notfound",
  "hint": "`rta kv list` lists every key",
  "message": "no key missing-key",
  "type": "error"
}

The code is stable across versions; the message is not. Match on the code.

--dry-run ​

Reports what would happen without doing it, on any capability that changes something:

bash
rta net hosts add 10.0.0.5 db.local --dry-run
rta note rm 4 --dry-run

This is not only a convenience for you — it is what rta shows an operator on a parked agent call, which turns "may this agent call note.rm" into "may it remove this note", and it is what the TUI shows you before a destructive run, for the same reason.

--yes ​

Skips the question a destructive command asks. At a terminal, rta note rm 4 finds the note, shows what it would remove — the line --dry-run prints — and asks Go ahead? [y/N]: y or yes goes ahead, and anything else, an empty line, ^D and ^C included, leaves it alone and exits 3. A target that is not there is reported without asking. rta profile rm, rta plugin remove, rta plugin prune and rta plugin untrust --all ask the same way, with their own --dry-run as the preview, and say so rather than ask when there is nothing to remove. Where there is no terminal on both standard input and standard error — a script, a pipe, cron, an agent's shell — nothing is asked, and a destructive command exits 3 unless it is given --yes (or -y), which is how a script says it means it. A plugin from outside rta's own binary is confirmed on what it will run with, a credential masked, and its own preview is not run before you have said yes.

bash
rta note rm 4 --yes

--help ​

Every command's --help describes what it does, the arguments it takes, and the flags it accepts, laid out for the width of your terminal — eighty columns in a pipe, COLUMNS when you set it.

bash
rta cert expiry --help
  Arguments:
    targets  hosts to check (host[:port], or an https:// URL)

  USAGE
    rta cert expiry <targets...> [--flags]

The arguments are read from the same declaration as the flags below them, so a capability cannot take one without documenting it — <targets...> alone would never tell you it accepts a port, and the dots say it takes several.

The commands you type every day — grant allow, lock add, agent log, kv set, mcp install and the rest of the operator's verbs — carry an EXAMPLES block of two to four lines that run as written, so the first usable command is on the screen and not in a paragraph. --yes and --dry-run are listed on the commands that write, and --profile on the ones with a connection to point it at; every command accepts them all the same. rta --help opens with the four commands to type first, and bare rta opens the dashboard.

rta explain ​

The authoritative reference for anything, and the one that goes deeper than --help: types, defaults, which config key fills an input, and whether an MCP caller may supply it at all.

bash
rta explain              # every capability, one row each
rta explain sys          # what is under a plugin, or under any ID prefix
rta explain sys.cpu      # one, in full
id           sys.cpu
summary      Show CPU model, core count and current usage
safety       read
idempotent   true
cli          rta sys cpu [--cores <bool>]
mcp-tool     sys_cpu
profiles     --profile <name> runs this against a configured connection; over MCP that always needs `rta grant allow sys --profile <name>`
input:cores  bool, from config plugins.sys.cpu.cores — per-core usage as a bar chart
config file  ~/.config/rta/config.yaml
set a key    rta config set plugins.sys.cpu.cores true
dashboard    a tile when added (`rta dashboard add sys.cpu`, or + on it in the TUI), re-run every few seconds; the automatic dashboard shows sys.overview for this plugin

A capability that needs a grant over MCP says so on its card, with the command that issues it — rta explain kv.get has a grant required (mcp) row — and an input an agent may not give is marked local (never offered to MCP callers).

This is generated from the same declaration the CLI parser, the TUI form and the MCP schema are built from. It cannot drift, which is more than most documentation can say — including this page.

Completion ​

rta's completion goes further than subcommand names. A capability that declares Options completes to its allowed values; one that declares Suggest completes from what actually exists on your machine — your tags, your keys, your hosts file — and only on human surfaces. Path inputs complete filesystem paths. Under a profile that fences an input, what completes is narrowed to the values the connection reaches — and an input with nothing of its own to offer offers them. What you used before is offered back per profile: a bucket typed under staging is on the list under staging and under no other profile, nor with none named, since a value is a statement about the connection it was typed for. A connection typed on one command line has the shortlist of its own, called ad hoc, and an instance — staging/analytics — has one apart from staging's.

See Installation to turn it on.

Guessing a verb ​

A verb answers to the spellings you would guess for it, in every namespace and in plugins' as well: ls for list; rm, remove and delete for one another; and get for show wherever there is no get of its own — kv get reveals a value and kv show does not, so each stays itself, and never the other way round: show is the look that leaves a value out, so it is no spelling of a get. The verbs of http are request methods and have no synonyms. rta grant rm takes a grant back, and rta lock claude is rta lock add claude, with its flags. A word after rta lock that is a typo of one of its verbs, or that reads as a verb it does not have (unlock, status, show), is refused as one, never taken for a name to freeze; rta lock add <name> freezes any name. None of these is listed in --help, which shows each command once, under its own name. A verb that the deny list rta audit clients --fix prints names by itself keeps the one spelling it is denied by, since a harness matches that list as a string; a namespace denied whole covers its aliases.

A word that is no command at all is answered with the command that most likely means it, before anything else: rta revoke with rta grant revoke, rta ps with rta sys ps, rta uuid with rta gen uuid, rta settings or rta theme with rta config, rta freeze claude with rta lock add claude. It is the same matcher rta explain and the TUI's search use, so a word finds the same thing wherever it is typed. A flag after the wrong word does not hide it, a mistyped flag is answered with the nearest one (--core with --cores), an argument a command has no place for names the flags that take a value, and inside a namespace the refusal lists its verbs on the line.

The name of a first-party plugin that is not installed is answered with the command that installs it: rta pg query, rta explain pg.query and rta grant allow pg.query all say that pg is a first-party plugin and that rta plugin install pg installs it. No other word is taken for a plugin, since installing is a decision about what to run and rta names it only for the plugins it knows.

Several at once, and a date ​

A command that takes more than one id says so with dots in its usage — rta note done <id...> — and takes them in one call: rta note done 1 2, or rta note rm 1 2 --yes. Completion offers the ids the command can still act on, and leaves out one already on the line. A due date is written the way it is said: today, tomorrow, a weekday, +3d, 2w, next-week, 10-20 for the next 20 October, or 2026-10-20, as in rta note add "renew the certificate" --due +3d; a due date makes the note a to-do, and rta explain note.add has the whole list.

Piping in ​

A few capabilities read a pipe when the argument naming their input is left out: debug ansi the text it explains, keys restore the seed words, codec jwt the token and codec jwk the key. For the last three that is the point — a pipe keeps a bearer token or a seed phrase out of ps and out of your shell history, where an argument is readable by every user on the machine for as long as the call runs:

bash
kubectl get secret app -o jsonpath='{.data.token}' | base64 -d | rta codec jwt

They read the pipe only on the CLI and only when stdin is not a terminal, so the pipe read never waits on a keyboard; keys restore, given no --words and no pipe, asks for the words at a masked prompt instead, on one line: words pasted one to a line are read off the terminal and refused, so none of them reaches your shell's history. Each reads up to a bound — a pipe holding more is refused rather than cut short. Nothing else infers an input from a pipe. What every Path input accepts is /dev/stdin:

bash
echo -n hello | rta fs hash /dev/stdin
kubectl get secret db -o jsonpath='{.data.password}' | base64 -d | rta kv set db-password --file /dev/stdin

The second line is the one that matters: it is how a credential another command prints reaches the store without ever being an argument in ps or in your shell history. To type one instead, run rta kv set db-password at a terminal: it asks for the value with echo off. The http client takes its credential the same way: --bearer-file and --basic-file read a token or user:password from a file or from /dev/stdin, so rta http get https://api.example.com/me --bearer-file /dev/stdin keeps it out of ps and your history, and there is no environment variable for it on purpose, since one would follow every URL the client is asked for. The client refuses loopback and private addresses; --local-network lets one call at a terminal reach a service of your own there, never the addresses cloud metadata lives at, and an agent cannot ask for it.

--local-network reaches localhost, ::1 and 127.0.0.1 alike, and a name with two addresses is tried on each until one answers. A URL written without a scheme is https://, except that with --local-network a host that is localhost or a loopback or private address is http:// (port 443 keeps https://): rta http get localhost:8080 --local-network. A name that merely might be private, like grafana.internal or app.localhost, keeps https://, since the scheme is chosen before the name is looked up and a DNS server decides where such a name goes; write http:// for it.

A body that is a JSON object or array goes out as Content-Type: application/json, so rta http post https://api.example.com/items --data '{"name":"x"}' needs no header; a plain value, a form or a bare 42 is sent with no type claimed. Every request asks for Accept: application/json, */* and names itself User-Agent: rta/<version>. A --header of your own always wins, and an empty one (--header 'User-Agent:') takes the default away; --dry-run shows the request as it would leave. --data-file reads the body from a file or from /dev/stdin, as the credential files above do, and like them an agent cannot name a path. curl's --data @payload.json and --data @- are refused at a terminal when they name a file or a pipe, and say to use --data-file, rather than sending the text @payload.json.

Scripting notes ​

  • Be explicit about format. -o json or RTA_OUTPUT=json.
  • Match error codes, not messages.
  • 3 is not a failure. It is a question you did not answer.
  • Pretty output is plain ASCII under a locale that does not name UTF-8. LC_ALL=C, LANG=C or a Latin-1 charset, TERM=linux and TERM=dumb draw tables with +-|, rules with -, bars with # and tree branches with |--, whether the result goes to a screen or a log; LC_ALL=C.UTF-8 brings the boxes back. Nothing set at all keeps the boxes. Only the lines rta draws change — text a capability returns is shown as it is — and -o json, yaml, csv and md are the same everywhere.
  • Bare rta in a pipe, or on a TERM=dumb terminal, prints help rather than opening the TUI, so a script never hangs on an invisible interface and a terminal that cannot move the cursor is never handed one.
  • --no-color is honoured, and colour is already suppressed when stdout is not a terminal.
  • Colour follows the environment a call is about. The palette is your theme: with the colours of the environment in force laid over it (a palette per environment): the one named by --profile, else the one rta use switched on, and none for a call that leaves it with --no-profile. --no-color, NO_COLOR and a pipe suppress colour exactly as before, and -o json and the other machine formats carry none.
  • Pretty output takes COLUMNS when it is set, over the terminal's own width and through a pipe too: COLUMNS=100 rta doctor | less. Without it a pipe gets every table at its natural width, which does not depend on whose window ran the command. With it a pipe is shaped as a terminal that wide would be, so a value longer than the width — a token, a hash — is broken across lines there too; a script reads values from -o json, never from the pretty layout.
  • Setup itself is scriptable. rta profile set and rta policy state an environment and a ceiling from flags, rta config set writes a preference, and rta dashboard add puts a tile on the TUI's landing screen, all idempotent, so provisioning does not have to fall back to writing YAML by hand — see Profiles, Your config file and Dashboard and theme.
  • Never pass a credential on a command line. Store it (rta kv set <entry>, which asks for it at a terminal, --file <path>, or --file /dev/stdin from a pipe) and reference it: --secret password=kv:<entry>. A value in argv is in ps, in your shell history, and in most CI logs. rta profile set refuses one rather than writing it.

Next ​

The TUI — the same capabilities, interactively.