---
title: "verbatra init"
description: "Create a verbatra config and .env example for this project, detected from your locale files, interactively or fully unattended."
type: "reference"
---
# verbatra init

Create a verbatra config and .env example for this project, detected from your locale files, interactively or fully unattended.

Scaffold a project: `init` writes a `verbatra.config.ts`, a `.env.example` naming your provider's API key variable (never a key value), and the `.gitignore` entries for `.env`, `.env.local`, `.verbatra-local/` and `verbatra.cache.json`. It reads the locale files already in the project first, so usually the provider is all you have to tell it.

## Synopsis [#synopsis]

```bash
verbatra init [flags]
```

## Examples [#examples]

```bash
# prompt for anything not passed or detected
verbatra init --provider gemini

# fully unattended, one JSON document on stdout
verbatra init --provider gemini --yes --json

# choose the format and pattern yourself
verbatra init --provider deepl --format yaml --path "config/locales/{locale}.yml" --yes

# a local server that needs no key
verbatra init --provider openai-compatible --base-url http://localhost:11434/v1 --model llama3.1 --yes

# a self-hosted LibreTranslate server, no language model
verbatra init --provider libretranslate --base-url http://localhost:5000 --yes

# human-only: no provider, no API key
verbatra init --provider none --yes

# also set the project up for coding agents
verbatra init --provider gemini --yes --agent

# wire Cursor and VS Code, and only show what would change
verbatra init --agent --client cursor,vscode --dry-run
```

## Flags [#flags]

| Flag | Argument | Default | Effect |
| --- | --- | --- | --- |
| `--provider` | `<id>` | none (required unless asked) | provider to scaffold: `anthropic`, `openai`, `gemini`, `deepl`, `google-translate`, `opencode`, `openai-compatible`, `libretranslate`, or `none` |
| `--format` | `<id>` | detected, else `i18next-json` | the locale file format, one of the ids on [Formats](https://verbatra.kreitz-webdev.de/docs/formats.md) |
| `--source` | `<locale>` | detected, else `en` | the source locale your strings are written in |
| `--targets` | `<locales>` | detected, else `de` | comma-separated target locales to translate into |
| `--path` | `<pattern>` | detected, else the format's usual layout ([Detection](#detection)) | locale file pattern; must contain the `{locale}` token |
| `--model` | `<name>` | the scaffold default | the model; required for `openai-compatible`, optional for `anthropic`, `openai`, `gemini` and `opencode` |
| `--base-url` | `<url>` | none | the server URL; `openai-compatible` and `libretranslate` only, and required there |
| `--api-key-env-var` | `<name>` | `OPENAI_COMPATIBLE_API_KEY` | the variable `openai-compatible` reads its key from |
| `--yes` | none | off | never prompt; take the default for anything neither passed nor detected |
| `--force` | none | off | overwrite an existing `verbatra.config.ts` whose content differs |
| `--agent` | none | off | also write the coding-agent section to `AGENTS.md` or `CLAUDE.md` and the verbatra MCP server to each detected client's project config ([Set up coding agents](#set-up-coding-agents)) |
| `--client` | `<ids>` | detected | comma-separated clients `--agent` wires instead of the detected ones: `claude`, `cursor`, `vscode`, `codex`, `gemini`, or `all`; needs `--agent` |
| `--dry-run` | none | off | report every file `init` would write or change, and write nothing ([Dry run](#dry-run)) |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd` and `--json`, but not `--config`. Here `--cwd` is the directory to scan and write to, and it must already exist. `--json` never prompts.

## Behavior [#behavior]

### Prompts [#prompts]

<AvailableFrom version="0.12.0" />

A flag always wins. Anything you leave out is detected from the project, then prompted for when stdin is a terminal and neither `--yes` nor `--json` is given, and otherwise taken from its default only under `--yes`. Pressing Enter accepts the suggested value. A flag with an empty value, such as `--targets ''`, is a usage error (`INVALID_OPTION`), never a request for the default. If stdin ends at a prompt (Ctrl-D), `init` exits `2` with `MISSING_OPTIONS`.

Each answer is checked as you give it. An invalid one is explained on stderr and asked again, up to three times, keeping your earlier answers; a third invalid answer stops `init` with that answer's error code (such as `INVALID_LOCALE`), names the flag to pass instead, and writes nothing.

### Detection [#detection]

`init` scans the directory for files a format adapter claims, skipping hidden directories, `node_modules` and build output, and finds the part of each path that spells a locale, such as `de.json`, `messages_pt_BR.properties`, `de.lproj/` or Android's `values-de/`. The file pattern covering the most locales wins, and it fills in:

- `format`: the one format whose adapter reads every matched file. Every JSON adapter reads `.json`, so for JSON files a single matching dependency in `package.json` (`i18next`, `vue-i18n`, `next-intl` or `@ngx-translate/core`) decides.
- `files.pattern` and `files.localeStyle`: `posix` when the files spell `pt_BR`, `android` for `values-*` directories, `literal` otherwise.
- `sourceLocale`: the source language of an `.xcstrings` catalogue, else `en` or the single `en-*` locale found, else the only locale found.
- `targetLocales`: every other locale found.

A file beside the locale files that holds strings under no locale name, such as `messages.properties`, `Strings.resx` or a gettext `app.pot`, is reported as `unqualifiedSourceFile`. verbatra cannot read the source from such a file, so `init` does not guess the source locale: pass `--source`, even with `--yes`, and the next steps name the file to rename or copy the base file to.

With no locale file at all, the dependencies alone decide the format, and the pattern follows the format's usual layout, the one listed per format on [Pick your stack](https://verbatra.kreitz-webdev.de/docs/pick-your-stack.md).

An `apple-xcstrings` catalogue is one file shared by every locale, and verbatra never creates it: create it in Xcode first. `--format`, `--path` and `--source` narrow the detection instead of being overridden by it, so `--format yaml` only looks at YAML files.

`init` never guesses between equals. Several formats that read the files (plain JSON with no i18n dependency) exit `2` with `FORMAT_AMBIGUOUS`, and several tied file patterns (one file per namespace) exit `2` with `LAYOUT_AMBIGUOUS`. Both list the candidates; pass `--format` or `--path` to choose. At a terminal it asks instead.

### Unattended runs [#unattended-runs]

Without a terminal, or with `--json`, `init` never prompts. A value that is neither passed, detected, nor defaulted by `--yes` is an error: `init` exits `2` with `MISSING_OPTIONS` and names every missing flag. An ambiguity or an invalid flag also lists the flags still missing, so one run tells an agent everything it has to pass. `--provider` has no default, and neither do `--base-url` and `--model` for `openai-compatible` or `--base-url` for `libretranslate`. This is the command an agent runs:

```bash
npx @verbatra/cli init --provider gemini --yes --json
```

### JSON output [#json-output]

Schema: [`init-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/init-envelope.json) validates the whole envelope; [Schemas](https://verbatra.kreitz-webdev.de/docs/cli/output.md#schemas) lists the others.

With `--json`, stdout carries exactly one [envelope](https://verbatra.kreitz-webdev.de/docs/cli/output.md#json-output). Its `result`:

```ts
interface InitResult {
  configPath: string;                  // absolute path of verbatra.config.ts, or of the config --agent kept
  files: { path: string; action: "created" | "overwritten" | "updated" | "unchanged" }[];
  dryRun: boolean;                     // true under --dry-run: files holds the planned actions
  config: {                            // the config that was validated and written
    sourceLocale: string;
    targetLocales: string[];
    format: string;
    files: { pattern: string; localeStyle?: "posix" | "android" };
    provider: { id: string; options?: Record<string, unknown> };
    sensitiveData?: { mode: "warn" };
  } | null;
  sources: {                           // where each value came from
    provider: "flag" | "prompt";
    format: "flag" | "prompt" | "files" | "dependencies" | "default";
    pattern: "flag" | "prompt" | "detected" | "default";
    sourceLocale: "flag" | "prompt" | "detected" | "default";
    targetLocales: "flag" | "prompt" | "detected" | "default";
  } | null;
  apiKeyEnvVar: string | null;         // the variable to set; null for provider none
  detection: {
    format: { id: string; from: "input" | "files" | "dependencies" } | null;
    layout: {
      pattern: string;
      localeStyle: "literal" | "posix" | "android";
      locales: string[];
      sourceLocale: string | null;
      unqualifiedSourceFile: string | null;
      files: string[];
    } | null;
    ambiguities: { subject: "format" | "layout"; candidates: string[] }[];
    confidence: "high" | "medium" | "low" | "none";
    reasons: string[];                 // one sentence per decision or doubt
  } | null;
  agent: {                             // null without --agent
    instructionsFile: "AGENTS.md" | "CLAUDE.md";
    mcpServer: "added" | "present" | "differs" | "skipped" | null; // Claude Code; null when not wired
    configKept: boolean;               // true when --agent kept an existing config
    clients: {
      id: "claude" | "cursor" | "vscode" | "codex" | "gemini";
      file: string;                    // the client's config file, such as .mcp.json
      server: "added" | "present" | "differs" | "skipped";
      reason: "plugin" | "symlink" | null; // why the client was skipped
      selectedBy: "flag" | "markers" | "default";
      markers: string[];               // the client's marker files found in the project
    }[];
  } | null;
  nextSteps: { description: string; command: string | null }[];
}
```

`confidence` is `high` when the files alone decided everything, `medium` when a convention or a dependency filled a gap (such as taking `en` as the source) or a gap stayed open, `low` when no locale file backed the format, and `none` when nothing was detected. A `default` in `sources` is the value to double-check. `nextSteps` ends with the exact commands to run next.

An error envelope for `FORMAT_AMBIGUOUS`, `LAYOUT_AMBIGUOUS`, `INVALID_PROVIDER` or `INVALID_FORMAT` also carries `candidates`, the values to choose from. Every usage error that leaves flags unanswered carries `missing`, naming them, such as `"--source <locale>"`.

### Providers [#providers]

<AvailableFrom version="0.12.0" />

- `--provider none` scaffolds a [human-only](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md) config with no `options` and writes no `.env.example`, since no API key is ever read.
- `--provider openai-compatible` scaffolds a local or self-hosted server from `--base-url` and `--model`, with `maxOutputTokens: 4096`. Its key is optional: a server that needs none leaves `OPENAI_COMPATIBLE_API_KEY` unset, and `--api-key-env-var` names a different variable, in upper case such as `LLM_API_KEY`. A rejected value is never echoed back, in case it was the key itself.
- `--provider libretranslate` scaffolds a self-hosted [LibreTranslate](https://verbatra.kreitz-webdev.de/docs/providers.md#libretranslate) server from `--base-url` alone, such as `http://localhost:5000`, and refuses `--model` and `--api-key-env-var`. `.env.example` names `LIBRETRANSLATE_API_KEY` for a server started with `--api-keys`.

`init` writes a variable's name into the config and `.env.example`, never a value, and refuses a base URL with credentials, a query string or a fragment (`https://user:key@host`, `https://host/v1?api-key=...`), where a key could end up in the committed config.

### Existing files [#existing-files]

Running `init` again with the same answers changes nothing and succeeds. A `verbatra.config.ts` with different content is refused with `CONFIG_EXISTS` unless you pass `--force`, and that refusal comes before any prompt or other error. Another verbatra config in the directory (a `.verbatrarc.json`, or a `verbatra` property in `package.json`) is refused even under `--force`, because it would be read first. Nothing is written when `init` refuses. A parent directory's config covering this one draws a stderr warning (dry run too): the new `verbatra.config.ts` replaces it here.

A directory `init` cannot write to is refused with `INIT_UNWRITABLE`, naming the file and any file already written. A `verbatra.config.ts`, `.env.example` or `.gitignore` that is a symbolic link leading outside the project, or nowhere, is refused with `INIT_UNWRITABLE` before anything is written. An existing `.env.example` is kept; when it does not name the key variable yet, that one line is appended, and under `--force` a header line `init` wrote itself is rewritten for the new provider. `.gitignore` gets only the entries it is missing.

### Dry run [#dry-run]

<AvailableFrom version="0.12.0" />

`--dry-run` runs every check and refusal of a real run except a failed write, such as a read-only directory, prints `would create`, `would update`, `would overwrite` or `unchanged` for each file, and writes nothing. The JSON result lists the planned actions and sets `dryRun` to `true`.

### Set up coding agents [#set-up-coding-agents]

<AvailableFrom version="0.12.0" />

`--agent` also prepares the project for coding agents. It writes no key value, never writes through a link out of the project, and adds:

- **The instruction file.** A verbatra section between `<!-- verbatra:start -->` and `<!-- verbatra:end -->` markers, word for word the snippet from [Tell your agent about verbatra](https://verbatra.kreitz-webdev.de/docs/agent-recipes.md#tell-your-agent-about-verbatra). It goes to `AGENTS.md`, or to `CLAUDE.md` when that is the only instruction file; when both exist, `AGENTS.md` wins. In a file that already holds the section, the text between the markers is replaced with the current one and reported `updated`, or `unchanged` when it already matches.
- **Each client's MCP config.** The `verbatra` server entry from [Connect an MCP client](https://verbatra.kreitz-webdev.de/docs/connect-an-mcp-client.md), `npx -y @verbatra/mcp` without `--allow-spend` or `VERBATRA_MCP_ALLOW_SPEND`, so the spend tools stay hidden. `@verbatra/cli` exports these entries as `AGENT_CLIENT_CONFIGS`.

| Client | `--client` | File | Detected from |
| --- | --- | --- | --- |
| Claude Code | `claude` | `.mcp.json` | `CLAUDE.md`, `.claude/`, `.mcp.json` |
| Cursor | `cursor` | `.cursor/mcp.json` | `.cursor/`, `.cursorrules` |
| VS Code | `vscode` | `.vscode/mcp.json`, under `servers` | `.vscode/mcp.json` |
| Codex | `codex` | `.codex/config.toml`, as `[mcp_servers.verbatra]` | `.codex/` |
| Gemini CLI | `gemini` | `.gemini/settings.json` | `.gemini/`, `GEMINI.md` |

`init` wires every client it detects, and only Claude Code when it finds no marker. `AGENTS.md` picks the instruction file, not a client. A `.vscode` folder without `mcp.json` is not wired: a next step names the command that wires it. Gemini CLI reads `GEMINI.md`: unless its settings or `GEMINI.md` load the instruction file, a next step says how. A detected client whose config path runs through a symbolic link is skipped with reason `symlink` and a next step. `--client` replaces the detection; an unknown id, an empty list or `--client` without `--agent` exits `2` with `INVALID_OPTION` and the candidates.

Nothing outside the markers is touched, and other servers and keys keep their bytes, indentation and line endings when the file is laid out one entry per line; otherwise it is rewritten with the same content. The Codex table is appended. An existing `verbatra` server is never touched: the file is reported `unchanged`, and the client's `server` is `present` when the entry matches the scaffold, or `differs` when it does not, which also adds a `nextSteps` note.

When `.claude/settings.json` or `.claude/settings.local.json` enables the verbatra Claude Code plugin, `init` skips `.mcp.json`: Claude Code is reported `skipped` with reason `plugin` and a next step, and an existing entry is never deleted. A settings file behind a symbolic link is ignored.

With an existing config, `init --agent` keeps it, writes only the agent files, prints `kept verbatra.config.ts`, never prompts and detects nothing. The JSON result lists the config as `unchanged`, sets `agent.configKept` to `true`, and carries `null` for `config`, `sources`, `apiKeyEnvVar` and `detection`. A config flag or `--force` puts the config back into the run, as without `--agent`.

`init` refuses before writing anything, with `AGENT_FILE_INVALID`, when a client file is not plain JSON (it never rewrites comments or trailing commas), not an object, repeats a key, or has a servers value that is not an object; when `.codex/config.toml` sets `mcp_servers` or the verbatra server inline, with dotted keys or as an array of tables, repeats that table or a key in it, or has an unterminated string, array or header; when a client that `--client` names has a symbolic link on its config path, or an instruction file links outside the project; or when the instruction file has an unpaired, misordered or repeated marker. The message never quotes the file.

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | files written, or already up to date |
| `2` | a usage error: `MISSING_OPTIONS`, `INVALID_PROVIDER`, `INVALID_FORMAT`, `INVALID_OPTION`, `INVALID_LOCALE`, `INVALID_LOCALES`, `FORMAT_AMBIGUOUS`, `LAYOUT_AMBIGUOUS`, `CONFIG_EXISTS`, `INIT_UNWRITABLE`, `AGENT_FILE_INVALID`, or `CONFIG_INVALID` when the answers would not make a valid config |

See [the exit-code contract](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes) for how every command uses these codes.

## Related [#related]

- [Set up verbatra with an AI agent](https://verbatra.kreitz-webdev.de/docs/start-with-ai.md) is the agent prompt built on this command.
- [Quickstart](https://verbatra.kreitz-webdev.de/docs/quickstart.md) walks from init to the first run.
- [The config file](https://verbatra.kreitz-webdev.de/docs/config-file.md) documents every key the scaffold sets.
- [Providers](https://verbatra.kreitz-webdev.de/docs/providers.md) covers the provider options and API key variables.
