This is the reference page for two questions: where did that come from and where did that go. It covers the project configuration file, the complete set of files the leed CLI writes on your machine, and every environment variable it reads. Nothing here is something you routinely edit — the value of knowing it is that when a command behaves unexpectedly, one of these three things explains it.
leed.config.json
leed.config.json is your site’s project configuration. The CMS generates it, the installer downloads it, and leed site init finalizes it. It identifies which company and which environment this checkout belongs to, and it records the versions of the files the CLI manages inside your repository.
{
"environment": "production",
"domain": "example.com",
"companyId": "cmp_4a71",
"location": "/home/you/leed",
"userId": "usr_1c8e",
"initialized": true,
"huskyVersion": "4.106.0",
"vscodeVersion": "4.106.0",
"aiConfiguration": {
"claude": {
"skillsVersion": "4.106.0",
"installedSkills": ["site-builder", "site-preview", "site-publish"]
}
}
}| Field | Type | Default | Set by | What reads it |
|---|---|---|---|---|
environment | production, staging or dev | production | the CMS, when it generates the file | Every command: it selects the credentials block and the CMS base URL |
domain | string | required | the CMS, from your company’s public domain | slugify(domain) is the customer slug used for your folder name, your GitLab paths and --base-url’s default |
companyId | string | "" | the CMS | Keys the on-disk caches, so one machine can hold state for more than one company |
location | string | optional | computed at load time, not stored meaningfully | The CLI, to find raw-content/ relative to the config |
userId | string | required | the CMS, from the account that downloaded the file | Embedded in every commit message so the CMS can attribute the change |
initialized | boolean | false | leed site init, which flips it to true | Every leed site command: a false here is what produces “Site configuration not initialized” |
huskyVersion | string | "" | leed site init, and every later resync | The drift check that reinstalls the git hooks after a CLI upgrade |
vscodeVersion | string | "" | leed site init, and every later resync | The drift check that reinstalls the VS Code workspace and tasks |
aiConfiguration.claude.skillsVersion | string | "" | leed site init, and every later resync | The drift check that reinstalls the Claude Code skills |
aiConfiguration.claude.installedSkills | string[] | [] | the skill installer | Lets a resync remove a skill that a newer CLI no longer ships |
Where the CLI looks for it
Exactly two places, in this order:
leed/ <- run from here: ./leed.config.json is found
└── example.com/
├── leed.config.json
├── example.com.code-workspace
└── raw-content/ <- run from here: ../leed.config.json is foundNowhere else. From a third location — your home directory, a subfolder of raw-content/src/, anywhere outside the tree — every leed site command stops with:
Could not locate leed.config.json. Run this application from the directory where your repo exists.There is one asymmetry worth knowing about. The leed auth commands do not load the config file the way leed site does; when they need a default environment they walk three levels — the current directory, its parent and its grandparent — so leed auth status can pick up your site’s environment from one level deeper than a leed site command would.
The fields the CLI maintains
Three of the fields above are drift markers, and they are the reason files reappear in your repository after you upgrade. When you run any leed site command other than init, the CLI compares huskyVersion, vscodeVersion and aiConfiguration.claude.skillsVersion against the version of the CLI that is running. Where they differ it reinstalls the husky hooks, the VS Code workspace and tasks, and the Claude Code skills, then rewrites leed.config.json with the new versions. Nothing asks first, and nothing is destroyed that the CLI did not put there. What gets installed is listed on leed site init.
location is the odd one out: it is filled in every time the file is loaded, from where you actually ran the command, and the value on disk is not consulted. Editing it does nothing.
How the environment is decided
Every command resolves one of production, staging or dev before it does anything else, and the answer decides which credentials block is read and which CMS it talks to. There are two resolvers, and they do not agree — which is deliberate, because the consequences of guessing wrong are not symmetrical.
flowchart TD
A["leed <command>"] --> B{"leed auth,<br/>or leed site?"}
B -- "leed auth login / logout" --> C{"--env passed?"}
C -- "yes" --> R1["Use it"]
C -- "no" --> D{"ENV_NAME set<br/>and valid?"}
D -- "yes" --> R2["Use it"]
D -- "no" --> E{"leed.config.json found<br/>in cwd, parent or<br/>grandparent?"}
E -- "yes" --> R3["Use its environment"]
E -- "no" --> R4["production"]
B -- "leed site data commands" --> F{"leed.config.json<br/>environment present?"}
F -- "yes" --> R5["Use it"]
F -- "no" --> G{"ENV_NAME set<br/>and valid?"}
G -- "yes" --> R6["Use it"]
G -- "no" --> R7["Refuse:<br/>Could not determine the<br/>target environment."]
For leed auth login and leed auth logout the chain is --env → ENV_NAME → the config file → production. The -e, --env flag exists only on those two commands and is hidden from --help; CLI authentication covers when you need it.
For the leed site commands that talk to the CMS — push, commit, notify-cms, upload, import, create-page-type and list — the strict resolver runs instead. It reads the config file’s environment first, falls back to ENV_NAME, and then refuses rather than assuming anything:
Could not determine the target environment. Run from inside a leed-managed site
directory (leed.config.json) or set ENV_NAME=dev|staging|production.The inversion is on purpose. A wrong-environment push is hard to detect after the fact, so the checkout you are standing in wins over an environment variable that might be left over from something else. The practical consequence: inside a site folder, setting ENV_NAME does not redirect a push or an import. Change environment in leed.config.json, or work from a checkout for the environment you mean.
Which CMS each environment is
| Environment | CMS base URL | Override |
|---|---|---|
production | https://app.leed.ai | LEED_CMS_URL |
staging | https://staging.app.leed.ai | LEED_CMS_URL |
dev | http://localhost:8787 | LEED_CMS_URL |
LEED_CMS_URL beats all three and applies to every request the CLI makes. An unrecognized environment falls back to the production URL.
Everything the CLI writes to disk
Three groups: your home directory, your project, and your cache.
~
├── .config/leed/credentials.json 0600, in a 0700 directory
├── .bunfig.toml the @leed registry scope
└── .cache/leed/<env>/<companyId>/
├── leed-validation.json what you validated, and your last build
└── imports/<pageTypeId>.json one resumable OpenAPI import manifest
leed/example.com/
├── leed.config.json
├── example.com.code-workspace
└── raw-content/ the git repository
├── .git/config origin URL, containing a token
├── .husky/ pre-commit, pre-push, commit-msg
├── .vscode/tasks.json 12 tasks
├── .claude/skills/ site-builder, site-preview, site-publish
├── src/ your content and templates
└── .build/ build output; .build/site is the rendered site| Path | Written by | Contents | Permissions | In git? |
|---|---|---|---|---|
${XDG_CONFIG_HOME:-~/.config}/leed/credentials.json | the installer, leed auth login, every token refresh | one session block per environment | 0600 in a 0700 directory | outside the repo |
~/.bunfig.toml | the installer, leed auth rotate, leed auth reset | the [install.scopes] "@leed" entry with its registry URL and deploy token | default | outside the repo |
<CLI install dir>/.lastcheck.json | the automatic update check | the 24-hour throttle timestamp | default | outside the repo |
<project>/<customer>/leed.config.json | the installer, then leed site init | project configuration, no secrets | default | outside the repo |
<project>/<customer>/<customer>.code-workspace | leed site init and every later resync | a VS Code workspace opening raw-content/ | default | outside the repo |
raw-content/.git/config | leed site init, leed auth rotate, leed auth reset | the origin URL, which embeds your GitLab token | default | not committed |
raw-content/.husky/ | leed site init and every later resync | pre-commit, pre-push, commit-msg and their helpers | default | gitignored |
raw-content/.vscode/tasks.json | leed site init and every later resync | 12 tasks covering build, validate, commit, push and auth | default | gitignored |
raw-content/.claude/skills/ | leed site init and every later resync | site-builder, site-preview, site-publish | default | gitignored |
raw-content/.build/ | leed site build | intermediate output, with the rendered site at .build/site | default | gitignored |
${XDG_CACHE_HOME:-~/.cache/leed}/<env>/<companyId>/leed-validation.json | leed site validate, commit and build | which files you validated, your last successful build timestamp, the pending commit set | 0700 directory | outside the repo |
${XDG_CACHE_HOME:-~/.cache/leed}/<env>/<companyId>/imports/<pageTypeId>.json | leed site generate and import | one resumable import manifest per page type | 0700 directory | outside the repo |
The cache paths are keyed by environment and company because one person can belong to more than one company on one machine, and a validation record from a staging checkout must never satisfy a production commit. What is inside the repository itself, folder by folder, is mapped in your site repository.
Relocating with XDG
XDG_CONFIG_HOME moves the credentials directory; XDG_CACHE_HOME moves the caches. Both are read at the moment a path is needed, so setting either for a single command works.
Environment variables
Split into three groups, because most of these are not for you.
Ones you might set
| Variable | Used by | Effect | Default |
|---|---|---|---|
ENV_NAME | leed auth, the strict environment resolver, wrangler config generation | Names the target environment. Loses to leed.config.json on leed site data commands; beats it on leed auth | unset |
LEED_CMS_URL | every CMS request | Overrides the base URL for the resolved environment entirely | unset — the table above applies |
LEED_JSON | every command | 1, true, yes or on turns on JSON mode, exactly as --json does. Opt-in only: LEED_JSON=0 does not turn it off for a run that passed --json | unset |
FILE_VALIDATION_MAX_SIZE | leed site validate | Maximum file size in kilobytes. Note the unit — FILE_VALIDATION_MAX_SIZE=2000 means 2 MB, not 2 KB | 500 |
XDG_CONFIG_HOME | credential storage | Relocates the credentials directory, keeping the leed/ segment | ~/.config |
XDG_CACHE_HOME | validation tracking, import manifests | Relocates the caches, dropping the leed/ segment | ~/.cache/leed |
DEBUG | the logger | Merged with whatever -v or -n enabled, never replaced. DEBUG=Eleventy* leed site build is the documented way to see Eleventy’s own internals | unset |
NO_COLOR | the palette | Any non-empty value disables color. Beats FORCE_COLOR. Set to the empty string it does nothing | unset |
FORCE_COLOR | the palette | Any non-empty value other than 0 or false forces color on when stdout is not a terminal — which is how a piped or CI run gets color at all. Loses to NO_COLOR, and --json overrides both | unset |
CI | the update check, the build recorder, the leed site resolver | CI=true suppresses the automatic self-update and puts the CLI into direct mode. It is the lever for anyone scripting leed who does not want the binary replaced mid-run | unset |
Ones CI sets for you
These are set on the Cloudflare Builds trigger during provisioning, or injected by the build platform. You do not set them by hand, and setting them locally will make a command behave as though it were running in CI.
| Variable | Used by | Effect | Default |
|---|---|---|---|
LEED_API_TOKEN | leed site upload | The CMS API token a CI build authenticates with. Required in CI — the command refuses immediately with LEED_API_TOKEN not set. Required for CI builds. | unset |
LEED_ENTITLEMENTS | the build’s entitlement resolution | A JSON { tier, mcp } object. In CI it is the only trusted source and the build fails closed without it, so repository contents can never claim a tier they were not sold. Outside CI the build falls back to src/src.11tydata.json | unset |
BUILD_CUSTOMER | the hidden -c, --customer option | The customer slug a CI build is building | unset |
BUILD_SITE_LOCATION | the hidden -l, --location option | The directory holding the content repositories | unset |
WORKERS_CI_BRANCH | leed site build, leed site upload | The branch, used instead of asking git — and the branch is what decides preview versus public output | unset |
WORKERS_CI_COMMIT_SHA | leed site upload | The commit reported to the CMS with the build result | unset |
WORKERS_CI_BUILD_UUID | leed site upload | Correlates the upload with the Cloudflare build that produced it | unset |
Internal variables, for completeness
You will not set these, and nothing in the documented workflow depends on them. They are listed because a developer grepping their environment for an unexplained LEED_- or DNS_-prefixed name deserves an answer.
| Variable | Used by | Effect |
|---|---|---|
BUNDLED | the CLI entry point | Set inside the shipped bundle. Switches asset paths from the development layout to the packaged one, and is what makes the “Running in direct mode (dev)” banner disappear |
SHELL | leed completion install | Detects bash, zsh or fish. With an unrecognized or unset value the command refuses rather than guessing |
CUSTOMER_FUNCTIONS_PORT | wrangler configuration generation | Points a locally generated wrangler config at a customer-functions dev server on a non-default port |
DNS_ZONE_NAME | wrangler configuration generation | Names the DNS zone written into a generated wrangler config |
The hidden leed site options
leed site carries three options that are hidden from --help because they exist for Cloudflare Builds rather than for you: -l, --location <path> (bound to BUILD_SITE_LOCATION), -c, --customer <string> (bound to BUILD_CUSTOMER), and --ci (bound to CI). They select between three ways of working out where your files are. In direct mode — any CI signal, or a bare raw-content checkout with an identity.json and no reachable leed.config.json — the current directory is the content repository and the customer is derived from the folder above it. In local mode, the ordinary case, leed.config.json is loaded and the repository is <location>/<customer>/raw-content. Passing both --customer and --location states it explicitly. Everything else on the command tree is indexed at CLI command reference.
Passing --customer or --location also suppresses the automatic update check, which is a side effect worth knowing about rather than a feature to rely on.
Cleaning up
Most of what the CLI leaves on your machine is disposable. What each deletion costs:
| Delete this | What happens next |
|---|---|
~/.cache/leed/ (or one company’s subdirectory) | You lose your validation state and your last-build timestamp. The next leed site commit will tell you to validate and then to build. Nothing is lost but the two minutes it takes |
~/.config/leed/credentials.json | You are signed out of every environment. leed auth login puts it back |
raw-content/.build/ | One rebuild |
raw-content/.husky/, .vscode/, .claude/skills/ | The next leed site command reinstalls them, because the version comparison now sees a mismatch it cannot satisfy |
leed.config.json | Not recoverable from the CLI. Every leed site command stops at “Could not locate leed.config.json”, and nothing regenerates it locally |
If you lose leed.config.json, open the profile menu in the CMS, choose Developer Setup, and either download a fresh copy or re-run the one-line installer — it reuses your existing session, skips the prerequisites it already satisfied, and rewrites the file. That path is described on installing the Leed CLI.
A corrupt credentials.json behaves differently from a missing one in a way worth knowing: a file the CLI cannot parse reads as an empty file, so the symptom is “not logged in” rather than an error. The same is true of an import manifest, which is treated as “start over”.
The 500 KB validation ceiling that FILE_VALIDATION_MAX_SIZE overrides is one of several product limits that have nothing to do with your plan — they are collected in limits that are not plan limits, and the rule it enforces is on leed site validate, commit and push. Large files that genuinely belong on your site go through the asset pipeline instead of the repository — see static files and caching. When one of these paths or variables turns out to be the cause of a failure you are looking at, the message is in the catalog on CLI troubleshooting and exit codes.