CLI Command Reference
Every stringlane command and flag, what each one writes, and the exit code it sets. Transcribed from the shipped binary at version 0.9.0.
Last updated
Every command except upgrade and telemetry takes an optional [project] argument, which defaults to the current directory. Those two are not about a folder and refuse one with exit 2.
This page is transcribed from stringlane --help and each command's own --help at version 0.9.0. Where it disagrees with your terminal, your terminal is right: run stringlane <command> --help.
Reading commands#
These write nothing inside your project: not a lock file, not a re-saved locale file, not a byte. They do write to StringLane's own state directory, outside your repository, so there is nothing to gitignore:
install.json, a random install ID, created by the first command on a machine. Not whenDO_NOT_TRACK=1,STRINGLANE_TELEMETRY=0or a CI runner turns usage data off: then nothing is written to the state directory at all. A storedstringlane telemetry offdoes not stop the file being created; it stops anything being sent.update-check.json, the date and result of the update check. The background check does not run whenSTRINGLANE_NO_UPDATE_CHECK=1,DO_NOT_TRACK=1,STRINGLANE_TELEMETRY=0or a CI runner turns it off;stringlane upgradewrites it regardless.- From
checkandscan,projects/<hash>/usage-cache-v1.json: each scanned source file's path inside the project, size and modification time, and every key name found in it with its line number.
STRINGLANE_STATE_HOME moves the directory. Install and update lists everything it can hold.
stringlane inspect [project]#
Reports what the project holds: every locale, how complete it is, and which files were read.
stringlane check [project]#
The inspect report plus every validation finding, and an exit code.
check also runs the source-code scan described under scan. Its two findings are reported at info severity, so adopting a newer CLI does not turn a pipeline red and a recorded baseline does not flip. Opt in with --fail-on unused_key, or skip the scan entirely with --no-scan. Passing both is refused rather than accepted: with the scan off nothing can emit that code, so the gate could never fire.
--positions adds the file and line to each finding, as de/app_name (Localizable.xcstrings:15) in the text report and a position object with file and line in --json.
If .stringlane/baseline.yaml exists it is read automatically, and only findings not in it decide the exit code. --fail-on filters what is left, so an accepted finding stays accepted either way.
--fail-on decides which findings the exit code is computed from, in both directions. Naming a code that is present exits 1, even for an info finding that would not fail on its own, and naming codes that are all absent exits 0. It cannot change a 2: a run that could not read your project still exits 2 whatever you pass.
With sourceStamps: true in stringlane.yaml, check also reports translation_stale, a translation whose base text changed since it was confirmed, and translation_unconfirmed, one nobody confirmed or whose text changed since. Both are warnings, so a stale translation makes check exit 1, and both work with --fail-on. stringlane accept confirms them. See the validation codes.
check also exits 2 in three cases that come from source stamps:
sourceStamps: trueis set and.stringlane/source-stamps.jsonis missing, unreadable, malformed or not a plain file. In CI that usually means the file was never committed, or a.gitignorehides it.- The baseline holds a
translation_staleortranslation_unconfirmedentry. A baseline cannot hide a stale translation; runstringlane baseline writeagain to regenerate it. --fail-onnamestranslation_staleortranslation_unconfirmedwhilesourceStampsis off, so the gate could never fire.
When the skills stringlane setup skills wrote are older than the CLI running check, it prints one line about it on stderr. The report, --json, --output and the exit code are unaffected.
stringlane plan [project]#
The exact list of what needs translating, for an agent to work from.
--out alone writes the human report. Pass --json as well to write the plan itself, which is the file validate and apply read. It refuses any path this project loaded as a translation file.
Exit 0 even when there is work to do, and 1 only when a file failed to parse.
An empty plan says why it is empty, because zero items can mean five different things and each needs a different next step. --json carries the reason as emptyReason:
Only complete means the project is done. With sourceStamps: true, complete means nothing is left to translate; stringlane check can still list translations a person has to confirm.
A cell an earlier apply wrote that a revert emptied again is offered as missing. A translation whose base text changed since it was confirmed is offered again as source-changed, and one an earlier apply wrote whose context or description changed since as context-changed. Without sourceStamps: true, only cells an apply wrote are covered. With it, a translation the stamps file records as confirmed is offered once its base text moves, carrying the old text as previousSourceValue when the stamps file holds it; one never confirmed is reported by check instead.
stringlane validate [project]#
Checks an agent's translations without touching a file.
Exit 0 clean, 1 the candidates have problems, 2 could not run. A clean validate is not permission to write: see How the write gate works.
stringlane scan [project]#
Reads your source code and reports two things: keys the project defines that no call site names, and keys the code calls that the project never defined.
The text report caps both lists so its caveat stays on screen; --json carries them whole. Both flags add to ignoreKeyPatterns and ignoreFilePatterns in stringlane.yaml rather than replacing them.
"Not referenced" is what the scanner saw, not a verdict. See Unused keys for what it cannot see and why it refuses rather than guessing.
Writing commands#
Each asks before writing, and refuses when there is no terminal to ask at unless --yes is passed.
stringlane init [project]#
Detects the project and writes stringlane.yaml, with source stamps on for a new config.
In a folder with no stringlane.yaml, init writes two files after one question that names both:
.stringlane/source-stamps.json, written first. It stamps every translation the project already holds as current against today's base text, and says so: StringLane cannot know whether they were current before today. A String Catalog unit Xcode marksneeds_reviewis left unstamped, andinitsays how many.stringlane.yaml, written last, withsourceStamps: true.
Commit both. With the flag on, check, plan and apply read the stamps file, and a project without it exits 2. init warns when a line in the project's own .gitignore hides .stringlane/ or the stamps file. A project that did not fully load gets a stringlane.yaml without the flag and no stamps file. With --force, init replaces an existing stringlane.yaml, writes no stamps file and does not add the flag; an existing sourceStamps line is carried over.
stringlane apply [project]#
Writes an agent's translations into your locale files.
Re-runs every validate check, and additionally verifies under a per-project lock that no file the plan fingerprinted has changed since the plan was made: every locale file the project loads, not only the targets, plus the .stringsdict beside each .strings file, stringlane.yaml and .stringlane/metadata.yaml. From desktop 1.3.0 the app's own saves take the same lock, so an apply and the app never write one project at the same time. All or nothing. Exit 0 applied, 1 the candidates have problems, 2 could not run.
With sourceStamps: true it also writes .stringlane/source-stamps.json, and exits 2 when that file is missing, unreadable, malformed or not a plain file.
Before opening any file, apply and validate both refuse a plan that names a file the project does not manage: a locale file deleted, renamed or broken since the plan was made, or a plan edited by hand. That is STALE_PLAN, exit 2. Build a new plan.
stringlane update [project]#
Translates the missing strings using your own AI provider, then writes them through the same gate.
The only command that spends money. Keys are read from the environment only: see Translate with your own key.
--parallelism wins over STRINGLANE_AI_PARALLELISM. A value that is not a whole number from 1 to 8 exits 2 before any credential is read or any request is made; it is never clamped. The spend question says how many items replace a translation that is already there, and --json carries that count as retranslatedCount.
stringlane describe [project]#
Writes key descriptions, so translations are made with context.
In a project that spreads its base locale across several files, such as a Gradle project with several modules, describe refuses with exit 2 and writes nothing, because it cannot yet choose which file each description belongs in. Add those descriptions in the desktop app. A project split into i18next namespaces is not refused: its descriptions go to .stringlane/metadata.yaml. One unknown key refuses the whole set.
stringlane add-locale [project]#
Creates the file for a locale this project does not have yet, in the right place and format.
The file it creates is empty apart from the locale marker, on purpose: every key then shows up as missing in the next plan, rather than looking like translations you chose to leave empty. In a project with more than one origin it creates one file per origin, in a single atomic write: either the locale exists everywhere it should or it exists nowhere. It does not make your app offer the language.
stringlane add-key [project]#
Adds a new key and its source string to the base locale.
Base locale only: every other locale then reports the key as missing in the next plan, and the ordinary plan, validate and apply chain fills it. The confirmation prompt names the key and the string. --key and --value are both required, because an empty value is what an untranslated cell looks like. --description is optional and is stored where the format keeps one. A key name the format cannot carry is refused before it breaks a build.
In a project that spreads its keys across several files, add-key refuses with exit 2 and writes nothing, because it cannot yet choose which file a new key belongs in. That includes a project split into i18next namespaces as well as a Gradle project with several modules. Add the key in the desktop app, then plan again.
stringlane pluralize [project]#
Replaces a key's single string with its plural forms in the base locale. It fixes a count_without_plural finding, and works in i18next projects only.
stringlane pluralize . --key translation:savedSearches.nGenres \
--one "{{count}} genre" --other "{{count}} genres" --yesAdded plural forms (one, other) to "translation:savedSearches.nGenres" in "public/locales/en/translation.json".
Still to translate:
"ru": one, few, many, other
Run stringlane update, or prepare_translation_plan over MCP, to fill them.Base file only: every other locale keeps its old string and reports the new forms as missing, and stringlane update, or stringlane plan then stringlane apply, fills them. In a project split into i18next namespaces the forms go into the file that holds the key. A missing or empty form, a form the language does not use, a form that drops a placeholder the current text has or adds one it does not, a form that writes a placeholder differently from the current text ({{count}} where it has {{- count}}), and a key that already has plural forms are each refused, and nothing is written. The one form may leave out the count, as in one genre; the other form may not. The confirmation prompt shows the file and the text of each form. With --json and no --yes it prints what it would change and writes nothing, even on a terminal. Exit 0 when it wrote, previewed or you declined; 2 when it refused or could not run. With no terminal it refuses unless you pass --yes. It needs no AI provider key.
stringlane accept [project]#
Confirms translations against the current base text, so check stops reporting them as translation_stale or translation_unconfirmed. It writes .stringlane/source-stamps.json.
stringlane accept . --all
stringlane accept . --key checkout.title --locale de --locale frIt needs sourceStamps: true in stringlane.yaml and never edits that file: without the flag it prints the line to add and exits 2. It creates the stamps file when there is none yet, and refuses a stamps file it cannot trust rather than overwriting it. Different selector flags narrow each other, so --key a --locale de is key a in German only, and without --quantity every plural form of the selected keys is included. --all stamps translations nobody checked, and says so: StringLane cannot know whether they were current before today.
It asks first, counting what it will mark current per locale by its status now. With no terminal it refuses unless you pass --yes. For an .xcstrings unit Xcode marks needs_review, named with --key, it also sets the unit's state in the catalog to translated; --all leaves those units alone and says how many. --yes typed in a shell is not visible to the write guard that makes an agent ask before it edits your files, so review the stamps file in the pull request. There is no MCP tool for accept, on purpose. Exit 0 written or declined, 2 refused or could not run.
stringlane context [project]#
Shows, or sets, the product, branding, locale and terminology context stored in stringlane.yaml.
stringlane baseline write [project]#
Records today's issues to .stringlane/baseline.yaml, so CI can start clean.
It records every finding except translation_stale and translation_unconfirmed, and says how many it left out, because one recorded staleness would silence every later one for that cell. Confirm those translations with stringlane accept. check refuses a baseline that holds either code and exits 2.
Agent and setup commands#
stringlane mcp [project]#
Serves the project to an AI agent over MCP on stdio. A server, not a command: it stays running until the agent disconnects. It has no flags of its own.
The --host values#
Six commands take --host, and they do not all accept the same names:
A name a command does not take is refused with exit 2, and the message lists the ones it does.
setup mcp prints a real configuration for the exact file each of the eight hosts reads. What differs is whether --apply then writes it. StringLane will not merge into another program's config file until somebody has run that program and recorded what it actually writes, and today that is claude-code. On the other seven, --apply prints and exits non-zero: the entry is still there to copy, and the exit code is the command telling you it changed nothing.
Three exceptions, all deliberate. setup rules and setup skills apply everywhere, because they write into your repository: AGENTS.md, and .agents/skills. --remove works everywhere too: refusing to install is cautious, and refusing to uninstall is a trap. See Agent hosts.
stringlane setup mcp#
Shows, or with --apply writes, your agent's MCP configuration.
Without --apply it prints the entry and the exact file, and writes nothing. The entry is given in that host's own shape: these hosts want it under four different container keys, and one under the wrong key is not an error on any of them. The host starts, lists no StringLane tools, and says nothing about why. With --apply on Claude Code it backs the file up first and tells you where the backup went. Every report ends by naming the command that installs that host's write guard, which is a separate step on every host but the two plugin hosts: stringlane setup hooks --host <host>, or stringlane setup guard --host zed|opencode.
stringlane setup hooks#
Shows, or with --apply writes, the write guard in your agent's settings.
Six hosts speak a hook StringLane answers: Claude Code, Codex, VS Code Copilot, Gemini CLI, Cursor and Windsurf. On Codex, Cursor, Gemini CLI and Windsurf there is no "ask", so the guard refuses the edit and the refusal names the way out: --remove for a hook you pasted, /hooks as well on Codex, and the plugin under Customize for the hook the Cursor plugin ships. VS Code's hook installs but guards nothing yet, and the command prints that warning under the document. Zed and OpenCode have no hook StringLane speaks; setup guard is theirs.
stringlane setup rules#
Shows, or with --apply writes, three localization rules into your AGENTS.md, between two HTML-comment markers.
Everything outside the markers is left byte for byte as it was, and running it twice still leaves one block. It refuses rather than guessing when the markers are ambiguous, and names the line numbers so you can see what it saw. With setup skills, it is one of the two setup commands whose --apply writes on every host.
stringlane setup skills [project]#
Shows, or with --apply writes, the agent plugin's skills into your repository's .agents/skills, one stringlane-* folder each. It is for agents with no StringLane plugin.
Every file is stamped with the CLI version that wrote it, so running --apply again after an upgrade replaces a stale copy. A folder without the stamp, a copy somebody has edited, or a copy a newer CLI wrote is left alone and named, so a teammate a release behind cannot downgrade the committed files. --remove deletes only unedited folders that carry the stamp. StringLane's host table records every host but Claude Code as reading .agents/skills, but none has been confirmed doing so in a recorded session, so check that the skills show up in yours.
stringlane setup guard#
Prints the permission block that makes Zed or OpenCode confirm before an agent edits a locale file, in that host's own pattern language. Zed's patterns are regular expressions, OpenCode's are globs.
Print only: both files are JSONC, and a JSON writer would discard your comments on the way back out. It is a snapshot of the files loaded when you ran it, so a locale file added later is not covered until you run it again, and the block says so in its own first line. Exit 0 with the block on stdout; 2 if you passed --apply, or if the folder is not a project or has no locale files.
stringlane hook#
Answers an agent host's pre-write hook. Reads stdin; not run by hand. It answers for every host that has a dialect, recorded or not, because a printed hook document pointing at a command that refuses it would be a dead end rather than an escape hatch.
stringlane upgrade#
Asks the npm registry what the latest version is and prints the command to install it. It never installs anything itself.
Most commands also check quietly, at most once a day, printing a one-off notice on stderr when you are behind. hook never does, because it runs on every file an agent touches, and mcp checks when it starts rather than when it ends. Any of STRINGLANE_NO_UPDATE_CHECK=1, DO_NOT_TRACK=1, STRINGLANE_TELEMETRY=0, or a CI runner (CI=true, or one of the runner variables below) turns that background check off; a stored stringlane telemetry off does not. This command ignores all of them, because you typed it.
Exit 0 whether you are current or behind, and 3 when the registry gave no answer. Not 2, which would say you invoked it wrong.
stringlane telemetry status | on | off#
Reports or changes whether usage data is sent. status also shows the random install ID every event carries, and names any environment variable currently overriding the stored setting. Under DO_NOT_TRACK=1, STRINGLANE_TELEMETRY=0 or a CI environment, status creates no ID and shows none on a machine that has none; without one of those, the first command run on a machine creates the ID, status included. Writes only to StringLane's own state directory, never to your project.
Global flags#
Environment variables#
No environment variable can turn telemetry on over a stored opt-out.
Free · In your coding agent
Run the same checks from your coding agent
Paste this into Claude Code, Cursor, Codex or five other agents. It sets up the CLI and MCP server for this project, proposing each command and waiting for your yes.
Read https://stringlane.app/docs/cli/agent-setup.md and set up StringLane for this project.Or install it yourself:
npm install -g @stringlane/cli