These three commands are always run in this order, and each refuses to do its job until the one before it has done its own. This page is the reference: every flag, every gate in the order it runs, and the exact sentence each refusal prints. The workflow itself — why it exists, what it produces, and how it is enforced — is on validate, commit and push.
Run all three from the project folder or from raw-content/.
leed site validate
leed site validate
leed site validate --reset --dry-run
leed site validate --reset| Flag | Type | Default | Required | What it does |
|---|---|---|---|---|
-r, --reset | boolean | false | no | Returns every restricted file to the state the site template ships it in |
--dry-run | boolean | false | no | With -r, lists what would be reset and changes nothing |
-y, --yes | boolean | false | no | Confirms --reset without being asked. Required to run it unattended |
Validation runs its checks in a fixed order, and the order is what decides which message you get:
- The confirmation gate, for a real
--resetonly. See the warning below. - Is there anything to do. Nothing modified is a refusal, not a success: “No files were modified. Nothing to do.”, exit 1.
- The reset, when
-rwas passed. With--dry-runit lists what it would do and stops there, exiting 0 without validating anything. - The restricted-file check. Offending files are listed with the rules they broke, followed by “Run the following command to automatically remove and reset any restricted files:” and the
leed site validate --resetline. Exit 1. - Per-file validation, which records each file’s modification time.
- The tracking file is written and validation confirms: “All changed successfully validated. Please build and visually inspect your site.”
Validation is per file and per edit. It records the modification time of everything it passed, so touching a file again makes it unvalidated again and leed site commit will say so.
The rules
| Rule | What it covers | Effect |
|---|---|---|
| Read-only, restored on sight | identity.json, .gitignore | Reset before any other check runs, so they never even appear as changes |
| Protected paths | src/_data/**/* | Blocked — this is the data the CMS writes |
| Page-type folders | src/<page-type-slug>/**/* | Blocked. Built at run time from src/_data/pageTypeList.json, so the protected set grows the moment you create a page type |
| Disallowed patterns | **/*.11tydata.json, src/static/images/favicon-*, src/static/images/logo-*, README.md, bunfig.toml, .ci/**/* | Blocked |
.hbs exemption | A file matching a path or pattern rule above whose extension is hbs | Not blocked — templates are yours even inside a protected folder |
| Disallowed extensions | Media, office documents, archives and three font formats | Blocked. Full list below |
| Maximum file size | 500 KB, at or above | Blocked. Override with FILE_VALIDATION_MAX_SIZE, which is read in kilobytes |
The full disallowed-extension list
mov, mpg, avi, mp4, mp3, wav, pdf, xls, xlsx, doc, docx, ppt, pptx, zip, ttf, ps, otf
The three font entries are ttf, otf and ps. There is no allowlist and no WOFF2 rule: woff and woff2 are simply not on this list, so both commit normally.
Video, audio and PDF are on it because they belong in the asset library, where they are served from object storage and get a delivery URL — not in a git repository that is cloned on every build.
There is no frontmatter validation. Per-file validation currently records the file and passes it; the rules that actually block a commit are the path, pattern, extension and size rules above.
The same rules stated as repository policy, rather than as command output, are in file validation rules and editable and read-only files.
Reset semantics
--reset collects everything the four rules rejected — restricted paths, disallowed patterns, disallowed extensions and oversized files — and acts on each according to what git says about it:
| Git status of the file | What reset does |
|---|---|
| Modified | git restore — your edits are discarded |
| Deleted | git restore — the file comes back |
| Created and staged | git rm — unstaged and removed |
| Untracked | Deleted from disk |
Each file is printed with the action taken beside it, so the output is a record of what happened.
Because it is destructive, a real --reset now asks before it runs, defaulting to No:
Reset every protected file to its template state? Your changes to those files are discarded and cannot be recovered.
There are three ways that question is answered without a person:
--yesproceeds. This is the explicit, greppable way to run it unattended.- Under
--json, or withLEED_JSONset, it is refused with the codeconfirmation_requiredandleed site validate --reset --yesas the hint. Nothing is touched. - In human mode with no interactive terminal — a pipe, a cron job, a CI step — it is also refused, with the code
interactive_input_requiredand the same remedy.
--reset --dry-run is never gated: it exists to be run before deciding.
Where the tracking lives
Validation state is kept outside the git tree, at:
~/.cache/leed/<environment>/<companyId>/leed-validation.jsonOutside deliberately — a stray git add in the content repository could otherwise commit your local workflow state. The directory is created mode 0700. Both the environment and the company id come from leed.config.json, and there is no fallback for either: run one of these commands outside an initialized site directory and it refuses rather than operating without company context. Keying by company matters if you belong to more than one, since the same machine then holds several independent sets of state.
The file holds four things: lastSuccessfulBuild (the timestamp commit gates on), lastTouched (the newest validated edit), commitSet (the files, commits and per-file change kinds waiting to be reported to the CMS) and tracking (per-file modification times).
leed site commit
leed site commit -m "update the pricing page layout"
leed site commit -m "…" --dry-run| Flag | Type | Default | Required | What it does |
|---|---|---|---|---|
-m, --msg <string> | string | — | yes | Your commit message. Commander refuses the command without it |
--dry-run | boolean | false | no | Lists the files that would be committed, then exits 0 |
There is no git add. The CLI stages everything with git add --all after its gates pass, which is why the workflow is validate → build → commit rather than a staging dance.
The gates, in order
| # | Gate | Message, verbatim | Fix |
|---|---|---|---|
| 1 | Git identity | Run the following commands to complete your setup: | Run the two printed git config --global commands |
| 2 | Branch | You are only allowed on specific branches. Change back to staging! | git checkout staging |
| 3 | Anything modified | No files were modified. Nothing to do. | Make a change |
| 4 | Restricted files | Run the following command to automatically remove and reset any restricted files: | leed site validate --reset |
| 5 | Everything validated | You must run the following command before changes can be committed: | leed site validate — the files needing it are listed underneath |
| 6 | A build newer than your newest edit | You must run the following command before changes can committed: | leed site build |
Every one of these exits 1. Gate 6’s message is missing a word — it reads “before changes can committed” in the shipped CLI. It is quoted here exactly because that is the string people paste into search.
Gate 6 is the one that traps people, because a --debug build does not satisfy it. The rule for which builds count is on leed site build.
Past the gates, --dry-run prints “These files have been modified and will committed:” followed by the file list, and exits 0. A real run stages, commits, and confirms:
Changes were committed. You may now push your changes:
leed site pushA staging failure prints “An error occurred while adding files:” with git’s own words beneath it; a commit failure prints git’s message directly, or “An error occurred while committing:” when the CLI’s own hook was what refused.
What your commit message actually looks like
The message git stores is not the sentence you typed. It is a prefix followed by JSON:
@leed/site-management=>{"message":"update the pricing page layout","userId":"k7q2x9","version":"4.105.0"}Your text is the message field. userId is what lets the CMS attribute the change to you on the deployment row, and version is the CLI release that produced it, so a build failure can be traced back to a release. The commit-msg hook parses this and rejects any commit that does not carry all three fields — which is what stops a hand-written git commit from slipping through.
Git is invoked as git -c leed.invoked=true, a flag the pre-commit and pre-push hooks check for. That flag, set only by the CLI for the duration of one command, is the whole enforcement mechanism.
leed site push
leed site pushNo flags. Push does nine things in this order:
| # | Step | On failure |
|---|---|---|
| 1 | Check the git identity | Run the following commands to complete your setup: |
| 2 | Check the branch is staging | You are only allowed on specific branches. Change back to staging! |
| 3 | Count commits ahead of origin/staging | Zero is a success: “No changes have been committed. Nothing to push.”, exit 0 |
| 4 | Revalidate the session against the server | Drops into an interactive device login; a refusal ends the command |
| 5 | git fetch origin staging | Could not fetch from origin: with git’s words. If git’s words read as a credential refusal, the hint is leed auth rotate |
| 6 | Rebase onto the remote, if it has moved | The rebase is aborted and the recovery printed — see below |
| 7 | git push origin staging --no-verify | An error occurred while pushing: with git’s error: and hint: lines |
| 8 | Re-point the tracked commit id at the post-rebase HEAD | — |
| 9 | Notify the CMS in-process | The CMS was not notified: after the push confirmation — see below |
Step 4 is unconditional. Push does not trust the local expiry stamp — it round-trips the refresh endpoint on every run, so a session revoked from another machine is caught here rather than at step 9. The reason is step 9 itself: a push whose token dies partway would put commits on the remote and never tell the CMS, and that notification is not replayed automatically. CLI authentication covers the session model behind it.
Rebase, never merge
The content repository takes commits from more than one place — the CMS writes published pages and data files into it, and your teammates push templates. When the remote has moved, push replays your commits on top of it rather than merging, so history stays linear and every deployment maps to one commit.
A successful rebase says so before the push confirmation:
Rebased 3 remote commit(s) onto your work.
Site pushed successfully...
CMS successfully notified!A genuine conflict aborts the rebase — leaving your working tree exactly as it was — and prints the recovery:
Remote has conflicting changes that can't be auto-rebased.
Run `git pull --rebase` in the content repo, resolve conflicts, then retry `leed site push`.cd raw-content
git pull --rebase
# resolve the conflicts, then:
git add <the files you fixed>
git rebase --continue
cd ..
leed site pushStep 7 passes --no-verify, which skips the pre-push hook. That is intentional rather than a loophole: the CLI has already run every check the hook exists to enforce, and the hook would otherwise refuse the CLI’s own push. It is worth knowing so that seeing --no-verify in a trace does not read as something bypassing a safety net.
Step 8 exists because step 6 can rewrite your commit hashes. The commit id recorded at commit time would then name a commit that is not on the remote, and the CMS would trigger a build for a hash its clone cannot find. Re-pointing it at the post-rebase HEAD is what keeps the deployment matched to a commit that exists.
When the push works and the notification does not
Steps 7 and 9 can disagree. If the commits reach the remote and the CMS cannot be told, you get both facts, in that order:
Site pushed successfully...
The CMS was not notified:
An error while connecting to server:
TimeoutError: Request timed out: POST https://app.leed.ai/api/site/pushedYour work is on origin/staging. What is missing is the deployment record and the build. The fix is to re-run the notification, not the push — pushing again finds nothing ahead of the remote and exits 0 without ever reaching step 9:
leed site notify-cmsleed site notify-cms
Hidden from --help, and normally reached only as step 9 of a push. It is documented here because it is the recovery for the failure above.
leed site notify-cms [--timeout <seconds>] [--retry <number>]| Flag | Type | Default | Required | What it does |
|---|---|---|---|---|
--timeout <number> | number | 5 | no | Seconds to wait for the request |
--retry <number> | number | 3 | no | Maximum retries |
It posts the recorded commit set and the current branch to /api/site/pushed, which is what creates the deployment record and launches the build workflow. Retries happen on 500, 502, 503 and 504 only, with a fixed three-second delay — a 401 or a 403 is answered once and reported, because retrying cannot fix either. On a 200 it clears the local tracking, so the next push starts from a clean commit set.
Its exit codes are its own, and they are the reason it is worth knowing about in a script:
| Exit | Meaning | Message |
|---|---|---|
0 | Nothing to send — no commit set since the last notification | No changes detected. Nothing to push. |
0 | The CMS accepted the change set | CMS successfully notified! |
1 | A commit set exists but carries no HEAD commit | No HEAD commit recorded. Run ‘leed site commit’ first. |
2 | No environment in leed.config.json, or no credentials for it | Missing environment in leed.config. Run: leed site init / Not authenticated for |
8 | The request could not be completed at all | An error while connecting to server: with the underlying error beneath |
9 | The CMS answered with a non-200 | An error occurred while sending changes: |
A 401 inside exit 9 is reported as an expired session, with the login command as the hint; a 403 is reported as a permission problem, with the hint to ask an administrator of your organization to grant this account permission to push site content.
When notify-cms reports “No changes detected. Nothing to push.” after a push has already gone out, that is the awkward case: your commits are on the remote and no deployment was created for them, because the tracking was already cleared. The way forward is a fresh commit — a whitespace change is enough — validated, built, committed and pushed, which carries the earlier work along with it.
Every code either command can return is listed in CLI troubleshooting and exit codes, and once the notification succeeds the CMS takes over: what happens after you push. These three commands, and every other one with its flags, are indexed at the CLI command reference.