Update to Netlify
Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0
Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Instructions updated for netlify-config
Instruction wording changed from “Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redir...” to “Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response head...”. 188 additional added or edited lines are in the evidence.
Observed in instructions or declared skills. Runtime behavior has not been tested.
Product description
Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redir...
Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response head...
Skill instructions
Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redir...
Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response head...
Supporting files
[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":332},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}]
[]
Compare saved observations
Download comparison JSONFull technical diff · 3 changed fields
changed /description
"Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redirects with splats/conditions, headers, deploy contexts, functions config, and edge functions config."
"Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify."
changed /included_files
[
{
"relative_path": "LICENSE.txt",
"size_in_bytes": 10776
},
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 332
},
{
"relative_path": "assets/netlify-small.svg",
"size_in_bytes": 1291
},
{
"relative_path": "assets/netlify.png",
"size_in_bytes": 2686
}
][]
changed /skill_md_contents
"---\nname: netlify-config\ndescription: Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redirects with splats/conditions, headers, deploy contexts, functions config, and edge functions config.\n---\n\n# Netlify Configuration (netlify.toml)\n\nPlace `netlify.toml` at the repository root (or at the base directory for monorepos).\n\n## Build Settings\n\n```toml\n[build]\n base = \"project/\" # Base directory (default: root)\n command = \"npm run build\" # Build command\n publish = \"dist/\" # Output directory\n```\n\n## Redirects\n\n```toml\n# Basic redirect\n[[redirects]]\nfrom = \"/old\"\nto = \"/new\"\nstatus = 301 # 301 (default), 302, 200 (rewrite), 404\n\n# SPA catch-all\n[[redirects]]\nfrom = \"/*\"\nto = \"/index.html\"\nstatus = 200\n\n# Splat (wildcard)\n[[redirects]]\nfrom = \"/blog/*\"\nto = \"/news/:splat\"\n\n# Path parameters\n[[redirects]]\nfrom = \"/users/:id\"\nto = \"/api/users/:id\"\nstatus = 200\n\n# Force (override existing files)\n[[redirects]]\nfrom = \"/app/*\"\nto = \"/index.html\"\nstatus = 200\nforce = true\n\n# Proxy to external service\n[[redirects]]\nfrom = \"/api/*\"\nto = \"https://api.example.com/:splat\"\nstatus = 200\n[redirects.headers]\n X-Custom = \"value\"\n\n# Country/language conditions\n[[redirects]]\nfrom = \"/*\"\nto = \"/fr/:splat\"\nstatus = 200\nconditions = { Country = [\"FR\"], Language = [\"fr\"] }\n```\n\n**Rule order matters** — Netlify processes the first matching rule. Place specific rules before general ones.\n\n## Headers\n\n```toml\n[[headers]]\nfor = \"/*\"\n[headers.values]\n X-Frame-Options = \"DENY\"\n X-Content-Type-Options = \"nosniff\"\n\n[[headers]]\nfor = \"/assets/*\"\n[headers.values]\n Cache-Control = \"public, max-age=31536000, immutable\"\n```\n\nHeaders apply only to files served from Netlify's CDN (not to function or edge function responses — set those in code).\n\n## Deploy Contexts\n\nOverride settings per deploy context:\n\n```toml\n[context.production]\ncommand = \"npm run build\"\nenvironment = { NODE_ENV = \"production\" }\n\n[context.deploy-preview]\ncommand = \"npm run build:preview\"\n\n[context.branch-deploy]\ncommand = \"npm run build:staging\"\n\n[context.dev]\nenvironment = { NODE_ENV = \"development\" }\n\n# Specific branch\n[context.\"staging\"]\ncommand = \"npm run build:staging\"\n```\n\n## Environment Variables\n\n```toml\n[build.environment]\nNODE_VERSION = \"20\"\n\n[context.production.environment]\nAPI_URL = \"https://api.prod.com\"\n\n[context.deploy-preview.environment]\nAPI_URL = \"https://api.staging.com\"\n```\n\n**Do not put secrets in netlify.toml** (it's committed to source control). Use the Netlify UI or CLI for sensitive values. See the **netlify-cli-and-deploy** skill for CLI environment variable management.\n\n## Functions Configuration\n\n```toml\n[functions]\ndirectory = \"netlify/functions\" # Default\nnode_bundler = \"esbuild\"\n\n# Scheduled function\n[functions.\"cleanup\"]\nschedule = \"@daily\"\n```\n\n## Edge Functions Configuration\n\n```toml\n[[edge_functions]]\npath = \"/admin\"\nfunction = \"auth\"\n\n# Import map for Deno URL imports\n[functions]\ndeno_import_map = \"./import_map.json\"\n```\n\n## Dev Server\n\n```toml\n[dev]\ncommand = \"npm start\" # Dev server command\nport = 8888 # Netlify Dev port\ntargetPort = 3000 # Your app's dev server port\nframework = \"#auto\" # \"#auto\", \"#static\", \"#custom\"\n```\n\n## Plugins\n\n```toml\n[[plugins]]\npackage = \"@netlify/plugin-lighthouse\"\n[plugins.inputs]\n audits = [\"performance\", \"accessibility\"]\n```\n\n## Image CDN\n\n```toml\n[images]\nremote_images = [\"https://example\\\\.com/.*\"]\n```\n\nSee the **netlify-image-cdn** skill for full Image CDN usage.\n""---\nname: netlify-config\ndescription: Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify.\n---\n\n# Netlify configuration\n\nConfig lives in three files at the repo **root** (or the base/package directory for monorepos):\n- `netlify.toml` — build, contexts, plugins, functions, redirects, headers, dev.\n- `_redirects` — plain-text redirect/rewrite rules, saved to the **publish directory**, no extension.\n- `_headers` — plain-text response headers, saved to the **publish directory**.\n\n`netlify.toml` values **take precedence over the Netlify UI** when they conflict. Paths in `netlify.toml` are absolute relative to the **base directory** (root `/` by default).\n\n## Modern vs legacy syntax to reach for\n- Functions bundler: use `node_bundler = \"esbuild\"`. `zisi` is the legacy JS default; TypeScript always uses `esbuild`.\n- Temporary redirect: use `status = 302`. `307` is **unsupported**.\n- Gatsby Image CDN: use `NETLIFY_IMAGE_CDN`, not the deprecated `GATSBY_CLOUD_IMAGE_CDN`.\n- Injecting env values into TOML: `key = \"$VAR\"` is **NOT supported** (except `signed` in proxy redirects). Use a build-command `sed` substitution or a build plugin (see below).\n\n## `netlify.toml` build + contexts\n\n```toml\n[build]\n base = \"frontend\"\n publish = \"dist\"\n command = \"npm run build\"\n environment = { NODE_VERSION = \"18\" }\n\n[context.production]\n publish = \"output/\"\n command = \"make publish\"\n\n[context.deploy-preview]\n publish = \"dist/\"\n\n[context.\"feat/branch\"] # quote names with special characters\n command = \"npm run preview\"\n```\n\n`[build]` runs in **Bash**. Context-aware keys include `[build]` and `[[plugins]]` — but **NOT** `[[redirects]]` or `[[headers]]` (those are always global). Precedence, least→most specific: UI < toml < any-context property < `[context.<name>]` < `[context.branchname]`.\n\n## Redirects and rewrites\n\n`_redirects` rules are processed **first**, then `netlify.toml`; within each, the **first matching rule top-to-bottom wins** — list specific rules before general ones. Edge functions run before redirects.\n\nSPA history-`pushState` fallback (required for clean URLs):\n```\n/* /index.html 200\n```\n```toml\n[[redirects]]\n from = \"/*\"\n to = \"/index.html\"\n status = 200\n```\n\n`_redirects` syntax — `from to [status] [conditions]`, `#` comments, paths case-sensitive, URL-encode special chars:\n```\n/home / 301\n/my-redirect / 302\n/ecommerce /store-closed 404 # custom 404 for a path\n/pass-through /index.html 200 # rewrite\n/best-pets/dogs /best-pets/cats.html 200! # force/shadow (! or force=true)\n/news/* /blog/:splat # splat\n/news/:month/:date/:year/:slug /blog/:year/:month/:date/:slug # placeholders\n/store id=:id /blog/:id 301 # query params\n/ /anz 302 Country=au,nz # no spaces in value list\n/israel/* /israel/he/:splat 302 Language=he\n/* /legacy/:splat 200 Cookie=is_legacy,my_other_cookie\n```\n\n`[[redirects]]` keywords: `from`, `to`, `status` (default `301`), `force` (default `false`; `!`/shadow), `query` (`query = {path = \":path\"}`), `conditions` (`{Language, Country, Role, Cookie}`), `headers` (proxy request headers), `signed` (env var name for signed proxies).\n\n**Gotchas:**\n- You **cannot** add/remove a trailing slash with a redirect — CDN normalizes URLs first; a `/x/ → /x 301!` rule loops infinitely. Rely on Pretty URLs (default on).\n- Splat asterisks work only at the **end** of a segment (`/jobs/*`), not mid-path (`/jobs/*.html` invalid). Placeholders (`:x`) only at the start of a segment; can't mix wildcard+placeholder in one segment.\n- You can't exclude a path from a splat; put a more specific rule first.\n- `Country` = ISO 3166-1 alpha-2; language redirects match only the **first** `Accept-Language` entry.\n- Role-based redirects with external auth providers are **Enterprise-only**.\n- 10,000+ redirects: use wildcards/placeholders or Edge Functions — oversized serialized output fails the deploy.\n\n## Proxies\n\n```\n/api/* https://api.example.com/:splat 200\n/netlify-site/* https://my-other-site.netlify.app/:splat 200 # use .netlify.app, not custom domain\n```\n```toml\n[[redirects]] # custom request headers + force\n from = \"/search\"\n to = \"https://api.mysearch.com\"\n status = 200\n force = true\n headers = {X-From = \"Netlify\"}\n```\nSigned proxy (`signed` names an env var scoped to **Runtime**; must live in `netlify.toml`; JWS is external-only, not Netlify→Netlify):\n```toml\n[[redirects]]\n from = \"/search\"\n to = \"https://api.mysearch.com\"\n status = 200\n force = true\n signed = \"API_SIGNATURE_TOKEN_PLACEHOLDER\"\n```\n\n**Gotchas:** cross-team rewrites disallowed; same-password-site rewrites OK but not across separate protected sites; proxy timeout **26 s**; one hop by default; relative-path assets break (use absolute or `<base>`); loops silently ignored.\n\n## Custom headers\n\n```\n/*\n X-Frame-Options: DENY\n/templates/index2.html\n X-Frame-Options: SAMEORIGIN\n```\nMulti-value — repeat the key (`_headers`) or a multiline TOML string:\n```toml\n[[headers]]\n for = \"/*\"\n [headers.values]\n cache-control = '''\n max-age=0,\n no-cache,\n no-store,\n must-revalidate'''\n```\n\n**Gotchas:**\n- Headers in `_headers`/`netlify.toml` are **global** — NOT scoped to branch/context. Workaround: strip global headers, keep header files in a custom dir, and `cp` them into the publish dir from a per-context build command:\n ```toml\n [context.staging]\n command = \"npm run build && cp ./custom-headers/_stagingHeaders ./dist/_headers\"\n ```\n- Headers apply only to files from Netlify's store — **NOT** to proxied content or function/edge (SSR) responses; those must set their own headers.\n- Ignored (server-set) names include `Content-Length`, `Content-Encoding`, `Location` (use redirects), `Set-Cookie`, `Server`, etc.\n- Basic auth headers: **Pro/Enterprise only**. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List).\n\n## Functions\n\n```toml\n[functions]\n directory = \"myfunctions/\" # default: <base>/netlify/functions\n node_bundler = \"esbuild\"\n external_node_modules = [\"package-1\"] # esbuild only; native add-ons etc.\n included_files = [\"files/*.md\"] # ! prefix excludes\n\n[functions.\"api_*\"] # glob/named blocks concatenate with top-level\n external_node_modules = [\"package-2\"]\n included_files = [\"!files/post-1.md\"]\n```\n\n## Environment variables\n\nTwo storage methods:\n- **UI / CLI / API** — stored on Netlify (not the repo). Supports site + shared vars, per-context values, scopes; reaches builds, functions/edge/ODB, snippet injection, forms, signed proxies. **Recommended for anything sensitive.**\n- **`netlify.toml`** — stored in the repo. Site vars only, per-context values, **no scope selection** (everything gets **Builds** + **Post processing**), reaches builds + snippet injection only.\n\n`netlify.toml` env vars **override** same-key UI/CLI/API vars.\n\nPer-context values in TOML:\n```toml\n[context.production]\n environment = { NODE_VERSION = \"14.15.3\" }\n[context.deploy-preview.environment]\n NOT_PRIVATE_ITEM = \"not so secret\"\n[context.branch-deploy.environment]\n NODE_ENV = \"development\"\n```\n\nCLI:\n```bash\nnetlify env:set KEY value # --secret marks it a secret\nnetlify env:import .env # site vars; --replace-existing wipes others first\nnetlify env:unset KEY\nnetlify env:list --plain --context production > .env\nnetlify build # local build with Netlify's env vars\n```\nAPI: `createEnvVars` / `updateEnvVar` (`is_secret: true`) / `setEnvVarValue` / `deleteEnvVar` / `deleteEnvVarValue`.\n\n**Access syntax:** Bash `$VAR` in `build.command`/`ignore.command`; `process.env.VAR` in Node scripts and plugins.\n\n**Scopes** (Pro/Enterprise; default all): Builds (site builds) · Functions (Functions/Edge/ODB) · Runtime (forms, signed proxies) · Post processing (snippet injection). Shared vars are Pro/Enterprise and **Team-Owner-only** to read/edit. Precedence for a site+shared key collision resolves **per scope** — a site var only wins within the scopes it actually carries.\n\n**Naming/limits:** keys alphanumeric + underscore, must start with a letter (`1KEY`, `_KEY1` invalid); keys ≤255 chars, values ≤5,000 chars. Read-only variable names are reserved. Changes need a build + deploy.\n\n**Set the build language via reserved config vars** — `NODE_VERSION`, `NPM_FLAGS`, `YARN_VERSION`, `BUN_VERSION`, `RUBY_VERSION`, `PHP_VERSION`, `PYTHON_VERSION`, `GO_VERSION`, `HUGO_VERSION`, `PNPM_FLAGS`, `NPM_TOKEN` (Yarn: `YARN_NPM_AUTH_TOKEN`), etc.\n\n**Must be set in UI/CLI/API, NOT `netlify.toml`** (read after the repo is cloned or a runtime-only var): `AWS_LAMBDA_JS_RUNTIME`, `GIT_LFS_ENABLED`, `GIT_LFS_FETCH_INCLUDE`, `NETLIFY_BUILD_DEBUG`.\n\n**`CI` gotcha:** defaults to `true`; if it breaks a build, prepend `CI='' ` to the build command.\n\n### Injecting env values into headers/redirects\n`key = \"$VAR\"` is unsupported. Only path (scope must include **Builds**):\n```toml\n[build]\n command = \"sed -i \\\"s|HEADER_PLACEHOLDER|${PROD_API_LOCATION}|g\\\" netlify.toml && yarn build\"\n```\n`sed` substitution works **only** for `[[headers]]`/`[[redirects]]` (read after the build) and is **not** visible to build plugins (they run before the build command). For plugin-visible changes, use a local build plugin editing `netlifyConfig`.\n\n### Useful read-only build vars\n`CONTEXT` (`production`/`deploy-preview`/`branch-deploy`/`dev`), `BRANCH`, `COMMIT_REF`, `CACHED_COMMIT_REF`, `PULL_REQUEST`, `REVIEW_ID`, `URL`, `DEPLOY_URL`, `DEPLOY_PRIME_URL`, `SITE_ID`, `SITE_NAME`.\n\n## Secrets Controller\n\nFlag a var as secret: `Contains secret values` (UI) / `--secret` (CLI) / `is_secret: true` (API). Enforced, non-customizable policy:\n- Secret values are **write-only** — no readable version after set; the flag can't be removed to reveal it.\n- Secrets need explicit contexts + scopes; **cannot** carry the `post processing` scope.\n- Only code on Netlify (edge/serverless/build) reads unmasked values; off-Netlify sees masked. The `dev`-context value is exempt (unmasked from UI/CLI/API); `netlify build` never emits raw values.\n\n**Secret scanning** runs automatically once any var is secret (and via smart detection). Fails the build on detection and logs the location. Configure via env vars set per context:\n- `SECRETS_SCAN_ENABLED=false` — disables **all** scanning (loses all secret protection).\n- `SECRETS_SCAN_SMART_DETECTION_ENABLED=false` — disables smart detection only.\n- `SECRETS_SCAN_OMIT_KEYS`, `SECRETS_SCAN_OMIT_PATHS` (comma lists; paths from repo root, globs OK).\n- `SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES` — safelist false positives (**prefer** this over disabling). Smart detection is Personal/Pro/Enterprise.\n\nScanning covers all build files, values >4 chars and non-boolean, searching plaintext + base64 + URI-encoded permutations.\n\n### Sensitive variable policy (public repos only)\nGoverns whether **untrusted** deploys (unrecognized authors) get sensitive vars. Site members' Git deploys are always trusted, even from forks. Set at Project configuration > Environment variables > Site policies:\n- **Require approval** (default) — untrusted deploys wait for a member's approval.\n- **Deploy without sensitive variables** — builds run, sensitive vars withheld.\n- **Deploy without restrictions** — all vars present.\n\nNOT available for GitHub Enterprise Server / GitLab self-managed repos (treated as private).\n\n## Ignore builds\n\n`ignore` under `[build]` decides whether to rebuild — runs from the base directory in Bash (or Node.js 18, fixed; site `package.json` deps **not** available). **Exit `1` = changed → build continues; exit `0` = no change → build stops.** A build hook always builds regardless of exit code.\n```toml\n[build]\n ignore = \"git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/blog-1 packages/common\"\n```\n```toml\n[build]\n ignore = \"node ignore_build.js\" # separate file paths must start with ./\n```\n```js\n// ignore_build.js\nprocess.exitCode = process.env.BRANCH.includes(\"debug\") ? 0 : 1\n```\n\n## Monorepos\n\nSet the site subdirectory as the **package directory** (keep its `netlify.toml` there), leave base at root `/`, declare deps at the subdirectory level. Package directory is **UI-only — cannot be set in `netlify.toml`** (Project configuration > Developer settings > Continuous deployment > Build settings). Config file discovery order: package dir → base dir → root. Paths in `netlify.toml` stay absolute relative to the base directory. `netlify <cmd> --filter <site>` selects a site.\n\n## JavaScript SPAs\n\nBuild command `npm run <script>` / `yarn <script>`; publish dir often `dist` (framework-dependent). Add the `/* /index.html 200` fallback (above) for `pushState` routing. Code splitting + hashed filenames with atomic deploys can throw `Uncaught SyntaxError: Unexpected token` on stale references — disable hashed filenames, use permalinks, or a service worker.\n\n## Netlify Dev `[dev]`\n\nDoes **NOT** run in Bash (no Bash syntax in `command`). There is **no `environment` key** — set local env vars under `[context.dev.environment]`.\n```toml\n[dev]\n command = \"yarn start\"\n targetPort = 3000 # if both command + targetPort set, framework must be \"#custom\"\n port = 8888\n framework = \"#custom\"\n [dev.https]\n certFile = \"cert.pem\"\n keyFile = \"key.pem\"\n```\n\n## Plugins & extensions\n\n```toml\n[[plugins]]\npackage = \"netlify-plugin-check-output-for-puppy-references\"\n [plugins.inputs]\n breeds = [\"pomeranian\", \"chihuahua\"]\n\n[[integrations]] # build-time extension; install on team first\n name = \"abc-performance-extension\"\n [integrations.config]\n output_path = \"reports/performance-reports.html\"\n```\n\nFull reference pages: build environment variables at https://docs.netlify.com/build/configure-builds/environment-variables.md, env-var overview at https://docs.netlify.com/build/environment-variables/overview.md, Secrets Controller at https://docs.netlify.com/build/environment-variables/secrets-controller.md, redirects at https://docs.netlify.com/manage/routing/redirects/overview.md, redirect options at https://docs.netlify.com/manage/routing/redirects/redirect-options.md, rewrites/proxies at https://docs.netlify.com/manage/routing/redirects/rewrites-proxies.md, custom headers at https://docs.netlify.com/manage/routing/headers.md, and file-based config at https://docs.netlify.com/build/configure-builds/file-based-configuration.md.\n\n<!-- Plan gating for the sensitive variable policy itself is unspecified in the sources; only its public-repo requirement and the smart-detection plan list are documented. -->\n\n<!-- system: agent-context/config/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (config)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Env vars set in `netlify.toml` are NOT available to functions or edge\n functions at runtime — reading them there returns `undefined`. Set\n runtime vars in the UI or with `netlify env:set`, not `netlify.toml`.\n2. Never put secrets in client-prefixed env vars (`VITE_`, `NEXT_PUBLIC_`,\n `PUBLIC_`, ...) — they are inlined into the client bundle; `--secret`\n does not protect them.\n3. When snapshotting env vars locally (`netlify env:list --plain > .env`),\n keep `.env` gitignored — never commit it.\n4. State env-var scope interaction explicitly: a site variable scoped to\n Builds does not shadow the shared variable for other scopes — precedence\n resolves independently per scope (site beats shared only within the\n scopes the site variable actually carries).\n"SKILL.md line diff
--- before +++ after @@ -1,175 +1,300 @@ --- name: netlify-config -description: Reference for netlify.toml configuration. Use when configuring build settings, redirects, rewrites, headers, deploy contexts, environment variables, or any site-level configuration. Covers the complete netlify.toml syntax including redirects with splats/conditions, headers, deploy contexts, functions config, and edge functions config. +description: Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify. --- -# Netlify Configuration (netlify.toml) +# Netlify configuration -Place `netlify.toml` at the repository root (or at the base directory for monorepos). +Config lives in three files at the repo **root** (or the base/package directory for monorepos): +- `netlify.toml` — build, contexts, plugins, functions, redirects, headers, dev. +- `_redirects` — plain-text redirect/rewrite rules, saved to the **publish directory**, no extension. +- `_headers` — plain-text response headers, saved to the **publish directory**. + +`netlify.toml` values **take precedence over the Netlify UI** when they conflict. Paths in `netlify.toml` are absolute relative to the **base directory** (root `/` by default). + +## Modern vs legacy syntax to reach for +- Functions bundler: use `node_bundler = "esbuild"`. `zisi` is the legacy JS default; TypeScript always uses `esbuild`. +- Temporary redirect: use `status = 302`. `307` is **unsupported**. +- Gatsby Image CDN: use `NETLIFY_IMAGE_CDN`, not the deprecated `GATSBY_CLOUD_IMAGE_CDN`. +- Injecting env values into TOML: `key = "$VAR"` is **NOT supported** (except `signed` in proxy redirects). Use a build-command `sed` substitution or a build plugin (see below). -## Build Settings +## `netlify.toml` build + contexts ```toml [build] - base = "project/" # Base directory (default: root) - command = "npm run build" # Build command - publish = "dist/" # Output directory + base = "frontend" + publish = "dist" + command = "npm run build" + environment = { NODE_VERSION = "18" } + +[context.production] + publish = "output/" + command = "make publish" + +[context.deploy-preview] + publish = "dist/" + +[context."feat/branch"] # quote names with special characters + command = "npm run preview" ``` -## Redirects +`[build]` runs in **Bash**. Context-aware keys include `[build]` and `[[plugins]]` — but **NOT** `[[redirects]]` or `[[headers]]` (those are always global). Precedence, least→most specific: UI < toml < any-context property < `[context.<name>]` < `[context.branchname]`. -```toml -# Basic redirect -[[redirects]] -from = "/old" -to = "/new" -status = 301 # 301 (default), 302, 200 (rewrite), 404 +## Redirects and rewrites -# SPA catch-all -[[redirects]] -from = "/*" -to = "/index.html" -status = 200 +`_redirects` rules are processed **first**, then `netlify.toml`; within each, the **first matching rule top-to-bottom wins** — list specific rules before general ones. Edge functions run before redirects. -# Splat (wildcard) +SPA history-`pushState` fallback (required for clean URLs): +``` +/* /index.html 200 +``` +```toml [[redirects]] -from = "/blog/*" -to = "/news/:splat" + from = "/*" + to = "/index.html" + status = 200 +``` -# Path parameters -[[redirects]] -from = "/users/:id" -to = "/api/users/:id" -status = 200 +`_redirects` syntax — `from to [status] [conditions]`, `#` comments, paths case-sensitive, URL-encode special chars: +``` +/home / 301 +/my-redirect / 302 +/ecommerce /store-closed 404 # custom 404 for a path +/pass-through /index.html 200 # rewrite +/best-pets/dogs /best-pets/cats.html 200! # force/shadow (! or force=true) +/news/* /blog/:splat # splat +/news/:month/:date/:year/:slug /blog/:year/:month/:date/:slug # placeholders +/store id=:id /blog/:id 301 # query params +/ /anz 302 Country=au,nz # no spaces in value list +/israel/* /israel/he/:splat 302 Language=he +/* /legacy/:splat 200 Cookie=is_legacy,my_other_cookie +``` -# Force (override existing files) -[[redirects]] -from = "/app/*" -to = "/index.html" -status = 200 -force = true +`[[redirects]]` keywords: `from`, `to`, `status` (default `301`), `force` (default `false`; `!`/shadow), `query` (`query = {path = ":path"}`), `conditions` (`{Language, Country, Role, Cookie}`), `headers` (proxy request headers), `signed` (env var name for signed proxies). -# Proxy to external service -[[redirects]] -from = "/api/*" -to = "https://api.example.com/:splat" -status = 200 -[redirects.headers] - X-Custom = "value" +**Gotchas:** +- You **cannot** add/remove a trailing slash with a redirect — CDN normalizes URLs first; a `/x/ → /x 301!` rule loops infinitely. Rely on Pretty URLs (default on). +- Splat asterisks work only at the **end** of a segment (`/jobs/*`), not mid-path (`/jobs/*.html` invalid). Placeholders (`:x`) only at the start of a segment; can't mix wildcard+placeholder in one segment. +- You can't exclude a path from a splat; put a more specific rule first. +- `Country` = ISO 3166-1 alpha-2; language redirects match only the **first** `Accept-Language` entry. +- Role-based redirects with external auth providers are **Enterprise-only**. +- 10,000+ redirects: use wildcards/placeholders or Edge Functions — oversized serialized output fails the deploy. -# Country/language conditions +## Proxies + +``` +/api/* https://api.example.com/:splat 200 +/netlify-site/* https://my-other-site.netlify.app/:splat 200 # use .netlify.app, not custom domain +``` +```toml +[[redirects]] # custom request headers + force + from = "/search" + to = "https://api.mysearch.com" + status = 200 + force = true + headers = {X-From = "Netlify"} +``` +Signed proxy (`signed` names an env var scoped to **Runtime**; must live in `netlify.toml`; JWS is external-only, not Netlify→Netlify): +```toml [[redirects]] -from = "/*" -to = "/fr/:splat" -status = 200 -conditions = { Country = ["FR"], Language = ["fr"] } + from = "/search" + to = "https://api.mysearch.com" + status = 200 + force = true + signed = "API_SIGNATURE_TOKEN_PLACEHOLDER" ``` -**Rule order matters** — Netlify processes the first matching rule. Place specific rules before general ones. +**Gotchas:** cross-team rewrites disallowed; same-password-site rewrites OK but not across separate protected sites; proxy timeout **26 s**; one hop by default; relative-path assets break (use absolute or `<base>`); loops silently ignored. -## Headers +## Custom headers +``` +/* + X-Frame-Options: DENY +/templates/index2.html + X-Frame-Options: SAMEORIGIN +``` +Multi-value — repeat the key (`_headers`) or a multiline TOML string: ```toml [[headers]] -for = "/*" -[headers.values] - X-Frame-Options = "DENY" - X-Content-Type-Options = "nosniff" + for = "/*" + [headers.values] + cache-control = ''' + max-age=0, + no-cache, + no-store, + must-revalidate''' +``` -[[headers]] -for = "/assets/*" -[headers.values] - Cache-Control = "public, max-age=31536000, immutable" +**Gotchas:** +- Headers in `_headers`/`netlify.toml` are **global** — NOT scoped to branch/context. Workaround: strip global headers, keep header files in a custom dir, and `cp` them into the publish dir from a per-context build command: + ```toml + [context.staging] + command = "npm run build && cp ./custom-headers/_stagingHeaders ./dist/_headers" + ``` +- Headers apply only to files from Netlify's store — **NOT** to proxied content or function/edge (SSR) responses; those must set their own headers. +- Ignored (server-set) names include `Content-Length`, `Content-Encoding`, `Location` (use redirects), `Set-Cookie`, `Server`, etc. +- Basic auth headers: **Pro/Enterprise only**. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List). + +## Functions + +```toml +[functions] + directory = "myfunctions/" # default: <base>/netlify/functions + node_bundler = "esbuild" + external_node_modules = ["package-1"] # esbuild only; native add-ons etc. + included_files = ["files/*.md"] # ! prefix excludes + +[functions."api_*"] # glob/named blocks concatenate with top-level + external_node_modules = ["package-2"] + included_files = ["!files/post-1.md"] ``` -Headers apply only to files served from Netlify's CDN (not to function or edge function responses — set those in code). +## Environment variables -## Deploy Contexts +Two storage methods: +- **UI / CLI / API** — stored on Netlify (not the repo). Supports site + shared vars, per-context values, scopes; reaches builds, functions/edge/ODB, snippet injection, forms, signed proxies. **Recommended for anything sensitive.** +- **`netlify.toml`** — stored in the repo. Site vars only, per-context values, **no scope selection** (everything gets **Builds** + **Post processing**), reaches builds + snippet injection only. -Override settings per deploy context: +`netlify.toml` env vars **override** same-key UI/CLI/API vars. +Per-context values in TOML: ```toml [context.production] -command = "npm run build" -environment = { NODE_ENV = "production" } + environment = { NODE_VERSION = "14.15.3" } +[context.deploy-preview.environment] + NOT_PRIVATE_ITEM = "not so secret" +[context.branch-deploy.environment] + NODE_ENV = "development" +``` -[context.deploy-preview] -command = "npm run build:preview" +CLI: +```bash +netlify env:set KEY value # --secret marks it a secret +netlify env:import .env # site vars; --replace-existing wipes others first +netlify env:unset KEY +netlify env:list --plain --context production > .env +netlify build # local build with Netlify's env vars +``` +API: `createEnvVars` / `updateEnvVar` (`is_secret: true`) / `setEnvVarValue` / `deleteEnvVar` / `deleteEnvVarValue`. -[context.branch-deploy] -command = "npm run build:staging" +**Access syntax:** Bash `$VAR` in `build.command`/`ignore.command`; `process.env.VAR` in Node scripts and plugins. -[context.dev] -environment = { NODE_ENV = "development" } +**Scopes** (Pro/Enterprise; default all): Builds (site builds) · Functions (Functions/Edge/ODB) · Runtime (forms, signed proxies) · Post processing (snippet injection). Shared vars are Pro/Enterprise and **Team-Owner-only** to read/edit. Precedence for a site+shared key collision resolves **per scope** — a site var only wins within the scopes it actually carries. -# Specific branch -[context."staging"] -command = "npm run build:staging" -``` +**Naming/limits:** keys alphanumeric + underscore, must start with a letter (`1KEY`, `_KEY1` invalid); keys ≤255 chars, values ≤5,000 chars. Read-only variable names are reserved. Changes need a build + deploy. -## Environment Variables +**Set the build language via reserved config vars** — `NODE_VERSION`, `NPM_FLAGS`, `YARN_VERSION`, `BUN_VERSION`, `RUBY_VERSION`, `PHP_VERSION`, `PYTHON_VERSION`, `GO_VERSION`, `HUGO_VERSION`, `PNPM_FLAGS`, `NPM_TOKEN` (Yarn: `YARN_NPM_AUTH_TOKEN`), etc. -```toml -[build.environment] -NODE_VERSION = "20" +**Must be set in UI/CLI/API, NOT `netlify.toml`** (read after the repo is cloned or a runtime-only var): `AWS_LAMBDA_JS_RUNTIME`, `GIT_LFS_ENABLED`, `GIT_LFS_FETCH_INCLUDE`, `NETLIFY_BUILD_DEBUG`. -[context.production.environment] -API_URL = "https://api.prod.com" +**`CI` gotcha:** defaults to `true`; if it breaks a build, prepend `CI='' ` to the build command. -[context.deploy-preview.environment] -API_URL = "https://api.staging.com" +### Injecting env values into headers/redirects +`key = "$VAR"` is unsupported. Only path (scope must include **Builds**): +```toml +[build] + command = "sed -i \"s|HEADER_PLACEHOLDER|${PROD_API_LOCATION}|g\" netlify.toml && yarn build" ``` +`sed` substitution works **only** for `[[headers]]`/`[[redirects]]` (read after the build) and is **not** visible to build plugins (they run before the build command). For plugin-visible changes, use a local build plugin editing `netlifyConfig`. -**Do not put secrets in netlify.toml** (it's committed to source control). Use the Netlify UI or CLI for sensitive values. See the **netlify-cli-and-deploy** skill for CLI environment variable management. +### Useful read-only build vars +`CONTEXT` (`production`/`deploy-preview`/`branch-deploy`/`dev`), `BRANCH`, `COMMIT_REF`, `CACHED_COMMIT_REF`, `PULL_REQUEST`, `REVIEW_ID`, `URL`, `DEPLOY_URL`, `DEPLOY_PRIME_URL`, `SITE_ID`, `SITE_NAME`. -## Functions Configuration +## Secrets Controller -```toml -[functions] -directory = "netlify/functions" # Default -node_bundler = "esbuild" +Flag a var as secret: `Contains secret values` (UI) / `--secret` (CLI) / `is_secret: true` (API). Enforced, non-customizable policy: +- Secret values are **write-only** — no readable version after set; the flag can't be removed to reveal it. +- Secrets need explicit contexts + scopes; **cannot** carry the `post processing` scope. +- Only code on Netlify (edge/serverless/build) reads unmasked values; off-Netlify sees masked. The `dev`-context value is exempt (unmasked from UI/CLI/API); `netlify build` never emits raw values. -# Scheduled function -[functions."cleanup"] -schedule = "@daily" -``` +**Secret scanning** runs automatically once any var is secret (and via smart detection). Fails the build on detection and logs the location. Configure via env vars set per context: +- `SECRETS_SCAN_ENABLED=false` — disables **all** scanning (loses all secret protection). +- `SECRETS_SCAN_SMART_DETECTION_ENABLED=false` — disables smart detection only. +- `SECRETS_SCAN_OMIT_KEYS`, `SECRETS_SCAN_OMIT_PATHS` (comma lists; paths from repo root, globs OK). +- `SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES` — safelist false positives (**prefer** this over disabling). Smart detection is Personal/Pro/Enterprise. -## Edge Functions Configuration +Scanning covers all build files, values >4 chars and non-boolean, searching plaintext + base64 + URI-encoded permutations. -```toml -[[edge_functions]] -path = "/admin" -function = "auth" +### Sensitive variable policy (public repos only) +Governs whether **untrusted** deploys (unrecognized authors) get sensitive vars. Site members' Git deploys are always trusted, even from forks. Set at Project configuration > Environment variables > Site policies: +- **Require approval** (default) — untrusted deploys wait for a member's approval. +- **Deploy without sensitive variables** — builds run, sensitive vars withheld. +- **Deploy without restrictions** — all vars present. -# Import map for Deno URL imports -[functions] -deno_import_map = "./import_map.json" +NOT available for GitHub Enterprise Server / GitLab self-managed repos (treated as private). + +## Ignore builds + +`ignore` under `[build]` decides whether to rebuild — runs from the base directory in Bash (or Node.js 18, fixed; site `package.json` deps **not** available). **Exit `1` = changed → build continues; exit `0` = no change → build stops.** A build hook always builds regardless of exit code. +```toml +[build] + ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/blog-1 packages/common" +``` +```toml +[build] + ignore = "node ignore_build.js" # separate file paths must start with ./ +``` +```js +// ignore_build.js +process.exitCode = process.env.BRANCH.includes("debug") ? 0 : 1 ``` -## Dev Server +## Monorepos +Set the site subdirectory as the **package directory** (keep its `netlify.toml` there), leave base at root `/`, declare deps at the subdirectory level. Package directory is **UI-only — cannot be set in `netlify.toml`** (Project configuration > Developer settings > Continuous deployment > Build settings). Config file discovery order: package dir → base dir → root. Paths in `netlify.toml` stay absolute relative to the base directory. `netlify <cmd> --filter <site>` selects a site. + +## JavaScript SPAs + +Build command `npm run <script>` / `yarn <script>`; publish dir often `dist` (framework-dependent). Add the `/* /index.html 200` fallback (above) for `pushState` routing. Code splitting + hashed filenames with atomic deploys can throw `Uncaught SyntaxError: Unexpected token` on stale references — disable hashed filenames, use permalinks, or a service worker. + +## Netlify Dev `[dev]` + +Does **NOT** run in Bash (no Bash syntax in `command`). There is **no `environment` key** — set local env vars under `[context.dev.environment]`. ```toml [dev] -command = "npm start" # Dev server command -port = 8888 # Netlify Dev port -targetPort = 3000 # Your app's dev server port -framework = "#auto" # "#auto", "#static", "#custom" + command = "yarn start" + targetPort = 3000 # if both command + targetPort set, framework must be "#custom" + port = 8888 + framework = "#custom" + [dev.https] + certFile = "cert.pem" + keyFile = "key.pem" ``` -## Plugins +## Plugins & extensions ```toml [[plugins]] -package = "@netlify/plugin-lighthouse" -[plugins.inputs] - audits = ["performance", "accessibility"] +package = "netlify-plugin-check-output-for-puppy-references" + [plugins.inputs] + breeds = ["pomeranian", "chihuahua"] + +[[integrations]] # build-time extension; install on team first + name = "abc-performance-extension" + [integrations.config] + output_path = "reports/performance-reports.html" ``` -## Image CDN +Full reference pages: build environment variables at https://docs.netlify.com/build/configure-builds/environment-variables.md, env-var overview at https://docs.netlify.com/build/environment-variables/overview.md, Secrets Controller at https://docs.netlify.com/build/environment-variables/secrets-controller.md, redirects at https://docs.netlify.com/manage/routing/redirects/overview.md, redirect options at https://docs.netlify.com/manage/routing/redirects/redirect-options.md, rewrites/proxies at https://docs.netlify.com/manage/routing/redirects/rewrites-proxies.md, custom headers at https://docs.netlify.com/manage/routing/headers.md, and file-based config at https://docs.netlify.com/build/configure-builds/file-based-configuration.md. -```toml -[images] -remote_images = ["https://example\\.com/.*"] -``` +<!-- Plan gating for the sensitive variable policy itself is unspecified in the sources; only its public-repo requirement and the smart-detection plan list are documented. --> + +<!-- system: agent-context/config/system.md — human-owned, merged by ctx-gen; edit system.md, not this section --> +# Netlify house rules (config) -See the **netlify-image-cdn** skill for full Image CDN usage. +These are org conventions, not docs facts — merged into the rendered skill by +ctx-gen and never generated. Owned by the skills maintainer. + +1. Env vars set in `netlify.toml` are NOT available to functions or edge + functions at runtime — reading them there returns `undefined`. Set + runtime vars in the UI or with `netlify env:set`, not `netlify.toml`. +2. Never put secrets in client-prefixed env vars (`VITE_`, `NEXT_PUBLIC_`, + `PUBLIC_`, ...) — they are inlined into the client bundle; `--secret` + does not protect them. +3. When snapshotting env vars locally (`netlify env:list --plain > .env`), + keep `.env` gitignored — never commit it. +4. State env-var scope interaction explicitly: a site variable scoped to + Builds does not shadow the shared variable for other scopes — precedence + resolves independently per scope (site beats shared only within the + scopes the site variable actually carries).
Full snapshot data
{
"description": "Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify.",
"included_files": [],
"name": "netlify-config",
"skill_md_contents": "---\nname: netlify-config\ndescription: Configure Netlify builds and routing via netlify.toml, _redirects, and _headers. Use when setting a build command or publish directory, adding redirects or rewrites or proxies, adding an SPA fallback rewrite, setting custom response headers or basic auth, managing environment variables and secrets, scoping vars per deploy context, marking a var as secret, disabling secret scanning, configuring functions bundling, ignoring builds, or wiring up a monorepo or JavaScript SPA on Netlify.\n---\n\n# Netlify configuration\n\nConfig lives in three files at the repo **root** (or the base/package directory for monorepos):\n- `netlify.toml` — build, contexts, plugins, functions, redirects, headers, dev.\n- `_redirects` — plain-text redirect/rewrite rules, saved to the **publish directory**, no extension.\n- `_headers` — plain-text response headers, saved to the **publish directory**.\n\n`netlify.toml` values **take precedence over the Netlify UI** when they conflict. Paths in `netlify.toml` are absolute relative to the **base directory** (root `/` by default).\n\n## Modern vs legacy syntax to reach for\n- Functions bundler: use `node_bundler = \"esbuild\"`. `zisi` is the legacy JS default; TypeScript always uses `esbuild`.\n- Temporary redirect: use `status = 302`. `307` is **unsupported**.\n- Gatsby Image CDN: use `NETLIFY_IMAGE_CDN`, not the deprecated `GATSBY_CLOUD_IMAGE_CDN`.\n- Injecting env values into TOML: `key = \"$VAR\"` is **NOT supported** (except `signed` in proxy redirects). Use a build-command `sed` substitution or a build plugin (see below).\n\n## `netlify.toml` build + contexts\n\n```toml\n[build]\n base = \"frontend\"\n publish = \"dist\"\n command = \"npm run build\"\n environment = { NODE_VERSION = \"18\" }\n\n[context.production]\n publish = \"output/\"\n command = \"make publish\"\n\n[context.deploy-preview]\n publish = \"dist/\"\n\n[context.\"feat/branch\"] # quote names with special characters\n command = \"npm run preview\"\n```\n\n`[build]` runs in **Bash**. Context-aware keys include `[build]` and `[[plugins]]` — but **NOT** `[[redirects]]` or `[[headers]]` (those are always global). Precedence, least→most specific: UI < toml < any-context property < `[context.<name>]` < `[context.branchname]`.\n\n## Redirects and rewrites\n\n`_redirects` rules are processed **first**, then `netlify.toml`; within each, the **first matching rule top-to-bottom wins** — list specific rules before general ones. Edge functions run before redirects.\n\nSPA history-`pushState` fallback (required for clean URLs):\n```\n/* /index.html 200\n```\n```toml\n[[redirects]]\n from = \"/*\"\n to = \"/index.html\"\n status = 200\n```\n\n`_redirects` syntax — `from to [status] [conditions]`, `#` comments, paths case-sensitive, URL-encode special chars:\n```\n/home / 301\n/my-redirect / 302\n/ecommerce /store-closed 404 # custom 404 for a path\n/pass-through /index.html 200 # rewrite\n/best-pets/dogs /best-pets/cats.html 200! # force/shadow (! or force=true)\n/news/* /blog/:splat # splat\n/news/:month/:date/:year/:slug /blog/:year/:month/:date/:slug # placeholders\n/store id=:id /blog/:id 301 # query params\n/ /anz 302 Country=au,nz # no spaces in value list\n/israel/* /israel/he/:splat 302 Language=he\n/* /legacy/:splat 200 Cookie=is_legacy,my_other_cookie\n```\n\n`[[redirects]]` keywords: `from`, `to`, `status` (default `301`), `force` (default `false`; `!`/shadow), `query` (`query = {path = \":path\"}`), `conditions` (`{Language, Country, Role, Cookie}`), `headers` (proxy request headers), `signed` (env var name for signed proxies).\n\n**Gotchas:**\n- You **cannot** add/remove a trailing slash with a redirect — CDN normalizes URLs first; a `/x/ → /x 301!` rule loops infinitely. Rely on Pretty URLs (default on).\n- Splat asterisks work only at the **end** of a segment (`/jobs/*`), not mid-path (`/jobs/*.html` invalid). Placeholders (`:x`) only at the start of a segment; can't mix wildcard+placeholder in one segment.\n- You can't exclude a path from a splat; put a more specific rule first.\n- `Country` = ISO 3166-1 alpha-2; language redirects match only the **first** `Accept-Language` entry.\n- Role-based redirects with external auth providers are **Enterprise-only**.\n- 10,000+ redirects: use wildcards/placeholders or Edge Functions — oversized serialized output fails the deploy.\n\n## Proxies\n\n```\n/api/* https://api.example.com/:splat 200\n/netlify-site/* https://my-other-site.netlify.app/:splat 200 # use .netlify.app, not custom domain\n```\n```toml\n[[redirects]] # custom request headers + force\n from = \"/search\"\n to = \"https://api.mysearch.com\"\n status = 200\n force = true\n headers = {X-From = \"Netlify\"}\n```\nSigned proxy (`signed` names an env var scoped to **Runtime**; must live in `netlify.toml`; JWS is external-only, not Netlify→Netlify):\n```toml\n[[redirects]]\n from = \"/search\"\n to = \"https://api.mysearch.com\"\n status = 200\n force = true\n signed = \"API_SIGNATURE_TOKEN_PLACEHOLDER\"\n```\n\n**Gotchas:** cross-team rewrites disallowed; same-password-site rewrites OK but not across separate protected sites; proxy timeout **26 s**; one hop by default; relative-path assets break (use absolute or `<base>`); loops silently ignored.\n\n## Custom headers\n\n```\n/*\n X-Frame-Options: DENY\n/templates/index2.html\n X-Frame-Options: SAMEORIGIN\n```\nMulti-value — repeat the key (`_headers`) or a multiline TOML string:\n```toml\n[[headers]]\n for = \"/*\"\n [headers.values]\n cache-control = '''\n max-age=0,\n no-cache,\n no-store,\n must-revalidate'''\n```\n\n**Gotchas:**\n- Headers in `_headers`/`netlify.toml` are **global** — NOT scoped to branch/context. Workaround: strip global headers, keep header files in a custom dir, and `cp` them into the publish dir from a per-context build command:\n ```toml\n [context.staging]\n command = \"npm run build && cp ./custom-headers/_stagingHeaders ./dist/_headers\"\n ```\n- Headers apply only to files from Netlify's store — **NOT** to proxied content or function/edge (SSR) responses; those must set their own headers.\n- Ignored (server-set) names include `Content-Length`, `Content-Encoding`, `Location` (use redirects), `Set-Cookie`, `Server`, etc.\n- Basic auth headers: **Pro/Enterprise only**. Cross-subdomain cookies need a custom domain (`netlify.app` is on the Public Suffix List).\n\n## Functions\n\n```toml\n[functions]\n directory = \"myfunctions/\" # default: <base>/netlify/functions\n node_bundler = \"esbuild\"\n external_node_modules = [\"package-1\"] # esbuild only; native add-ons etc.\n included_files = [\"files/*.md\"] # ! prefix excludes\n\n[functions.\"api_*\"] # glob/named blocks concatenate with top-level\n external_node_modules = [\"package-2\"]\n included_files = [\"!files/post-1.md\"]\n```\n\n## Environment variables\n\nTwo storage methods:\n- **UI / CLI / API** — stored on Netlify (not the repo). Supports site + shared vars, per-context values, scopes; reaches builds, functions/edge/ODB, snippet injection, forms, signed proxies. **Recommended for anything sensitive.**\n- **`netlify.toml`** — stored in the repo. Site vars only, per-context values, **no scope selection** (everything gets **Builds** + **Post processing**), reaches builds + snippet injection only.\n\n`netlify.toml` env vars **override** same-key UI/CLI/API vars.\n\nPer-context values in TOML:\n```toml\n[context.production]\n environment = { NODE_VERSION = \"14.15.3\" }\n[context.deploy-preview.environment]\n NOT_PRIVATE_ITEM = \"not so secret\"\n[context.branch-deploy.environment]\n NODE_ENV = \"development\"\n```\n\nCLI:\n```bash\nnetlify env:set KEY value # --secret marks it a secret\nnetlify env:import .env # site vars; --replace-existing wipes others first\nnetlify env:unset KEY\nnetlify env:list --plain --context production > .env\nnetlify build # local build with Netlify's env vars\n```\nAPI: `createEnvVars` / `updateEnvVar` (`is_secret: true`) / `setEnvVarValue` / `deleteEnvVar` / `deleteEnvVarValue`.\n\n**Access syntax:** Bash `$VAR` in `build.command`/`ignore.command`; `process.env.VAR` in Node scripts and plugins.\n\n**Scopes** (Pro/Enterprise; default all): Builds (site builds) · Functions (Functions/Edge/ODB) · Runtime (forms, signed proxies) · Post processing (snippet injection). Shared vars are Pro/Enterprise and **Team-Owner-only** to read/edit. Precedence for a site+shared key collision resolves **per scope** — a site var only wins within the scopes it actually carries.\n\n**Naming/limits:** keys alphanumeric + underscore, must start with a letter (`1KEY`, `_KEY1` invalid); keys ≤255 chars, values ≤5,000 chars. Read-only variable names are reserved. Changes need a build + deploy.\n\n**Set the build language via reserved config vars** — `NODE_VERSION`, `NPM_FLAGS`, `YARN_VERSION`, `BUN_VERSION`, `RUBY_VERSION`, `PHP_VERSION`, `PYTHON_VERSION`, `GO_VERSION`, `HUGO_VERSION`, `PNPM_FLAGS`, `NPM_TOKEN` (Yarn: `YARN_NPM_AUTH_TOKEN`), etc.\n\n**Must be set in UI/CLI/API, NOT `netlify.toml`** (read after the repo is cloned or a runtime-only var): `AWS_LAMBDA_JS_RUNTIME`, `GIT_LFS_ENABLED`, `GIT_LFS_FETCH_INCLUDE`, `NETLIFY_BUILD_DEBUG`.\n\n**`CI` gotcha:** defaults to `true`; if it breaks a build, prepend `CI='' ` to the build command.\n\n### Injecting env values into headers/redirects\n`key = \"$VAR\"` is unsupported. Only path (scope must include **Builds**):\n```toml\n[build]\n command = \"sed -i \\\"s|HEADER_PLACEHOLDER|${PROD_API_LOCATION}|g\\\" netlify.toml && yarn build\"\n```\n`sed` substitution works **only** for `[[headers]]`/`[[redirects]]` (read after the build) and is **not** visible to build plugins (they run before the build command). For plugin-visible changes, use a local build plugin editing `netlifyConfig`.\n\n### Useful read-only build vars\n`CONTEXT` (`production`/`deploy-preview`/`branch-deploy`/`dev`), `BRANCH`, `COMMIT_REF`, `CACHED_COMMIT_REF`, `PULL_REQUEST`, `REVIEW_ID`, `URL`, `DEPLOY_URL`, `DEPLOY_PRIME_URL`, `SITE_ID`, `SITE_NAME`.\n\n## Secrets Controller\n\nFlag a var as secret: `Contains secret values` (UI) / `--secret` (CLI) / `is_secret: true` (API). Enforced, non-customizable policy:\n- Secret values are **write-only** — no readable version after set; the flag can't be removed to reveal it.\n- Secrets need explicit contexts + scopes; **cannot** carry the `post processing` scope.\n- Only code on Netlify (edge/serverless/build) reads unmasked values; off-Netlify sees masked. The `dev`-context value is exempt (unmasked from UI/CLI/API); `netlify build` never emits raw values.\n\n**Secret scanning** runs automatically once any var is secret (and via smart detection). Fails the build on detection and logs the location. Configure via env vars set per context:\n- `SECRETS_SCAN_ENABLED=false` — disables **all** scanning (loses all secret protection).\n- `SECRETS_SCAN_SMART_DETECTION_ENABLED=false` — disables smart detection only.\n- `SECRETS_SCAN_OMIT_KEYS`, `SECRETS_SCAN_OMIT_PATHS` (comma lists; paths from repo root, globs OK).\n- `SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES` — safelist false positives (**prefer** this over disabling). Smart detection is Personal/Pro/Enterprise.\n\nScanning covers all build files, values >4 chars and non-boolean, searching plaintext + base64 + URI-encoded permutations.\n\n### Sensitive variable policy (public repos only)\nGoverns whether **untrusted** deploys (unrecognized authors) get sensitive vars. Site members' Git deploys are always trusted, even from forks. Set at Project configuration > Environment variables > Site policies:\n- **Require approval** (default) — untrusted deploys wait for a member's approval.\n- **Deploy without sensitive variables** — builds run, sensitive vars withheld.\n- **Deploy without restrictions** — all vars present.\n\nNOT available for GitHub Enterprise Server / GitLab self-managed repos (treated as private).\n\n## Ignore builds\n\n`ignore` under `[build]` decides whether to rebuild — runs from the base directory in Bash (or Node.js 18, fixed; site `package.json` deps **not** available). **Exit `1` = changed → build continues; exit `0` = no change → build stops.** A build hook always builds regardless of exit code.\n```toml\n[build]\n ignore = \"git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/blog-1 packages/common\"\n```\n```toml\n[build]\n ignore = \"node ignore_build.js\" # separate file paths must start with ./\n```\n```js\n// ignore_build.js\nprocess.exitCode = process.env.BRANCH.includes(\"debug\") ? 0 : 1\n```\n\n## Monorepos\n\nSet the site subdirectory as the **package directory** (keep its `netlify.toml` there), leave base at root `/`, declare deps at the subdirectory level. Package directory is **UI-only — cannot be set in `netlify.toml`** (Project configuration > Developer settings > Continuous deployment > Build settings). Config file discovery order: package dir → base dir → root. Paths in `netlify.toml` stay absolute relative to the base directory. `netlify <cmd> --filter <site>` selects a site.\n\n## JavaScript SPAs\n\nBuild command `npm run <script>` / `yarn <script>`; publish dir often `dist` (framework-dependent). Add the `/* /index.html 200` fallback (above) for `pushState` routing. Code splitting + hashed filenames with atomic deploys can throw `Uncaught SyntaxError: Unexpected token` on stale references — disable hashed filenames, use permalinks, or a service worker.\n\n## Netlify Dev `[dev]`\n\nDoes **NOT** run in Bash (no Bash syntax in `command`). There is **no `environment` key** — set local env vars under `[context.dev.environment]`.\n```toml\n[dev]\n command = \"yarn start\"\n targetPort = 3000 # if both command + targetPort set, framework must be \"#custom\"\n port = 8888\n framework = \"#custom\"\n [dev.https]\n certFile = \"cert.pem\"\n keyFile = \"key.pem\"\n```\n\n## Plugins & extensions\n\n```toml\n[[plugins]]\npackage = \"netlify-plugin-check-output-for-puppy-references\"\n [plugins.inputs]\n breeds = [\"pomeranian\", \"chihuahua\"]\n\n[[integrations]] # build-time extension; install on team first\n name = \"abc-performance-extension\"\n [integrations.config]\n output_path = \"reports/performance-reports.html\"\n```\n\nFull reference pages: build environment variables at https://docs.netlify.com/build/configure-builds/environment-variables.md, env-var overview at https://docs.netlify.com/build/environment-variables/overview.md, Secrets Controller at https://docs.netlify.com/build/environment-variables/secrets-controller.md, redirects at https://docs.netlify.com/manage/routing/redirects/overview.md, redirect options at https://docs.netlify.com/manage/routing/redirects/redirect-options.md, rewrites/proxies at https://docs.netlify.com/manage/routing/redirects/rewrites-proxies.md, custom headers at https://docs.netlify.com/manage/routing/headers.md, and file-based config at https://docs.netlify.com/build/configure-builds/file-based-configuration.md.\n\n<!-- Plan gating for the sensitive variable policy itself is unspecified in the sources; only its public-repo requirement and the smart-detection plan list are documented. -->\n\n<!-- system: agent-context/config/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (config)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Env vars set in `netlify.toml` are NOT available to functions or edge\n functions at runtime — reading them there returns `undefined`. Set\n runtime vars in the UI or with `netlify env:set`, not `netlify.toml`.\n2. Never put secrets in client-prefixed env vars (`VITE_`, `NEXT_PUBLIC_`,\n `PUBLIC_`, ...) — they are inlined into the client bundle; `--secret`\n does not protect them.\n3. When snapshotting env vars locally (`netlify env:list --plain > .env`),\n keep `.env` gitignored — never commit it.\n4. State env-var scope interaction explicitly: a site variable scoped to\n Builds does not shadow the shared variable for other scopes — precedence\n resolves independently per scope (site beats shared only within the\n scopes the site variable actually carries).\n"
}SHA-256 of public snapshot: 0a7e949a06872b96878b76555513f32a68e9868b8d76047e73d94649f6793771