---
title: "Exit-Codes und JSON-Ausgabe"
description: "Der Exit-Code-Vertrag, der JSON-Envelope jedes Befehls mit --json, die Codes, die die CLI selbst auslöst, und wie verbatra auf stderr und ins Terminal schreibt."
type: "reference"
---
# Exit-Codes und JSON-Ausgabe

Der Exit-Code-Vertrag, der JSON-Envelope jedes Befehls mit --json, die Codes, die die CLI selbst auslöst, und wie verbatra auf stderr und ins Terminal schreibt.

Alles, worauf sich ein Skript oder CI-Schritt über alle Befehle hinweg verlassen kann. Jede [Befehlsseite](https://verbatra.kreitz-webdev.de/de/docs/cli.md) ergänzt nur, was für diesen Befehl gilt.

## Exit-Codes [#exit-codes]

Der Exit-Code ist der Vertrag, auf den dein CI-Schritt verzweigt:

| Code | Bedeutung |
| --- | --- |
| `0` | Erfolg, je Befehl wie unter der Tabelle aufgeführt |
| `1` | der Lauf kam durch, aber das Ergebnis ist nicht sauber: `translate` oder `import` endete mit mindestens einer fehlgeschlagenen oder unvollständigen Locale, `check` fand eine Locale nicht synchron (oder mit `--qa` eine eingecheckte Übersetzung mit einem Integritätsfehler oder mit `--strict` einer Review-Warnung oder einem unvollständigen Plural, oder mit `--require-reviewed` eine maschinell geschriebene Übersetzung, die niemand freigegeben hat, oder mit `--sensitive` Inhalt in Quelldatei oder Glossar, der sensibel aussieht), `check --file` fand einen Syntax- oder Integritätsfehler in der Datei (oder mit `--strict` eine Review-Warnung oder einen unvollständigen Plural), `diff` fand einen fehlenden oder geänderten Key (oder mit `--unused` fand ein vollständiger Scan einen ungenutzten Quell-Key), `report provenance` fand `verbatra.provenance.json` beschädigt oder von einer neueren verbatra-Version geschrieben, `doctor` fand mindestens eine fehlgeschlagene Prüfung, `types --check` stellte fest, dass die committete Deklaration veraltet ist, oder `studio` beziehungsweise `mcp` scheiterte beim Herunterfahren seines Servers |
| `2` | konnte nicht laufen: ein Fehler des gesamten Laufs (kaputte Konfiguration, unlesbare Quelle, kaputte Lock-Datei, eine Locale, die der Provider nicht unterstützt), ein Verwendungsfehler (eine leere `--locales`-Liste, `INVALID_LOCALES`, oder ein ungültiges `--debounce` oder `--port`), ein nicht konfigurierter `--locales`-Wert (`UNKNOWN_LOCALE`), `init`, dem ein benötigtes Flag fehlt, das mehrere passende Formate oder Dateimuster findet, das sich weigert, eine bestehende Konfiguration zu überschreiben, oder das keine gültige Konfiguration anlegen kann, `watch`, das nicht starten oder nicht stoppen kann, `studio`, das die Konfiguration nicht laden, `@verbatra/studio` nicht importieren oder seinen Server nicht starten kann, oder `mcp`, dem `--config` einen Pfad nennt, den es nicht gibt, oder das `@verbatra/mcp` nicht importieren, seinen Server nicht starten oder `--redact-values` mit dem installierten `@verbatra/mcp` nicht umsetzen kann (eine fehlende oder ungültige Konfiguration aus der Suche startet ihn unkonfiguriert) |
| `3` | `translate` (auch als Dry Run und mit `--estimate`) in einem [rein menschlichen](https://verbatra.kreitz-webdev.de/de/docs/human-only-workflow.md) Projekt (`provider: { id: "none" }`) lief sauber durch, hat aber Keys für eine menschliche Übersetzung übrig gelassen; eine fehlgeschlagene oder teilweise Locale endet weiterhin mit `1` |
| `130` | `translate` oder `import` wurde mit Ctrl-C (SIGINT) abgebrochen, nachdem die Schreibsperren freigegeben waren, oder `watch`, `studio` oder `mcp` wurde durch einen zweiten Interrupt zwangsbeendet |
| `143` | `translate` oder `import` wurde mit SIGTERM beendet, nachdem die Schreibsperren freigegeben waren |

Exit `0` bedeutet je Befehl:

- `translate` oder `import`: jede Locale wurde vollständig abgeschlossen
- `check`: jede Locale ist synchron
- `diff`: keine ausstehenden Änderungen
- `report`: der Bericht wurde ausgegeben
- `doctor`: kein Problem in der Einrichtung
- `extract`: der Scan wurde abgeschlossen
- `pseudo`: die Pseudolocale wurde erzeugt
- `types`: die Deklaration wurde erzeugt
- `export`: die Arbeitsmappe wurde geschrieben
- `tmx`: der Übersetzungsspeicher wurde importiert oder exportiert
- `init`: das Projekt wurde angelegt oder war schon auf dem aktuellen Stand
- `watch`, `studio` oder `mcp`: wurde sauber beendet
- `--help` oder `--version`: die Hilfe oder die Version wurde ausgegeben

Bei `130` und `143` endet stderr mit `verbatra: interrupted (SIGINT), released locks` (oder `SIGTERM`), oder unter `--json` mit dem Datensatz `{"type":"interrupted","signal":"SIGINT","locksReleased":true}`. Scheitert das Freigeben der Sperren oder ist es nach 5 Sekunden nicht fertig, sagt die Zeile das stattdessen (`could not release locks: <reason>` oder `lock release did not finish within 5s`), und der JSON-Datensatz trägt `"locksReleased":false` mit `"reason":"failed"` (plus `message`) oder `"reason":"timed-out"` (plus `deadlineMs`).

Einige Randfälle, die du kennen solltest:

<AvailableFrom version="0.9.0" />

- Eine **unvollständige** Locale zählt als Fehlschlag. Das heißt, die Datei wurde geschrieben, aber einige Keys fehlen weiterhin, meist weil ein Provider-Sub-Batch fehlschlug oder das Integritäts-Gate eine Übersetzung abgelehnt hat. Sie endet mit `1` genau wie eine fehlgeschlagene Locale. Um beide auseinanderzuhalten, lies `partial` in der Zusammenfassung. Ein fehlgeschlagener Sub-Batch hinterlässt an der Locale einen Hinweis `SUB_BATCH_FAILED`, der den Fehlercode des Providers nennt, oder `PROVIDER_CALL_FAILED`, wenn der Fehlschlag kein strukturierter Provider-Fehler war.
- Eine **Locale, die der Provider nicht unterstützt**, ist ebenfalls ein Fehler des gesamten Laufs, nie eine einzelne fehlgeschlagene Locale. `translate` (auch ein Dry Run und `--estimate`), `watch` beim Start und eine Neuübersetzung eines einzelnen Keys lehnen sie mit `LOCALE_UNSUPPORTED_BY_PROVIDER` ab und enden mit `2`, bevor ein Provider konstruiert oder irgendetwas ausgegeben wird; keine Locale des Laufs hat also begonnen, und nichts wurde geschrieben. Beurteilt wird gegen die Sprachtabelle, die verbatra für DeepL und Google Cloud Translation mitliefert; ein LLM-Provider akzeptiert jede Locale. Lass die Locale mit `--locales` weg, um die anderen zu übersetzen, und führe [`verbatra doctor --locales`](https://verbatra.kreitz-webdev.de/de/docs/cli/doctor.md#locale-unterstützung) aus, um zu sehen, warum. Siehe [Sprachunterstützung](https://verbatra.kreitz-webdev.de/de/docs/language-support.md#unterstützung-vor-einem-lauf-prüfen).
- Eine **kaputte Lock-Datei** ist bei `translate` wie bei `import` ein Fehler des gesamten Laufs, nie eine einzelne fehlgeschlagene Locale. Sie ist eine einzige gemeinsame Datei, deshalb bricht ein Lauf, dessen Lock-Datei mittendrin kaputtgeht, mit `2` ab, statt die restlichen Locales weiter abzuarbeiten. Was vor dem Abbruch geschrieben wurde, bleibt auf der Platte: Repariere die Lock-Datei und starte den Befehl erneut.
- Ein einzelner Interrupt ist ein sauberer Stopp, und alle drei langlaufenden Befehle (`watch`, `studio` und `mcp`) enden dabei mit `0`. Gehe darüber hinaus nicht davon aus, dass sie sich gleich verhalten: Scheitert der Stopp selbst, endet `watch` mit `2`, während `studio` und `mcp` mit `1` enden.
- Ein fehlgeschlagener Lauf während `watch` erscheint als Datensatz im Ausgabestrom, nie als Exit-Code ungleich null.
- `export` hat keinen Fehlermodus pro Locale: Der Befehl endet mit `0` oder `2`, nie mit `1`.
- `doctor` liest eine kaputte Konfiguration anders als jeder andere Befehl: Eine Konfiguration, die die Suche nicht findet, oder eine, die die Validierung nicht besteht, ist eine fehlgeschlagene Prüfung und Exit `1`, denn genau das zu melden ist die Aufgabe des Befehls. Exit `2` gibt es nur, wenn er überhaupt nicht laufen kann, etwa bei einem explizit angegebenen `--config`-Pfad, der nicht existiert.
- Ein Fehlermodus liegt außerhalb des Vertrags: ein Parse-Fehler, der kein Verwendungsfehler ist, wird weitergeworfen, und das Binary fängt ihn nicht ab. Damit greift Nodes Standardverhalten für eine unbehandelte Rejection statt eines der sechs Codes.
- Ein [geschützter](https://verbatra.kreitz-webdev.de/de/docs/protecting-human-translations.md) Key (ein veralteter Wert, den ein Mensch geschrieben, importiert oder außerhalb von verbatra geändert hat, oder ein gepinnter Key) ändert den Exit-Code von `translate` in einem Projekt mit Provider nicht: Der Befehl endet mit `0` und sagt auf stderr, wie viele Keys er übrig gelassen hat (gepinnte Keys zählen nicht mit). `check` endet dafür trotzdem mit `1`, weil der Key veraltet bleibt, bis ein Mensch ihn auflöst. In einem rein menschlichen Projekt zählt er zum Exit `3`.

## JSON-Ausgabe [#json-ausgabe]

Jeder Befehl außer `studio` und `mcp` akzeptiert `--json` für maschinenlesbare Ausgabe auf stdout: `translate`, `watch`, `check`, `diff`, `report`, `export`, `import`, `tmx`, `doctor`, `pseudo`, `types`, `extract` und `init`. Jeder Datensatz ist eine Zeile, und jeder Datensatz steckt im selben Envelope. Du verzweigst also auf ein einziges Feld und musst nie raten, von welchem Befehl die Daten stammen, die du gerade in der Hand hast:

```ts
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` ist die Version dieser Envelope-Form, nicht die Version des Pakets. Es ist eine ganze Zahl, du vergleichst sie also mit `===`, statt einen Bereich zu parsen, und sie ändert sich nur, wenn ein bestehendes Feld seine Bedeutung ändert oder verschwindet. Neue Felder können hinzukommen, ohne dass die Version steigt; ignoriere also die, die du nicht kennst.

Ein Lauf, der als Ganzes fehlschlägt, schreibt genau einen `ok: false`-Datensatz auf stdout und endet mit `2`. Sein `code` ist derselbe stabile Fehlercode, den auch die stderr-Zeile trägt, und damit das, worauf du verzweigst:

```json
{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }
```

`command` ist nur dann `null`, wenn der Fehlschlag eintrat, bevor ein Unterbefehl aufgelöst war. `candidates` erscheint nur bei einem Fehler, der dich wählen lässt, etwa `FORMAT_AMBIGUOUS` von `init`, und listet die Werte auf, aus denen du wählst. `missing` erscheint bei einem Verwendungsfehler von `init`, der Flags unbeantwortet lässt, und nennt jedes Flag, das du noch übergeben musst. `causeCode` erscheint, wenn der Fehler einen anderen Fehler mit Code umhüllt, etwa ein `PROVIDER_CONSTRUCTION_FAILED` mit der Ursache `MISSING_API_KEY`; die stderr-Zeile endet dann mit `(cause: MISSING_API_KEY)`.

<AvailableFrom version="0.12.0" />

`hint` erscheint, wenn der Fehler einen nächsten Schritt hat: einen kurzen Satz im Imperativ, der ihn behebt, etwa `"Set GEMINI_API_KEY in the environment, or, with the CLI, in a .env file in the project directory."` für einen fehlenden Key oder `` "Run `verbatra check --help` to see the options and arguments it accepts." `` für einen `USAGE_ERROR`. Handle danach, statt `message` zu parsen. Er nennt Variablen, Flags und Pfade, nie den Wert eines Keys. Jeder Code aus dem SDK, von einem Provider und von einem Format-Adapter hat einen (das SDK exportiert denselben Text als [`errorHint`](https://verbatra.kreitz-webdev.de/de/docs/sdk/errors.md#errorhint)), und ebenso jeder [CLI-Fehlercode](https://verbatra.kreitz-webdev.de/de/docs/error-codes.md#cli-error-codes) außer `CLI_ERROR`. Ohne `--json` folgt derselbe Text der Fehlerzeile auf stderr als `next: ...`, außer `--quiet` ist gesetzt. `mcp --json` ist die Ausnahme: Es wird mit einer einzigen `USAGE_ERROR`-Zeile auf stderr abgelehnt, ohne Envelope und ohne `next:`. Startfehler von `studio` und `mcp` bekommen ebenfalls keine `next:`-Zeile.

Das `result` eines erfolgreichen Envelopes ist bei jedem Befehl anders und steht auf seiner Seite unter JSON-Ausgabe: [`extract`](https://verbatra.kreitz-webdev.de/de/docs/cli/extract.md), [`translate`](https://verbatra.kreitz-webdev.de/de/docs/cli/translate.md), [`watch`](https://verbatra.kreitz-webdev.de/de/docs/cli/watch.md), [`check`](https://verbatra.kreitz-webdev.de/de/docs/cli/check.md), [`diff`](https://verbatra.kreitz-webdev.de/de/docs/cli/diff.md), [`report`](https://verbatra.kreitz-webdev.de/de/docs/cli/report.md), [`doctor`](https://verbatra.kreitz-webdev.de/de/docs/cli/doctor.md), [`pseudo`](https://verbatra.kreitz-webdev.de/de/docs/cli/pseudo.md), [`types`](https://verbatra.kreitz-webdev.de/de/docs/cli/types.md), [`export`](https://verbatra.kreitz-webdev.de/de/docs/cli/export.md), [`import`](https://verbatra.kreitz-webdev.de/de/docs/cli/import.md), [`tmx`](https://verbatra.kreitz-webdev.de/de/docs/cli/tmx.md), [`init`](https://verbatra.kreitz-webdev.de/de/docs/cli/init.md).

## Schemas [#schemas]

<AvailableFrom version="0.12.0" />

Jeder Datensatz, den `--json` ausgibt, hat ein JSON Schema (Draft 2020-12). Die Schemas liegen in den npm-Paketen und sind unter festen URLs erreichbar, damit ein Skript, ein Agent oder ein Editor die Ausgabe prüfen kann, statt Prosa zu lesen. Jedes Ausgabe-Schema erlaubt Eigenschaften, die es nicht aufführt, so wie es der Envelope-Vertrag oben verspricht; behalte das bei, wenn du eines kopierst. Das Konfigurationsschema ist die bewusste Ausnahme: Es beschreibt, was du schreibst, und lehnt deshalb einen unbekannten Key ab, um einen Tippfehler zu erkennen.

| Dokument | URL | Datei im Paket |
| --- | --- | --- |
| Jeder Envelope, Erfolg oder Fehler | [`envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/envelope.json) | `@verbatra/cli/schemas/envelope.json` |
| Der Envelope eines Befehls, mit seinem `result` | `https://verbatra.kreitz-webdev.de/schema/v1/<command>-envelope.json` | `@verbatra/cli/schemas/<command>-envelope.json` |
| Der Fehler-Envelope | [`error-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/error-envelope.json) | `@verbatra/cli/schemas/error-envelope.json` |
| Das Ergebnis von `init` allein, ohne Envelope | [`init-result.json`](https://verbatra.kreitz-webdev.de/schema/v1/init-result.json) | `@verbatra/cli/schemas/init-result.json` |
| Eine JSON-Zeile auf stderr: Fortschritt, Warten auf eine Sperre, Abbruch | [`stderr-record.json`](https://verbatra.kreitz-webdev.de/schema/v1/stderr-record.json) | `@verbatra/cli/schemas/stderr-record.json` |
| Die Konfigurationsdatei | [`config.json`](https://verbatra.kreitz-webdev.de/schema/v1/config.json) | `@verbatra/sdk/schemas/config.json` oder `@verbatra/sdk/config-schema.json`: dasselbe Schema ohne `$id` und `title` |

Jede Befehlsseite verlinkt ihren Envelope unter JSON-Ausgabe, und [https://verbatra.kreitz-webdev.de/schema/v1](https://verbatra.kreitz-webdev.de/schema/v1) listet jedes Dokument mit seinem Titel auf. Ein Befehls-Envelope verweist per URL in `$ref` auf das Schema seines `result`. Ein Validator lädt es also über das Netz, oder aus `node_modules/@verbatra/sdk/dist/schemas`, wenn du diese Dateien vorher registrierst.

`v1` folgt Envelope `version: 1` und liefert immer das neueste Release aus. Ein neues Feld bleibt in `v1`; eine Änderung, die eine Nutzlast bricht, erhöht die `version` des Envelopes und fügt `/schema/v2/` hinzu. Das Konfigurationsschema nutzt dasselbe URL-Schema, folgt aber den Änderungen der Konfiguration selbst. Die URL liefert immer das neueste Release aus, eine Kopie aus npm bleibt bei ihrer Version. Eine ältere Kopie kann deshalb einen Datensatz einer neueren CLI ablehnen, wo ein Schema geschlossene Werte aufzählt, etwa den `type` eines stderr-Datensatzes oder den `command` eines Envelopes.

## CLI-Fehlercodes [#cli-fehlercodes]

<AvailableFrom version="0.12.0" />

Diese Codes löst die CLI selbst aus. Dieselbe Liste wird als `CLI_ERROR_CODES` (mit dem Typ `CliErrorCode`) von `@verbatra/cli` ausgeliefert, sodass ein Skript einen Code dagegen prüfen kann. Jeder andere Code kommt vom SDK, einem Provider oder einem Format-Adapter. Was jeder Code bedeutet, welche Befehle ihn auslösen und was der nächste Schritt ist, steht unter [Fehlercodes](https://verbatra.kreitz-webdev.de/de/docs/error-codes.md#cli-error-codes).

## Fehler auf stderr [#fehler-auf-stderr]

Jeder Fehler geht in beiden Modi als eine strukturierte Zeile (`verbatra: error [CODE] message`) auf stderr. Seit 0.12.0 gilt das auch für einen Fehler beim Parsen der Argumente (eine unbekannte Option oder ein unbekannter Befehl, ein fehlendes Argument): er gibt `verbatra: error [USAGE_ERROR] unknown option '--nope'` aus und endet mit `2`, wo vorher commanders eigenes `error: unknown option '--nope'` ohne Code stand. `init` folgt derselben Grammatik seit 0.12.0; davor gab ein fehlender Provider `verbatra: --provider is required (...)` ohne Code aus. Seit 0.12.0 folgt ohne `--json` der Zeile ein Hinweis als `next: ...`, und mit `--json` schreibt ein Parse-Fehler den `USAGE_ERROR`-Envelope samt `hint` auf stdout; `mcp` hält stdout für das Protokoll frei und lehnt `--json` daher nur auf stderr ab. Der Start von `studio` und `mcp` ist die Ausnahme von der strukturierten Zeile: Ist `@verbatra/studio` oder `@verbatra/mcp` nicht installiert, steht auf stderr allein die Installationsanweisung, ohne das Präfix `verbatra:`. Mit `--json` ist die `message` eines Parse-Fehlers commanders eigener Text samt Präfix `error: `. Ohne `--json` schreibt ein fehlgeschlagener Lauf weiterhin überhaupt nichts auf stdout. Datensätze zu Fortschritt und Lock-Wartezeit gehen immer auf stderr, auf stdout stehen also ausschließlich Envelopes.

## Terminalausgabe [#terminalausgabe]

<AvailableFrom version="0.12.0" />

Ergebnisse gehen auf stdout. Fortschritt, Hinweise, Tipps, Warnungen und Fehler gehen auf stderr, und verbatra passt sie daran an, wohin stderr geht:

- In einem interaktiven Terminal sind feste Labels wie `error`, `[ok]` und `next:` eingefärbt, und ein Schritt, der länger als 300 ms dauert, zeigt einen ASCII-Spinner (`| / - \`).
- Wenn stderr umgeleitet oder in eine Pipe geschrieben wird, `CI` auf einen anderen Wert als leer, `0` oder `false` gesetzt ist oder `TERM` den Wert `dumb` hat, besteht die Ausgabe aus schlichten, nur angehängten Zeilen ohne Farbe und ohne Animation.
- Die Statuslabels des [`doctor`](https://verbatra.kreitz-webdev.de/de/docs/cli/doctor.md)-Berichts auf stdout (`[ok  ]`, `[warn]`, `[fail]`, `[skip]`) sind farbig, wenn stdout selbst ein interaktives Terminal ist; ein umgeleiteter Bericht bleibt schlicht, außer `FORCE_COLOR` schaltet die Farbe ein.
- `--json` fügt nie Farbe oder Animation hinzu, und stdout trägt ausschließlich Envelopes. Schlichte `verbatra:`-Warnzeilen können trotzdem auf stderr landen, ein Konsument parst also nur die stderr-Zeilen, die mit `{` beginnen. Siehe [JSON-Ausgabe](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md#json-ausgabe).

| Flag oder Variable | Wirkung |
| --- | --- |
| `-q`, `--quiet` | gibt nur Ergebnisse, Warnungen und Fehler aus: lässt Fortschrittszeilen, `next:`-Tipps und informative stderr-Zeilen weg, während die Ergebniszusammenfassung samt ihren Hinweisen bleibt. Es wirkt vor oder nach dem Befehlsnamen |
| `--no-color` | färbt die Ausgabe nie ein |
| `NO_COLOR`, `NODE_DISABLE_COLORS`, `VERBATRA_NO_COLOR` | jeder nicht leere Wert schaltet die Farbe ab; `VERBATRA_NO_COLOR` übersteuert auch `FORCE_COLOR` |
| `FORCE_COLOR` | `1`, `2`, `3`, `true` oder ein leerer Wert schaltet die Farbe ein, auch wenn stderr oder stdout kein Terminal ist, und jeder andere Wert, auch `0`, schaltet sie ab. Ist es gesetzt, gewinnt es gegen `NO_COLOR`, `NODE_DISABLE_COLORS`, `CI` und `TERM=dumb`; `--no-color` und `VERBATRA_NO_COLOR` haben trotzdem Vorrang |
| `VERBATRA_NO_SPINNER` | jeder nicht leere Wert ersetzt den Spinner durch statische Zeilen `label... done (1.2s)`, für Screenreader und Terminalaufzeichnungen |

Die `output`-Beispiele auf den Befehlsseiten zeigen die schlichte Form. Ein interaktives Terminal zeigt einen laufenden Schritt als Spinner-Zeile, die mit `[ok]` und der verstrichenen Zeit endet.

Statuswörter sind reines ASCII (`[ok]`, `[warn]`, `[fail]`, `[skip]`), damit die Ausgabe in jedem Terminal und Log gleich lesbar ist. Jede Zeile durchläuft dieselbe Schlüsselschwärzung wie die übrige Ausgabe, und eingefärbt werden nur die festen Labels, nie ein Schlüssel, ein Pfad oder eine Meldung.
