Reference / CLI
Exit codes and JSON output
The exit-code contract, the JSON envelope every --json command prints, the codes the CLI raises itself, and how verbatra writes to stderr and the terminal.
Everything a script or CI step can rely on across commands. Each command page adds only what is specific to that command.
Exit codes
The exit code is the contract your CI step branches on:
| Code | Meaning |
|---|---|
0 | success, per command as listed below the table |
1 | it ran, but the result is not clean: translate or import finished with at least one failed or partial locale, check found a locale out of sync (or, with --qa, a committed translation with an integrity error, or with a review warning or an incomplete plural under --strict, or, with --require-reviewed, a machine-written translation nobody approved, or, with --sensitive, content that looks sensitive in the source file or glossary), check --file found a syntax or integrity error in the file (or, under --strict, a review warning or an incomplete plural), diff found a missing or changed key (or, with --unused, a complete scan found an unused source key), report provenance found verbatra.provenance.json corrupt or from a newer verbatra, doctor found at least one failed check, types --check found the committed declaration out of date, or studio or mcp failed while shutting its server down |
2 | could not run: a whole-run error (bad config, unreadable source, corrupt lock file, a locale the provider does not support), a usage error (an empty --locales list, INVALID_, or an invalid --debounce or --port), an unconfigured --locales value (UNKNOWN_), init missing a flag it needs, finding several formats or file patterns that fit, refusing to overwrite an existing config, or unable to scaffold a valid config, watch failing to start or to stop, studio unable to load the config, import @verbatra/studio, or start its server, or mcp given a --config path that does not exist, or unable to import @verbatra/mcp, start its server, or honor --redact-values with the installed @verbatra/mcp (a missing or invalid config found by the search starts it unconfigured) |
3 | translate (a dry run and --estimate included) in a human-only project (provider: { id: "none" }) finished cleanly, but left keys for a human translation; a failed or partial locale still exits 1 |
130 | translate or import was interrupted with Ctrl-C (SIGINT) after releasing its write locks, or watch, studio or mcp was force-stopped by a second interrupt |
143 | translate or import was stopped with SIGTERM after releasing its write locks |
Exit 0 means, per command:
translateorimport: every locale finished completecheck: every locale is in syncdiff: no pending changesreport: it printed its reportdoctor: no setup problemextract: it completed its scanpseudo: it generated its pseudolocaletypes: it generated its declarationexport: it wrote its workbooktmx: it imported or exported its translation memoryinit: it scaffolded the project or found it already up to datewatch,studioormcp: it stopped cleanly--helpor--version: it printed the help or the version
On 130 and 143, stderr ends with verbatra: interrupted (SIGINT), released locks (or SIGTERM), or under --json with the record {"type":"interrupted","signal":"SIGINT","locksReleased":true}. If releasing the locks fails or does not finish within 5 seconds, the line says so instead (could not release locks: <reason> or lock release did not finish within 5s), and the JSON record carries "locksReleased":false with "reason":"failed" (plus message) or "reason":"timed-out" (plus deadlineMs).
Some edge cases worth knowing:
Available from 0.9.0- A partial locale counts as failure. It means the file was written but some keys are still missing, usually because a provider sub-batch failed or the integrity gate refused a translation. It exits
1exactly like a failed locale. Readpartialon the summary to tell the two apart. A failed sub-batch leaves aSUB_notice on the locale naming the provider's error code, orBATCH_ FAILED PROVIDER_when the failure was not a structured provider error.CALL_ FAILED - A locale the provider does not support is a whole-run error too, never one failed locale.
translate(a dry run and--estimateincluded),watchat startup, and a single-key retranslation refuse it withLOCALE_and exitUNSUPPORTED_ BY_ PROVIDER 2before any provider is constructed or anything is spent, so no locale of the run has started and nothing was written. It is judged against the language table verbatra ships for DeepL and Google Cloud Translation; an LLM provider accepts any locale. Leave the locale out with--localesto translate the others, and runverbatra doctor --localesto see why. See Language support. - A corrupt lock file is a whole-run error in
translateandimportalike, never one failed locale. It is a single shared file, so a lock file that turns corrupt while the run is under way stops the run with2rather than carrying on through the remaining locales. Whatever was written before the abort stays on disk; fix the lock file and re-run the command. - A single interrupt is a clean stop, and all three long-running commands (
watch,studioandmcp) exit0on it. Do not assume they behave alike beyond that: if the stop itself fails,watchexits2whilestudioandmcpexit1. - A failed run during
watchshows up as a record on the output stream, never as a non-zero exit code. exporthas no per-locale failure mode: it exits0or2, never1.doctorreads a broken config differently from every other command: a config the search does not find, or one that fails validation, is a failed check and exit1, because reporting exactly that is the command's job. It exits2only when it cannot run at all, such as an explicit--configpath that does not exist.- One failure mode sits outside the contract: a parse failure that is not a usage error is re-thrown, and the binary does not catch it, so Node's default handling of an unhandled rejection applies instead of any of the six codes.
- A protected key (a stale value a person wrote, imported, or changed outside verbatra, or a pinned key) does not change the exit code of
translatein a project with a provider: it exits0and says on stderr how many keys it left (pinned keys are not counted).checkstill exits1for it, because the key stays stale until a person resolves it. In a human-only project it counts toward exit3.
JSON output
Every command except studio and mcp accepts --json for machine-readable output on stdout: translate, watch, check, diff, report, export, import, tmx, doctor, pseudo, types, extract, and init. Every record is one line, and every record has the same envelope around it, so you branch on one field and never have to guess which command's payload you are holding:
type Envelope<TResult> =
| { ok: true; version: 1; command: string; result: TResult }
| {
ok: false;
version: 1;
command: string | null;
code: string;
message: string;
causeCode?: string; // the code of the error this one wraps, when it has one
candidates?: string[]; // the values to choose from, on an error that has them
missing?: string[]; // the flags still to pass, on an init usage error
hint?: string; // the next step that resolves the error, when there is one
};version is the version of this envelope shape, not the package version. It is an integer so you compare it with === rather than range-parsing it, and it changes only when an existing field changes meaning or disappears. New fields can appear without a bump, so ignore the ones you do not recognize.
A run that fails as a whole writes exactly one ok: false record to stdout and exits 2. Its code is the same stable error code the stderr line carries, so it is what you branch on:
{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }command is null only when the failure happened before a subcommand was resolved. candidates appears only on an error that asks you to choose, such as init's FORMAT_, and lists the values to choose from. missing appears on an init usage error that leaves flags unanswered and names each flag still to pass. causeCode appears when the error wraps another coded error, such as a PROVIDER_ caused by MISSING_; the stderr line then ends with (cause: MISSING_.
hint appears when the error has a next step: one short imperative sentence that resolves it, such as "Set GEMINI_ for a missing key or "Run `verbatra check --help` to see the options and arguments it accepts." for a USAGE_. Act on it instead of parsing message. It names variables, flags, and paths, never a key value. Every SDK, provider, and format adapter code has one (the SDK exports the same text as errorHint), and so does every CLI error code except CLI_. Without --json, the same text follows the error line on stderr as next: ..., unless --quiet is set. mcp --json is the exception: it is refused with one USAGE_ line on stderr, with no envelope and no next:. Startup errors of studio and mcp get no next: line either.
The result of a successful envelope is different for each command and is described under JSON output on its page: extract, translate, watch, check, diff, report, doctor, pseudo, types, export, import, tmx, init.
Schemas Available from 0.12.0
Every record --json prints has a JSON Schema (draft 2020-12). The schemas ship inside the npm packages and are served at stable URLs, so a script, an agent or an editor can validate output instead of reading prose. Every output schema allows properties it does not list, as the envelope contract above promises; keep it that way if you copy one. The config schema is the deliberate exception: it describes what you write, so it rejects an unknown key to catch a typo.
| Document | URL | File in the package |
|---|---|---|
| Any envelope, success or error | envelope.json | @verbatra/cli/schemas/envelope.json |
One command's envelope, with its result | https://verbatra.kreitz-webdev.de/schema/v1/<command>-envelope.json | @verbatra/cli/schemas/<command>-envelope.json |
| The error envelope | error-envelope.json | @verbatra/cli/schemas/error-envelope.json |
The init result alone, without the envelope | init-result.json | @verbatra/cli/schemas/init-result.json |
| A JSON line on stderr: progress, lock wait, interrupt | stderr-record.json | @verbatra/cli/schemas/stderr-record.json |
| The config file | config.json | @verbatra/sdk/schemas/config.json, or @verbatra/sdk/config-schema.json: the same schema without $id and title |
Each command page links its envelope under JSON output, and https://verbatra.kreitz-webdev.de/schema/v1 lists every document with its title. A command envelope points at its result schema by URL in $ref, so a validator loads it over the network, or from node_ when you register those files first.
v1 tracks envelope version: 1 and always serves the latest release. An added field stays in v1; a change that breaks a payload raises the envelope version and adds /schema/v2/. The config schema uses the same URL scheme but follows the config's own changes. The URL always serves the latest release while a copy from npm stays at its version, so an older copy can reject a record from a newer CLI where a schema lists closed values, such as a stderr record's type or an envelope's command.
CLI error codes Available from 0.12.0
The CLI raises these codes itself. The same list ships as CLI_ (with the type CliErrorCode) from @verbatra/cli, so a script can check a code against it. Any other code comes from the SDK, a provider, or a format adapter. What each code means, which commands raise it, and its next step are on Error codes.
Errors on stderr
Every error goes to stderr as one structured line (verbatra: error [CODE] message) in both modes. Since 0.12.0 that includes an argument-parsing error (an unknown option or command, a missing argument): it prints verbatra: error [USAGE_ and exits 2, where it printed commander's own error: unknown option '--nope' with no code before. init follows the same grammar since 0.12.0; before that, a missing provider printed verbatra: --provider is required (...) with no code. Since 0.12.0, a hint follows the line as next: ... without --json, and under --json a parsing error writes the USAGE_ envelope with its hint to stdout; mcp keeps stdout for the protocol, so it refuses --json on stderr only. The startup of studio and mcp is the exception to the structured line: when @verbatra/studio or @verbatra/mcp is not installed, stderr carries the install instruction alone, with no verbatra: prefix. Under --json, the message of a parsing error is commander's own text, error: prefix included. Without --json, a failing run still writes nothing at all to stdout. Progress and lock-wait records always go to stderr, so stdout carries nothing but envelopes.
Terminal output Available from 0.12.0
Results go to stdout. Progress, notices, hints, warnings and errors go to stderr, and verbatra adapts them to where stderr is going:
- On an interactive terminal, fixed labels such as
error,[ok]andnext:are colored, and a step that takes longer than 300 ms shows an ASCII spinner (| / - \). - When stderr is piped or redirected,
CIis set to anything but empty,0orfalse, orTERMisdumb, the output is plain, append-only lines with no color and no animation. - The status labels of the
doctorreport on stdout ([ok ],[warn],[fail],[skip]) are colored when stdout itself is an interactive terminal, so a redirected report stays plain unlessFORCE_turns color on.COLOR --jsonnever adds color or animation, and stdout carries nothing but envelopes. Plainverbatra:warning lines can still reach stderr, so a consumer parses only the stderr lines that start with{. See JSON output.
| Flag or variable | Effect |
|---|---|
-q, --quiet | print only results, warnings and errors: drops progress lines, next: hints and informational stderr lines, while the result summary, including its notices, stays. It works before or after the command name |
--no-color | never color the output |
NO_, NODE_, VERBATRA_ | any non-empty value turns color off; VERBATRA_ also overrides FORCE_ |
FORCE_ | 1, 2, 3, true or an empty value turns color on even when stderr or stdout is not a terminal, and any other value, 0 included, turns it off. Once set, it wins over NO_, NODE_, CI and TERM=dumb; --no-color and VERBATRA_ still win |
VERBATRA_ | any non-empty value replaces the spinner with static label... done (1.2s) lines, for screen readers and terminal recordings |
The output samples on the command pages show the plain form. An interactive terminal shows a running step as a spinner line that ends in [ok] and the elapsed time.
Status words are plain ASCII ([ok], [warn], [fail], [skip]), so the output reads the same in every terminal and log. Every line passes through the same key redaction as the rest of the output, and only the fixed labels are ever colored, never a key, path or message.