Editable and Read-Only Files

Your site repository has two owners. You own the templates and the CSS — how the site looks. The CMS owns the data, the page content and the brand files — what the site says. The line between them is not a convention you are trusted to respect: it is enforced by leed site validate on your machine and again server-side when you edit the same files from inside the CMS. There is no path around it, which is the point. A file the CMS regenerates cannot also be a file you maintain, because the next publish would overwrite your work without telling you.

This page is the map of that boundary. If what you want is the rules — the blocked extensions, the size ceiling, what --reset does — those are on File Validation Rules.

What you own

Path or globWhy it is yours
src/_layouts/**Every layout is a page shell you wrote
src/_includes/**Your partials — except _includes/leed/, which Leed replaces on every build
src/index.hbs, src/404.hbs, src/robots.hbsThe root pages the scaffold ships
Any new src/*.hbs you createNew root-level pages are yours by construction
src/static/**Images, scripts, webfonts, site.webmanifest — except the managed brand files below
tailwind/**All of it, unconditionally

Nothing in that list is plan-gated. Editing these files with the CLI works on every plan, Free included.

Your documentation brand layer lives here, not in the product

If you run a documentation set, the files that make it look like yours are repository files, not CMS settings:

What it controlsWhere it lives
Color theme (the @utility color-theme-<name> block)tailwind/docs/color-theme-<name>.css
Code-highlighting themetailwind/docs/code-theme-<name>.css
Docs chrome and component overridesother files under tailwind/docs/
Alert colors and iconstailwind/partials/alerts.css
Mermaid diagram palettesrc/static/js/mermaid.theme.json

That is worth stating on an ownership page rather than only on the theming pages, for three reasons. These are the highest-value files in tailwind/ and src/static/ that the repository owns outright. They can be iterated and shipped without waiting for a Leed release, because nothing about them is baked into the site builder. And they are why tailwind/** is unconditionally yours instead of partly managed — the brand layer has to be somewhere the CMS never rewrites.

What each of those files should contain is covered by Custom Documentation Themes, Theming Alerts and Theming Diagrams. This page only establishes that they are yours and where they go.

The one filename to avoid in src/static/js/

You can put your own scripts and data files in src/static/js/ — mermaid.theme.json is exactly that. What you must not do is name one l.<something>.js. That pattern belongs to Leed’s own bundled JavaScript: files matching l.*.js are gitignored, written into your tree during a build and deleted afterwards. A file of yours with that name is swept away by the next build and never appears in git status to tell you why. The full list of reserved names and patterns is on Static Files and Caching.

What the CMS owns

src/_data/**/*

Every file here is a projection of CMS state — menus, page types, labels, users, forms, autolinks — regenerated and committed when you publish the matching change. The validator’s one static protected path is exactly this glob. What each file holds and which CMS screen writes it is on Global Site Data and the Data Cascade.

Page-type folders — the lock that appears on its own

This is the rule most people miss, because it is not a fixed list. At validation time the CLI reads src/_data/pageTypeList.json and builds one pattern per page type: src/<slug>/**/*. Those patterns are the restricted paths, alongside src/_data/**/*.

The upside of the same mechanism: rename or delete a page type and the corresponding lock goes with it. Page types are configured on Configuring a Page Type.

*.11tydata.json, anywhere

The pattern is **/*.11tydata.json, so it catches the company-level src/src.11tydata.json and every per-page-type file such as src/blog/blog.11tydata.json and src/docs/docs.11tydata.json. All of them are written from CMS state on publish.

Brand files

src/static/images/favicon-* and src/static/images/logo-* — the files the CMS writes when you upload a logo or a favicon in Settings, named with a timestamp (logo-2026-07-11T15-12-07-709Z.png). They are managed on Site Identity and Branding.

Read the glob precisely, because it is narrower than it looks: it requires the hyphen, and * does not cross a directory separator. So a src/static/images/logo/ directory of your own artwork is entirely yours — leed.ai keeps leed-logo-p-light.svg and leed-logo-p-dark.svg in exactly such a folder — while a file you name src/static/images/logo-hero.png is blocked for what will look like no reason at all. Put your own logo artwork in a subdirectory, or give it a name that does not start logo-.

Repository plumbing

README.md, bunfig.toml and .ci/**/*. bunfig.toml is not a file Leed maintains — it is not in the scaffold and not in a working repository. It is a name reserved against you introducing one, because the build writes its own registry configuration.

The whole boundary, in one table

Path or globOwnerEnforced by.hbs exempt?What to do instead
src/_layouts/**, src/_includes/**, tailwind/**, root .hbs pagesYou—n/a—
src/_data/**/*CMSCLI validate + CMS serverYesChange it in the CMS, publish, pull
src/<page-type-slug>/**/*CMSCLI validate + CMS serverYes (CLI only)Edit the page in the CMS; add a .hbs here for your own listing pages
**/*.11tydata.jsonCMSCLI validate; CMS names src/src.11tydata.jsonYesChange the setting on the page type or in company settings
src/static/images/favicon-*CMSCLI validateYesUpload a new favicon in Settings
src/static/images/logo-*CMSCLI validateYesUpload a new logo in Settings, or use a filename that does not start logo-
README.mdLeedCLI validate + CMS serverYesNothing — it is generated
bunfig.tomlLeed (reserved)CLI validateYesDo not add one; the build writes its own registry config
.ci/**/*LeedCLI validate; hidden in the CMSYesRead it to understand the build; never edit it
identity.jsonLeedRestored automaticallyNo — restored before any checkNothing; it binds the checkout to your company
.gitignoreLeedRestored automaticallyNo — restored before any checkAsk Leed if something needs ignoring
src/_includes/leed/**LeedNot protected — absent between buildsn/aleed site eject to take a partial over permanently

The .hbs column reads “would a Handlebars file at this path be allowed?” — the exemption is explained below, and it applies to the CLI’s pattern checks only.

The two files Leed restores without asking

identity.json and .gitignore are on a separate, shorter list from everything above, and they behave differently. They are not reported. They are restored, before the command that noticed them does anything else.

The mechanism is that every command which reads your working tree calls the same helper, and that helper checks for those two filenames first. If either has been modified, deleted or staged, it is put back with git restore (or removed from the index, or deleted from disk, depending on how you changed it), and the file list is re-read from scratch. Only then does validation, the commit gate or the push gate see your changes.

The .hbs exemption

There is exactly one escape hatch, and it is narrow enough to state in one line: the allowed-extension list is ["hbs"], and it is applied inside the pattern checks only.

That means a Handlebars file is legal even in a locked folder:

src/blog/custom-list.hbs      ✅  legal — .hbs in a page-type folder
src/compare/index.hbs         ✅  legal — same rule
src/_data/menu.hbs            ✅  legal, if pointless — the pattern is exempted
src/blog/post.md              ❌  restricted path, no exemption
src/blog/data.json            ❌  restricted path, no exemption

This is what lets you write your own listing pages, index pages and paginated views that live inside a page type’s URL space, alongside the content the CMS publishes there.

Two limits, both of which people run into:

  • The exemption covers the restricted-path check and the read-only-pattern check. It does not cover the disallowed-extension check or the size check — a .hbs file at or over 500 KB is still rejected, and no extension is exempt from the blocked-types list. Those two rules are on File Validation Rules.
  • A path with no extension at all that matches a protected pattern is always rejected. The check looks for a . in the path and rejects outright when there is none, so src/blog/README is refused rather than waved through.

Will this file be rejected?

flowchart TD
  A["A changed file"] --> B{"Matches a page-type path<br/>or src/_data/**?"}
  B -- yes --> C{"Extension is .hbs?"}
  C -- no --> R1(["Rejected: restricted path"])
  C -- yes --> D
  B -- no --> D{"Matches a read-only pattern?"}
  D -- yes --> E{"Extension is .hbs?"}
  E -- no --> R2(["Rejected: read-only pattern"])
  E -- yes --> F
  D -- no --> F{"Extension on the<br/>blocked-types list?"}
  F -- yes --> R3(["Rejected: disallowed file type"])
  F -- no --> G{"500 KB or larger?"}
  G -- yes --> R4(["Rejected: too large"])
  G -- no --> OK(["Committable"])

The shape of that diagram is the part worth carrying away: .hbs short-circuits two branches and neither of the other two.

What a rejection looks like

When anything fails, the CLI prints one block per rule that fired, always in this order — restricted paths, read-only patterns, blocked extensions, size — followed by the command that cleans up:

	Error: Restricted file violation!

	These changes are not allowed:

		src/_data/menu.json

	No files may be added, deleted or modified at these paths:

		src/_data/**/*
		src/blog/**/*
		src/docs/**/*

	The following are considered read-only and cannot be committed:

		README.md

	Protected patterns:

		identity.json
		.gitignore
		**/*.11tydata.json
		src/static/images/favicon-*
		src/static/images/logo-*
		README.md
		bunfig.toml
		.ci/**/*

	Run the following command to automatically remove and reset any restricted files:

		leed site validate --reset

Decode it by section. “These changes are not allowed” is the restricted-path rule, and the list under it is the dynamic pattern set including one line per page type — that list is how you see which page-type folders currently exist. “The following are considered read-only” is the fixed pattern rule, and “Protected patterns” is that rule’s complete list. If you get a third block about file types, or a fourth about size, those are separate rules with their own pages.

The complete read-only pattern list

These eight patterns are the fixed half of the rule set; the restricted-path list is built per run from your page types.

identity.json
.gitignore
**/*.11tydata.json
src/static/images/favicon-*
src/static/images/logo-*
README.md
bunfig.toml
.ci/**/*

identity.json and .gitignore appear here and also on the auto-restore list, which is why in practice you never see them in a rejection — they are put back before the check runs.

Getting back to a clean state

leed site validate --reset returns every file that violates a rule to its committed state. Rehearse it first with leed site validate --reset --dry-run, which lists exactly what it would touch and writes nothing.

What it does to a file depends on how you changed it: a modified or deleted tracked file is restored from git, a newly staged file is removed from the index, and an untracked file is deleted from disk. That last one has no undo. The full behavior, and the confirmation prompt that guards it, are on File Validation Rules.

Leed’s own partials under src/_includes/leed/ are a separate case again: they are not protected, because they are not in your repository between builds. They are copied in before Eleventy runs and deleted afterwards. To take one over permanently, leed site eject copies it into your tree as a file you own — see Overriding Leed Templates.

ESC