File Validation Rules

leed site validate is a gate, not a linter. It does not read your Handlebars, it does not parse your JSON, and it does not look at your front matter. It answers one question — may these changes be committed? — against four rules:

  1. Protected paths — the src/_data/** glob and one pattern per page type.
  2. Read-only patterns — a fixed list of eight filenames and globs.
  3. Blocked extensions — seventeen file types that may never enter the repository.
  4. File size — a hard ceiling per file.

Rules 1 and 2 are about which file you touched and are documented on Editable and Read-Only Files, including the decision flowchart that shows how the four rules interact. This page owns rules 3 and 4, everything validation deliberately does not do, and the --reset behavior that gets you unstuck.

Blocked file types

Seventeen extensions, checked against the last dot in the filename, with no exemptions of any kind — the .hbs escape hatch does not apply here. This is the list exactly as the CLI prints it when it rejects something:

mov, mpg, avi, mp4, mp3, wav, pdf, xls, xlsx, doc, docx, ppt, zip, pptx, ttf, ps, otf
ExtensionCategoryWhere that content belongs instead
mov, mpg, avi, mp4VideoThe CMS asset library, which transcodes and serves from the CDN
mp3, wavAudioThe CMS asset library
pdfDocumentThe CMS asset library
xls, xlsxSpreadsheetThe CMS asset library
doc, docxDocumentThe CMS asset library
ppt, pptxPresentationThe CMS asset library
zipArchiveThe CMS asset library, or somewhere outside the site entirely
ttf, otfFont (desktop formats)Ship woff2 instead — see below
psPostScriptNot a web format; convert or drop it

The reason for the media and document half of the list is not disk space, it is delivery. A file in your repository is served as a static asset with no transcoding, no variants and no analytics. The same file in the asset library is served from the CDN with responsive image variants, video engagement tracking and a stable delivery URL that survives you reorganizing the repository. Asset Library is where that content goes; Image Variants and Responsive Images is why large images in particular are better off there.

Fonts — the correction

There is no woff2 allowlist. The rule is a denylist and nothing else: ttf, otf and ps are on it, and woff and woff2 are simply absent, so they pass.

The practical advice is unchanged — ship woff2 — but the reason matters, because a reader who is told “only woff2 is allowed”, then successfully commits a .woff, stops believing the rest of the page. Ship woff2 because it is the smallest format every current browser supports, not because validation would stop you doing otherwise.

Most sites never add a font file at all: Leed downloads its bundled font families at build time. Fonts and Webfonts covers both the bundled set and the case for adding your own.

The size ceiling

500 KB per file, and the comparison is greater than or equal to. A file of exactly 512,000 bytes is rejected. Write the rule to yourself as “at or over 500 KB is refused”, not “under 500 KB is fine” — the boundary case falls on the rejection side.

Deleted files are exempt, for the obvious reason: a file that is no longer on disk cannot be too big, so removing an oversized file is always a legal change.

The ceiling is read from the FILE_VALIDATION_MAX_SIZE environment variable, in kilobytes, defaulting to 500.

What is not validated

This is the honest half of the page, and the reason it exists as more than a denylist. Passing validation tells you your changes are allowed. It tells you nothing about whether they work.

There is no front-matter validation

The per-file validation hook exists and unconditionally returns success. Nothing parses your front matter, checks that it is valid JSON, or verifies that a required key is present. A page whose front matter has a trailing comma passes leed site validate cleanly and then fails the build.

The bundled site-builder reference lists “Frontmatter schema compliance” among the things validation checks. It does not. What front matter actually has to contain, and what happens when it does not, is on Front Matter Reference.

No Handlebars parse, no Tailwind compile, no link resolution. The build is what catches all three.

That is less of a gap than it sounds, because you cannot skip the build: leed site commit refuses to run until a successful build has been recorded since your last change, with You must run the following command before changes can committed: followed by leed site build. In practice the sequence enforces itself.

The two claims in the bundled reference that are out of date

.claude/skills/site-builder/references/cli-commands.md describes leed site validate as checking, among other things:

  • “No disallowed file extensions (media, office docs, fonts except WOFF2)” — the extension check is real; the font allowlist is not. woff and woff2 both pass because neither is on the denylist.
  • “Frontmatter schema compliance” — no such check exists. The per-file validator returns success for every file it is handed.

Both statements were true of an intended design rather than of the shipped code. The file is installed and refreshed by the CLI, so it will keep saying this until a new version replaces it.

How validation tracks what it has seen

Validation is incremental. A changed file needs re-validating when either of two things is true: it is not in the tracking file at all, or its recorded lastUpdated timestamp is older than the file’s modification time on disk. Everything else is skipped, which is why a second leed site validate over an unchanged tree is instant.

The tracking file lives outside the repository:

${XDG_CACHE_HOME:-~/.cache/leed}/<environment>/<companyId>/leed-validation.json

It is keyed by environment and company id because one person can work on several companies from one machine, and it is kept out of the git tree on purpose: a stray git add in the repository must not be able to commit your local validation state. Both values come from leed.config.json, and there is no fallback — run the command outside an initialized site directory and it refuses rather than guessing.

One path quirk to know: when XDG_CACHE_HOME is set, it is used as-is and the leed segment is not appended. With XDG_CACHE_HOME=/var/cache, the file is at /var/cache/<environment>/<companyId>/leed-validation.json, not under /var/cache/leed/.

This is the file to look at when you are stuck in a loop where leed site commit insists you validate, you validate, and it insists again. The tracking file also records the last successful build, which is what the commit gate consults.

Reset semantics

leed site validate --reset returns every file that breaks any of the four rules to its committed state. It covers all four classes — not just protected paths, but blocked extensions and oversized files too.

What happens to a file depends on how git currently sees it:

Git statusAction takenRecoverable?
Modified or deleted (tracked)git restore <file>Yes — the committed version comes back
Staged new file (created)git rm <file>Yes — the content is still in the index history
Untracked (not_added)Deleted from diskNo

A real reset also prints what it did as it goes, one line per file with the action in brackets, so the transcript in your terminal is the record of what was changed.

Reading the output and the exit codes

SituationWhat is printedExit
Everything passedAll changed successfully validated. Please build and visually inspect your site.0
--reset --dry-runThe list of files a reset would touch, with the action for each0
Nothing was modifiedNo files were modified. Nothing to do.non-zero
Something is restrictedThe violation block, then Run the following command to automatically remove and reset any restricted files: and leed site validate --resetnon-zero

Two things about that table are easy to get wrong. “Nothing to do” is a failure, not a success — the command refuses rather than reporting a no-op, and it is checked before the reset runs, so --reset on a clean tree stops there. And the exit code rule across the whole CLI is to branch on zero versus non-zero, never on a specific number; the envelope, the error codes and the handful of legacy codes are on CLI Troubleshooting and Exit Codes.

The four rules at a glance

RuleWhat it checks.hbs exempt?Configurable?On failure
Protected pathssrc/_data/**/* plus src/<page-type-slug>/**/* for every page typeYesNo — rebuilt from your page types on every runViolation block, exit non-zero
Read-only patternsThe fixed eight-entry listYesNoViolation block, exit non-zero
Blocked extensionsThe seventeen types aboveNoNoViolation block, exit non-zero
File size≥ 500 KBNoFILE_VALIDATION_MAX_SIZE, locally onlyViolation block, exit non-zero

Where validation sits in the sequence

Validation is the first of three gates, and they are enforced in order: validate → build → commit, then push. leed site commit checks your git identity, then the branch, then that something changed, then that nothing restricted was touched, then that every changed file has been validated, then that a build has happened since. Each refusal names the command that clears it.

Every one of those messages, and the order they fire in, is on leed site validate, commit and push; the same messages appear again with their remedies on Common Error Messages.

ESC