Install
The CLI is one file with no dependencies. It needs Node 20 or newer and talks to the REST API.
npx lvndr status # run without installing
npm i -g lvndr # or install the `lvndr` command
# from a checkout of the agent kit (kasazen/lvndr-agent)
node cli/lvndr.mjs statusIn the LVNDR app repository the same file is agent-kit/cli/lvndr.mjs.
Log in
Make a key in Settings > Agents & API (pick CLI as the client). Then:
lvndr login # asks for the key (not echoed), checks it with GET /me
lvndr login --with-token < key.txt # reads the key from stdin, for scripts
lvndr login --key lv_... --profile staging --url https://my-tunnel.example.comlvndr login writes ~/.config/lvndr/config.json with mode 600 ($XDG_CONFIG_HOME and $LVNDR_CONFIG are honoured), one entry per profile. The first profile saved becomes the default. --no-verify skips the check.
Each setting comes from the first of these that has it:
| Setting | Flag | Environment | Profile | Default |
|---|---|---|---|---|
| API key | --key | LVNDR_API_KEY | key | none (exit 3) |
| App URL | --url | LVNDR_API_URL | url | https://lvndr.toolfora.com |
| Profile | --profile | LVNDR_PROFILE | the default profile, else default | |
| Agent name (X-Lvndr-Agent) | --agent | LVNDR_AGENT | agent | lvndr-cli |
| Run id (X-Lvndr-Run-Id) | --run-id | LVNDR_RUN_ID | none |
Commands
lvndr help (or no command) prints the list; lvndr <command> --help or lvndr help <command> prints one command's usage; lvndr --version the version. An unknown command prints the list and exits 1.
Every write is bound to a plan someone saw. Undo commands preview and print the exact write command with its --plan-hash; --run without --plan-hash writes nothing and fails with preview first: lvndr undo <id>. A rollback needs --confirm --plan-hash from lvndr preview. --yes previews, prints the preview once, asks Apply? [y/N] and writes only on yes; it is for a person at a terminal and is refused when stdin is not one. A preview with nothing to write says “Nothing to undo” and prints no write command. Previews list each field with its record, the field in plain words and both values.
| Command | What it does |
|---|---|
| lvndr me | Store, key label, scopes, plan and key expiry |
| lvndr status | Plan, tracked counts, last activity check, pending deliveries |
| lvndr changes [filters] | Changes, newest first |
| lvndr change <id> | One change with its diff and attribution tier |
| lvndr history <gid | Type-digits> | A record's stored versions and changes |
| lvndr undo <id> [--run --plan-hash h | --yes] [--include-conflicts] [--reason text] | Preview the undo of one change, or write the previewed plan |
| lvndr batch <id> | Everything the same actor did around a change |
| lvndr undo-batch <id> [--run --plan-hash h | --yes] [--reason text] | Preview or undo that batch, newest first; waits for the job |
| lvndr points [--all | --kind k] [--limit n] [--cursor c] | Restore points (the daily check's only with --all) |
| lvndr mark <label...> [--reason text] | Mark a restore point now |
| lvndr preview <pointId> | What rolling back to a point would undo |
| lvndr rollback <pointId> (--confirm --plan-hash h | --yes) [--reason text] | Undo everything after the point, newest first; waits for the job |
| lvndr job <id> | One background undo job: status, counts and results |
| lvndr alerts [--all] | Open anomaly alerts; --all adds dismissed and resolved ones |
| lvndr dismiss <alertId> | Dismiss an alert (needs the Undo scope) |
| lvndr run start [--agent name] [--task text] [--client-id id] [--no-restore-point] | Open a run with a restore point; saved as the profile's current run; prints what it will capture and a WARNING when it captures nothing |
| lvndr run end [id] | End the run |
| lvndr run show [id] | The run and its changes |
| lvndr run list | Recent runs (lvndr runs is the same) |
| lvndr run undo [id] [--run --plan-hash h | --yes] [--reason text] | Preview or undo every change in the run; waits for the job |
| lvndr export [--format csv|json] [--since] [--until] [--out file] | Download the change log |
| lvndr guard [--agent name] [--task text] [--wait 300] -- <command...> | Run a command inside a run with a restore point, then wait until its changes are attributed |
| lvndr login [--key k | --with-token] | Save a profile (the key is not echoed; --with-token reads stdin) |
| lvndr openapi | Print the OpenAPI document |
lvndr me
Check a key before anything else. Exit 3 means the key is missing, revoked or expired; exit 8 means the store has no active plan with the API.
$ lvndr me
example.myshopify.com key: Claude Code scopes: read,undo plan: Scale expires: 2027-02-11T09:00:00.000Zlvndr status
$ lvndr status
example.myshopify.com plan: Scale
tracked: product 412, collection 18, page 6, blog 1, article 14, menu 3, theme 2, metafield_definition 9
last activity check: 2026-11-10T14:05:00.000Z pending deliveries: 0lvndr changes
One line each: id, time, type, action, title, actor, summary. The actor column is the label, name or app title, else the tier.
| Flag | Meaning |
|---|---|
| --since, --until | 30m, 24h, 7d, 2w or an ISO timestamp |
| --type | PRODUCT, COLLECTION, PAGE, BLOG, ARTICLE, MENU, THEME, METAFIELD_DEFINITION, any case |
| --action | create, update or delete |
| --resource-id | Product-123 or gid://shopify/Product/123 |
| --actor, --q | Part of the actor name; part of the record title |
| --hide-undos | Leave out LVNDR's own undos |
| --run-id | Only changes in this run |
| --limit, --cursor | Page size 1 to 100; the next page. When there is more, the next command is printed to stderr. |
$ lvndr changes --since 24h --type product --limit 2
cm3abc0000000000000000001 2026-11-10 14:03:12 PRODUCT update The Minimal Snowboard Joe Pace The price of one variant went from 885.95 to 750.
cm3abc0000000000000000002 2026-11-10 13:41:50 PRODUCT create Gift Card Shopify CLI Connector A new recordlvndr change, lvndr history, lvndr batch
$ lvndr change cm3abc0000000000000000001
PRODUCT The Minimal Snowboard (gid://shopify/Product/123)
update at 2026-11-10T14:03:12.000Z by Joe Pace [exact]
The price of one variant went from 885.95 to 750.
variants[ProductVariant/456].price: "885.95" -> "750.00"
$ lvndr history Product-123
$ lvndr batch cm3abc0000000000000000002lvndr undo, lvndr undo-batch
$ lvndr undo cm3abc0000000000000000001
ready variants[ProductVariant/456].price: "750.00" -> "885.95"
ready 1, conflict 0, noop 0, unsupported 0
preview only; nothing was written.
To run exactly this plan: lvndr undo cm3abc0000000000000000001 --run --plan-hash q3Xl0c2V4Zk9b1Rm
$ lvndr undo cm3abc0000000000000000001 --run --plan-hash q3Xl0c2V4Zk9b1Rm --reason "bad import"
ready variants[ProductVariant/456].price: "750.00" -> "885.95"
ready 1, conflict 0, noop 0, unsupported 0
undo done (cm9un...)
ok productVariantsBulkUpdateThe preview ends with the command that runs exactly that plan: lvndr undo <id> --run --plan-hash <hash>. --run without --plan-hash is refused before anything is sent. If the store moved since the preview, the undo is refused with plan_changed (exit 7); preview again.
--include-conflicts also overwrites fields changed again since. A failed operation prints ERR with the message and exits 2.
Background jobs: undo-batch, run undo, rollback, LVNDR job
undo-batch --run --plan-hash, run undo --run --plan-hash and rollback --confirm --plan-hash (or --yes) start a background job on the server. The job starts on the server at once; the CLI polls GET /api/v1/jobs/:id as often as the job's pollAfterMs says (waiting out Retry-After on a 429) until it is done, partial or failed. Progress goes to stderr when it moves; the finished job goes to stdout, one JSON line with --json. Exit 0 when the job is done, 2 when it is partial or failed.
$ lvndr run undo cm9run0000000000000000001 --run --plan-hash yWsE5uJhTgDq3Xl0
lvndr: job cm9job0000000000000000001 queued: 0 done, 0 failed, 0 skipped of 3
lvndr: job cm9job0000000000000000001 running: 1 done, 0 failed, 0 skipped of 3
job cm9job0000000000000000001 done: 3 done, 0 failed, 0 skipped of 3
cm9ch00000000000000000001 done update The Minimal Snowboard ...
$ lvndr job cm9job0000000000000000001Stopping the CLI does not stop the job; lvndr job <id> shows it later (exit 2 when it ended partial or failed). The previews of a batch, a run and a rollback print their --plan-hash command too; the job is refused if changes joined or left the set since the preview.
lvndr points, lvndr mark, lvndr preview, lvndr rollback
$ lvndr mark before the price update
marked cm3p10000000000000000000c at 2026-11-10T14:10:00.000Z before the price update
$ lvndr preview cm3p10000000000000000000c
...
To roll back exactly this: lvndr rollback cm3p10000000000000000000c --confirm --plan-hash N8pA7wYsE5uJhTgD
$ lvndr rollback cm3p10000000000000000000c --confirm --plan-hash N8pA7wYsE5uJhTgD --reason "import went wrong"Rolling back needs a key with the rollback scope, which new keys do not have unless you tick it. Marking a restore point needs only read. Without --confirm and --plan-hash, rollback refuses and points at preview.
lvndr alerts, lvndr dismiss
Each alert line ends with the lvndr batch command for its burst.
lvndr run
$ lvndr run start --agent nightly-import --task "supplier feed"
run cm9run0000000000000000001 started, restore point cm9pt00000000000000000001
capture all-unattributed: This run records every change Shopify left unattributed or LVNDR inferred while it is open (up to 24 h), staff edits included.
export LVNDR_RUN_ID=cm9run0000000000000000001
$ lvndr run end
run cm9run0000000000000000001 ended; 3 change(s) so far
Changes arrive over a few minutes; review with: lvndr run show cm9run0000000000000000001
To preview the undo: lvndr run undo cm9run0000000000000000001For run end, run show and run undo the id defaults to --run-id, then LVNDR_RUN_ID, then the run saved by run start. --no-restore-point starts a run without a point. Starting a run needs only read, but only a key with undo opens a catch-all run (all-unattributed); otherwise it captures its own OAuth client's changes (client) or nothing (none).
lvndr guard
Checks that the command exists (a typo leaves no run behind), starts a run with a restore point, runs the command with inherited stdio and LVNDR_RUN_ID and LVNDR_AGENT in its environment, ends the run, then polls the run about every 20 seconds until attributedThrough is after the run's end and pendingDeliveries is 0, for at most --wait seconds (default 300; --wait 0 skips it). Then it lists the run's changes and prints the undo preview line. Its own report goes to stderr, so the command's output is untouched. If the run cannot be started, the command is not run. The agent defaults to lvndr-guard; the task to the command line.
$ lvndr guard --agent bulk-price-fix -- shopify store execute --allow-mutations \
--store example.myshopify.com --query-file raise-prices.graphql
lvndr: run cm9run0000000000000000001 started, restore point cm9pt00000000000000000001
... shopify output ...
lvndr: command exited 0; run cm9run0000000000000000001 ended; 3 change(s) tagged
cm9ch00000000000000000001 2026-11-13 10:02:11 PRODUCT update The Minimal Snowboard ...
To preview the undo: lvndr run undo cm9run0000000000000000001 (it prints the --run --plan-hash line that runs it)While it waits, guard prints each change as it is tagged, the seconds waited and what it is still waiting for. Guard exits with the command's exit code (127 when the command does not exist). If attribution has not caught up when the wait ends, guard says so and the list says “tagged so far”. Collection rules and SEO, menus, themes and metafield definitions can take up to 10 minutes, longer than the default wait; lvndr run show <id> lists them again. With --json the report is one JSON line on stderr.
lvndr export, lvndr openapi, lvndr login
lvndr export --format json --since 30d --out changes.json
lvndr openapi > openapi.jsonGlobal flags
| Flag | Effect |
|---|---|
| --json | The API response as one JSON line on stdout. Errors become {"error": {...}} on stdout. |
| --ndjson | For list commands, one JSON object per line |
| --quiet, -q | Only ids (mark, run start, lists) |
| --key, --url, --profile, --agent, --run-id | See the settings table |
| --no-retry | Fail on the first 429 instead of waiting Retry-After (up to 3 retries of 60 s at most) |
| --timeout <s> | Request timeout, default 120 |
| --plan-hash <h> | Required with --run (and with rollback --confirm): the preview that printed it |
| --yes | Preview, print it, ask Apply? [y/N], then write it; only when stdin is a terminal |
| --help, -h; --version, -v | Help (for one command after its name); version |
Every POST carries a fresh Idempotency-Key, reused on its retries, so a retried write is not applied twice. Every request sends X-Lvndr-Agent, and X-Lvndr-Run-Id when a run id is set.
Exit codes
| Code | Meaning |
|---|---|
| 0 | OK. A preview is OK. |
| 1 | Usage error (including --run without --plan-hash), bad parameter (400), or server error |
| 2 | An undo, batch undo, run undo or rollback finished partial or failed (also lvndr job on such a job) |
| 3 | Auth: no key, 401, 403 (missing scope, app uninstalled) |
| 4 | Not found (404) |
| 5 | Rate limited (429) after retries |
| 6 | Network: the app could not be reached |
| 7 | Conflict (409): plan_changed (preview again) or an undo already running |
| 8 | Plan required (402): the store's LVNDR plan does not include the API |
lvndr guard exits with the wrapped command's code instead. See the error codes in REST API.
Scripting
# ids of everything changed in the last hour by an inferred actor
lvndr changes --since 1h --ndjson | jq -r 'select(.actor.tier == "inferred") | .id'
# a run around a multi-step job
RUN=$(lvndr run start --agent nightly-import --task "supplier feed" --quiet)
export LVNDR_RUN_ID=$RUN
./import.sh
lvndr run end
lvndr run show "$RUN" --json | jq '.changes | length'
# fail a pipeline when an undo does not complete
HASH=$(lvndr undo "$CHANGE" --json | jq -r .planHash)
lvndr undo "$CHANGE" --run --plan-hash "$HASH" --reason "bad import" --quiet || echo "undo exit $?"