CLI Troubleshooting and Exit Codes

Use this page by searching for the literal string the CLI gave you. Every message below is reproduced exactly as it is printed — including two with typos in them, which are quoted verbatim because that is what you will paste into a search box. The Cause column is where the plain-English explanation lives.

Most failures set the process exit code rather than exiting immediately, so the work that runs after a command — including the once-a-day update check — still happens. And one rule governs everything else here: JSON mode does not change the exit code. The document’s ok field and the process exit code are two separate signals, and they do not always agree.

Exit codes

Codeok in the JSON documentMeaningEmitted byWhat to do
0trueSuccess. Also “No changes have been committed. Nothing to push.”, a completed --dry-run, and an empty commit set on notify-cmsevery commandNothing. Note the second column of meanings if you are scripting: a no-op is a success
1falseUnclassified — an unexpected fault in the CLI itself (internal_error)every commandRead the error, and report it with the whole document
2falseUsage — the call itself is wrong: an unknown or invalid flag, an environment, company or site nobody named, or a page, page type or command that does not exist (invalid_arguments, not_found)every command, plus the argument parserFix the command line. error.hint is usually the same command with the missing flag added, and leed schema <command> --json gives the real flags
3falsePrecondition unmet — validation, restricted files, wrong branch, a failed build, a rebase conflict, a taken --serve port, a refused git command, a partial upload, not in a site checkout, or a sandbox site that has no CMS company (sandbox_site)every commandFollow error.hint
4falseTransient — the CMS or the git host could not be reached, or answered with a server error (network_error)every command that talks to the networkRetry with backoff
5falseNeeds a person — not signed in, session expired, permission denied, a request Cloudflare’s firewall blocked (request_blocked), a confirmation or an interactive terminal requiredevery commandStop and ask. Retrying fails identically
non-zerotrueThe command’s main work succeeded and a step after it failed: leed site upload when the build-result callback was not delivered, leed site push when the CMS was not notified, a settings publish or page write that succeeded in part. The exit is the failed step’s classthose commandsRead warnings. This row is why nothing may treat ok and the exit code as one signal
18no document is writtenA git hook rejected the command. Always husky, never the CLI itself.husky/pre-commit, pre-push, commit-msgUse leed site commit / leed site push instead of raw git
130 / 143true, with a warningA leed site build --wrangler session interrupted with Ctrl+C or SIGTERMleed site build --wranglerNothing; you stopped it
1n/aThe one-line installer’s fatal(), under set -euo pipefailthe installer shell scriptRead the last line it printed; the installer messages are cataloged below

Branching on failure, not on the exit code

This is the rule to take away from the page.

The exit code is 0 on success and names the kind of failure otherwise: 2 fix the call, 3 satisfy a precondition, 4 retry, 5 ask a person, 1 report a bug. A command may exit non-zero while reporting ok: true when a non-fatal step failed after the result was already determined; the detail is in warnings.

The exit class is derived from error.code by one table, so every failure document also carries it as error.exit, and error.retryable says whether repeating the call unchanged can succeed. The older per-command numbers — 8 and 9 from notify-cms, 2 for every CI-shaped refusal — are retired. A script that tested if [ $? -eq 1 ] would have missed most failures then and misreads them now; test the class, or parse the document.

In JSON mode, the reliable shape is:

doc=$(leed site push --json) || true
if [ "$(printf '%s' "$doc" | jq -r .ok)" = "true" ]; then
  printf '%s' "$doc" | jq -r '.warnings[]?'
else
  printf '%s' "$doc" | jq -r '.error.code, .error.message, .error.hint'
fi

Parse stdout, read ok, branch on error.code. The code enum is closed, so a switch over it can be exhaustive — the seventeen codes, their exit classes and what each one means are on the machine-readable output page, along with the envelope those fields live in.

“You cannot ‘commit’ directly”

This is the error most people meet in their first week, and it is not a bug.

	You cannot 'commit' directly, please use:

		leed site commit -m "update details"

The process exits 18, and nothing is committed.

leed site init installs three husky hooks into your repository: pre-commit, pre-push and commit-msg. The first two run git config --get leed.invoked and refuse unless it reads true — a flag only the CLI sets, and only for the duration of its own git call. A git push you run yourself gets the same treatment with leed site push in the message.

The third hook checks the message itself. A commit message that does not begin @leed/site-management=> and carry a JSON payload with userId, version and message is rejected the same way:

	You cannot commit directly to this repo, please use:

		leed site commit -m "reason for update"

The rule exists because a commit that skipped the CLI skips three things the platform depends on: file validation, the build gate, and the metadata the CMS reads to attribute the change and trace a failed build back to a CLI release. The reasoning in full is on validate, commit and push.

Only committing and pushing are intercepted. git status, git log, git diff, git branch, git checkout and git pull --rebase all behave normally, and you are meant to use them.

Setup and authentication problems

Message (verbatim)CommandCauseFix
Site configuration not initialized. Run `leed site init`!!!any leed site commandThe site’s entry in ~/.config/leed/sites.json still has initialized: falseRun leed site init
Site configuration already complete! Try running a build `leed site build`leed site initThe site is already set up at that checkoutNothing is wrong. Build instead
This is not a site checkout. Pass --site to name the site to work on. Registered sites:any leed site commandYou are outside every checkout. The CLI never picks a site for you, even when only one is registeredcd into the checkout, or pass the --site the hint names
No Leed site found at or above <folder>.any leed site commandYou are outside every checkout and no site is registered on this machineRun it inside your checkout, or set the site up with leed setup
<folder> is already a site checkout; leed site init would clone another one inside it.leed site initA full init was run inside a checkoutRun it elsewhere, or pass --dir <folder>
Name the site to set up: pass --company <companyId|domain> and -e <env>.leed site initNothing names the site to set upPass --company and -e, as the hint shows
This command must be run from inside a Leed site checkout.leed auth rotate, leed auth resetBoth are site-scoped — they rotate the token of the site you are standing incd into the checkout; leed site list shows where it is
No environment named. Pass -e <env>, or run the command inside a site checkout.any command that takes an environment, run outside a checkoutNothing names an environment, and none is assumedPass -e, or run it inside the checkout
No company selected for <env>. (followed by the companies you are logged in to)any command that needs a company outside a checkout, leed auth logoutThere is no default company, and nothing names onePass --company, or run from the checkout
<company> is logged in to production and staging. Pass -e to pick one.any command given --company without -eThe company has sessions in several environmentsAdd -e
Setup does not pick a saved <env> login on its own.leed setup under --json, --check or with no terminalLogins are saved and none was namedPass --company <companyId>, or --new-login
Could not determine the target environment. (the message goes on to name leed site list and ENV_NAME)leed site push, commit, notify-cms, uploadThe checkout’s identity.json names no environment and ENV_NAME is unset or invalid. These commands refuse rather than assume productionRun from a registered checkout, or set ENV_NAME to dev, staging or production
Not authenticated for <env>. Run: leed auth login --env <env>any authenticated commandNo session stored for the resolved environment and companyLog in for that environment and company, not another one
No company was selected for this login. Run: leed auth login --env <env> --company <companyId>leed auth loginThe approval bound the session to no company — the account belongs to several and none was picked. The session is kept as an unassigned legacy sessionSign in again with --company
The stored session for <env> belongs to company <id>, not <id>.leed site initThe site you named is a different company from the session you holdleed auth login --env <env> --company <companyId>
The credential file <path> is unreadable.any command that reads credentials~/.config/leed/auth.json is corrupt. It is reported rather than read as “not logged in”, so nothing overwrites itFix or remove the file, then leed auth login
<path> was written by a newer Leed CLI (format …)any command that records a site~/.config/leed/sites.json comes from a newer CLI, so it is left unchangedleed self-update
git 2.31 or newer is required to authenticate against the content repository.leed site init, leed site pushThe repository token reaches git through config environment variables that older git ignoresUpgrade git
Not authenticated.leed site initNo credentials at allleed auth login --browser --env <env>
Session expired or invalid.leed site init, leed auth rotateThe stored token is past the refresh grace windowLog in again
Session expired. Run: leed auth login --env <env>leed auth statusThe developer-credentials check got a 401Log in again
This sign-in is bound to no company; sign in again and choose one.leed site initThe session carries no company (HTTP 422)leed auth login --browser --env <env>, and pick the company on the approval page
You don't have developer access to this organization.leed auth rotate, leed auth resetYour account lacks the developer overrideAsk an administrator to grant it
No developer access for this organizationleed auth statusThe same 403, reported in the status listingSame
(no tokens stored yet — run 'leed auth rotate' or rerun the installer)leed auth statusYou are signed in, but no GitLab token has been issued to this machineleed auth rotate
Not logged in to <env>.leed auth logoutNothing was stored for that environmentNothing to do; the command returns successfully

The first two rows are the only messages on this page that the CLI prints with backticks of its own, around the command it is suggesting. They are set in ordinary type here rather than as code so that those backticks survive to the screen — copy the row and the two strings match your terminal character for character:

Site configuration not initialized. Run `leed site init`!!!
Site configuration already complete! Try running a build `leed site build`

“Signed in” and “able to git fetch” are different states, because they rest on different credentials. If leed auth status shows a valid session but git refuses you, the stored raw-content token is the stale one, and leed auth rotate replaces it — the three-credential model is on CLI authentication.

“Your session is no longer valid. Please re-authenticate to continue.” — or, under --json, auth_expired with “The stored session for … is no longer valid.” — means the CMS refused the stored session. A CLI session lasts 90 days from its last use, so that now has three causes: the session went unused for longer than that plus the 24-hour grace; it was ended by leed auth logout, leed auth reset or a revoke from another device; or it is a login from before CLI sessions became 90-day sessions, which the CMS will not renew. Each takes one leed auth login. Running commands in parallel is no longer a cause — they share one renewal under a lock on auth.json, and a token another command has just replaced keeps working for 60 seconds — and a rate-limited session check (HTTP 429) is reported as network_error, exit 4, not as an expired session. Wait a minute and retry it.

Validation, commit and push problems

Message (verbatim)CommandCauseFix
Error: Restricted file violation!leed site validateYou modified a protected path, a read-only file, a disallowed file type, or a file at or over 600 KB. The offending paths are listed under the messageleed site validate --reset restores them. --reset --dry-run lists what it would restore first
Some page types use a layout that does not exist in src/_layouts/:leed site validateA page type names a layout your checkout lacks. Each one is listed underneathAdd the layout under src/_layouts/, or point the page type at one that exists with leed settings pagetype set
Issues were encountered and validation failed!leed site validateThe closing line after any validation failureRead the lines above it
You must run the following command before changes can be committed:leed site commitOne or more modified files have not been validated since you last edited themRun leed site validate
You must run the following command before changes can committed:leed site commitYour last recorded successful build is older than your newest validated edit. The missing “be” is in the product — quoted here so a search matchesRun a plain leed site build, or --serve. A build you ran with -d will not satisfy this
You are only allowed on specific branches. Change back to staging!leed site commit, leed site pushYou are on a branch other than staginggit checkout staging
Run the following commands to complete your setup:leed site commit, leed site push, leed site initGit has no user.name / user.email on this machine, or one is blankRun the two git config --global lines it prints
No changes have been committed. Nothing to push.leed site pushNothing is ahead of origin/stagingNothing to do — and note this exits 0
Could not fetch from origin:leed site pushThe fetch before the rebase failed, usually authenticationleed auth rotate is the hint the command gives
Remote has conflicting changes that can't be auto-rebased.leed site pushSomeone — or the CMS — pushed something your commits cannot replay ontoResolve it by hand, then push again
An error occurred while pushing:leed site pushGit refused the push itselfRead the git output beneath it. If it reads as a credential refusal, leed auth rotate
The CMS was not notified:leed site pushThe commits reached origin/staging and the CMS callback did not. Exit 4 or 5See below — this one has a specific recovery
No HEAD commit recorded. Run 'leed site commit' first.leed site notify-cmsThere is no recorded commit set to reportCommit through the CLI first

A rebase conflict aborts the rebase and leaves your work intact. The recovery is ordinary git:

cd ~/leed/<your-domain-slug>/raw-content
git pull --rebase
# resolve the conflicts, then:
git rebase --continue
leed site push

That git pull --rebase is explicitly permitted by the hooks. Why the repository rebases rather than merges, and who else writes to it, is on Git workflow. Which files validation protects, and why some are restored on sight, is on editable and read-only files.

Build problems

Message (verbatim)CommandCauseFix
Port <n> is already in use, so the development server cannot start on it.leed site build --serveSomething already holds the port — often another --serve, or an editor’s port forwarding. Checked before any build work, coded conflict, exit 3Stop the holder (the hint names an lsof command), or pass --port <number>
bun install failed in build output directory: <path>leed site build --wrangler, CI buildsDependency installation into the generated worker directory failedRead the stderr it prints. Usually the npm registry could not be reached
No wrangler.jsonc found in <dir>. Run 'leed site build' first.leed site uploadThere is no build output to uploadBuild first
[site-build] … on a deployment rownone — this appears in the CMSYour content or a template failed to render during the CMS-side buildReproduce it locally with leed site build, then read when a deployment fails

Build failure output is summarized rather than dumped, which is worth knowing before you conclude that something was swallowed. The CLI scans the subprocess output for lines carrying ✘, [ERROR], a leading Error:, Failed: or fatal:, and keeps each such line together with the indented lines that follow it. When it finds no marker at all it falls back to the raw output. Either way the result is trimmed to its last 2,000 characters, so a very long failure is shown from its end, not its beginning. If that is not enough, re-run with --verbose for the CLI’s own channels, or DEBUG=Eleventy* leed site build for Eleventy’s internals. Both, and the recording rule that decides whether a build counts, are on leed site build.

OpenAPI import problems

Message (verbatim)CommandCauseFix
OpenAPI spec not found: <path>leed import openapiThe path does not exist. Checked before the engine opens anythingCorrect the path
OpenAPI spec must end with one of .json, .yaml, .yml: <path>leed import openapiUnrecognized extensionRename or convert the spec
Could not determine the page type slug.leed import openapi, markdown, htmlNo local pageTypeList.json entry for that page type idCreate it with leed settings pagetype create, then pass --page-type-path on the first staging run
Generated OpenAPI spec file(s) exceed the 26214400-byte limit and cannot be published.leed import openapiA generated spec file is over 25 MBSplit the API across more than one page type
The staged page set is not intact — aborting before any CMS write:leed import openapi, leed import applyAn integrity check on the page set failed. One indented detail line per problem followsFix the source (or the staged menu), then stage again — leed import clean first if a set is already staged
Nothing to import for this page type.leed import applyNothing is staged for that page type idStage first with leed import openapi, markdown or html
The OpenAPI spec changed or is missing since you staged it.leed import applyThe spec file moved, or its bytes changed, since staging recorded its SHA-256Re-run leed import openapi, then build again
Build and review the generated set before importing. …leed import applyNo recorded build is newer than the staging run. The message names which case you are in and prints both timestampsRun leed site build, or --serve to review it. A build you ran with -d is not recorded
Some pages failed. Re-run 'leed import apply' with the same flags to retry — progress is tracked in:leed import applyA partial importRe-run the same command; acknowledged pages are not re-sent
Menu not yet applied.leed import applyThe pages landed, the navigation did not. Treated as incompleteRe-run the same command
Failed to stage OpenAPI spec files: …leed import applyThe page import succeeded; staging the raw spec files did notRe-run leed import apply — it retries only the staging

The --debug trap deserves repeating because it looks like the CLI is ignoring you: a build you run with -d is deliberately not recorded as a successful build, so a developer with -d in their muscle memory can build ten times and still be told to build. (--serve skips minification by itself and still counts.) The whole workflow, with the two guards drawn as a diagram, is on importing an OpenAPI spec; the flags are on OpenAPI commands.

Eject problems

Message (verbatim)CauseFix
No templates are available to customize on your current plan.Every ejectable template is above your tier. Tier resolution is fail-closed, so an unknown plan lists nothingUpgrade, or leave the defaults in place
Unknown template '<name>'. Valid names: …A typo, or a template that does not existRun leed site eject with no argument to list what is available
'<name>' requires the <tier> plan or above; this site is on <tier>.The template exists but is gated. Gated templates are hidden from the listing, so you only see this if you named oneUpgrade
<path> already exists. Pass --force to overwrite your customization.You have already ejected this template and edited itNothing, unless you want to discard your version — then --force
The Leed default for '<name>' is missing at <path>The CLI’s own copy of the source template is absentleed self-update, or re-run the installer, to repair the installation

Each template, what it controls and what you take responsibility for by ejecting it are on leed site eject.

Installer problems

The one-line installer runs under set -euo pipefail, so any of the fatal messages below ends the run with exit 1. Sign-in and site setup are leed setup’s, so their messages are in the tables above.

Message (verbatim)CauseFix
curl is required but not found.No curl on the machineInstall it. You will have seen this only if you piped the script in some other way
git v2.31.0 or later is required but not found. …No gitInstall git as the message describes, and re-run
git v<version> is below the minimum v2.31.0. …Your git is too old to authenticate the content repositoryUpgrade git, and re-run
bun is required, and no terminal is attached to ask before installing it. …No Bun, and nobody to askInstall Bun with the command it prints, then re-run
bun is required. Install it with: curl -fsSL https://bun.sh/install | bash (see https://bun.sh/)You declined the offer to install BunRe-run and accept, or install it yourself
bun v1.1.0 or later is required. Upgrade with: bun upgrade …You declined the offer to update Bun, or no terminal was attached to askRe-run and accept, or upgrade it yourself
The requested leed v<version> is below the minimum v6.18.0 this installer needs. Nothing was installed. …A pinned version too old for leed setupRe-run with a newer version, or without one
leed on your PATH is still the old @leed/site-management package …The old package is earlier on your PATH than the new onebun remove -g @leed/site-management, then re-run
Installed leed v<version>, but this installer needs v6.18.0 or later. …An older leed earlier on your PATH shadows the one just installedCheck command -v leed, or re-run without a version argument
Installation failed — leed command not found.The global install completed but leed is not where Bun puts global binariesCheck bun pm bin -g and add it to your PATH
Failed to parse registry configurationA saved copy of an installer from before the CLI moved to public npmRe-run the curl command, which fetches the current installer
Could not remove @leed/site-management; remove it with: bun remove -g @leed/site-managementNot fatal. The new CLI is installed; the old package is still on diskRun the command it names
Shell tab completion was not installed. Run 'leed completion install' to set it up.Not fatal. Usually a SHELL the CLI does not supportRun the command it names
The Leed CLI is installed. No terminal is attached, so sign-in and site setup were skipped.Not an error — exits 0. Nobody could answer leed setup’s questionsRun the leed setup command it prints, at a terminal

Re-running the installer is safe and idempotent — it skips prerequisites that are already satisfied, keeps a CLI that meets the minimum, and hands off to leed setup, which reuses the saved login you pick and keeps a site that is already set up. The full walkthrough is on installing the Leed CLI.

Blocked requests and sandbox sites

Two refusals are not about your setup at all. request_blocked, exit 5, means Cloudflare’s firewall in front of the CMS mistook a legitimate request for an attack, and it never reached Leed: “… was blocked by Cloudflare’s firewall in front of the CMS (ray <id>). This is not a permissions problem …”. Do not sign in again or change the content to get past it. Retry at most once, then run the leed feedback blocked command in the error’s hint — it carries the ray id, method, path and time — so Leed can fix the false positive. At a terminal the CLI offers to send the report for you.

sandbox_site, exit 3, means you ran a command that needs the CMS inside a local sandbox site, which has no company in Leed: “This is a local sandbox site (non-cms-company), and … needs the Leed CMS.” No login helps. Building, previewing and ejecting work in a sandbox; to publish it, move it into Leed with /leed:site-sandbox-move in Claude.

When there is no document at all

Under --json the contract is exactly one JSON document on stdout, and the three paths that used to break it are closed. An interrupted leed site build --wrangler --json now writes its document, with the interruption in warnings, and exits 130 on Ctrl+C or 143 on SIGTERM. A Tailwind failure during a --json build is reported as a failure document instead of exiting with an empty stdout. And a failure after the action has already emitted no longer appends a second envelope.

An empty stdout therefore means the process was killed outright — by the out-of-memory killer, a forced kill, or a CI runner timing out — before it could write anything. Treat it as a failure, and read stderr for what the process managed to say.

What the document is supposed to look like is on machine-readable output (--json).

When nothing else works

Three repairs, in increasing order of how much they touch. Try them in order.

If none of that helps, a useful report has three things in it: the output of leed --version, the exact command you ran, and the run repeated with --verbose so the leed:* diagnostic channels are on. Add --json if the failure is in a script — error.code and error.details say more than the rendered text does.

When the failure is a wrong flag rather than a wrong state, the command’s signature is at CLI command reference, and every path and variable named on this page is cataloged on files and environment. Errors from the rest of the product — the CMS, the editor, the site build — are collected in common error messages, which links back here rather than duplicating this catalog.

ESC