---
title: "How to configure netlify.toml redirects, headers, and env vars | Netlify Knowledge Base"
description: "Guides and articles to help you get the most out of the Netlify platform."
source: "https://www.netlify.com/knowledge-base/how-to-configure-netlifytoml-redirects-headers-and-env-vars/"
last_updated: "2026-09-23T19:52:53.000Z"
---
## Key takeaways

`netlify.toml` sits at your repo root and controls build settings, redirects, headers, and build-time environment variables. It **overrides the Netlify UI** when the two conflict. The `_headers` and `_redirects` files live in your publish directory and are processed **before** `netlify.toml` rules. Redirects match top-down, first match wins.

-   **Env vars in `netlify.toml` don’t reach your functions.** They only get the Builds and Post processing scopes. Runtime vars go through the UI or `netlify env:set`.
-   **`[[redirects]]` and `[[headers]]` are global.** They can’t be scoped to a branch or deploy context.
-   **Headers apply only to files Netlify serves itself.** Functions, proxied content, and SSR pages set their own.
-   **The SPA fallback is `/* /index.html 200`.** Status 200, not 301.
-   **Never put a secret in a client-prefixed var** (`VITE_`, `NEXT_PUBLIC_`, `PUBLIC_`). They’re inlined into your bundle, and `--secret` doesn’t help.
-   **`.env` isn’t read by the Netlify build system.** Import with `netlify env:import` first.

## The file that decides everything

Most Netlify questions that start with “why is it doing that” end up in this file. A redirect that fires when it shouldn’t. A header that’s silently ignored. An environment variable that’s set, visible in the dashboard, and `undefined` in your function.

The reason those get confusing is that `netlify.toml` isn’t one system. It’s four, with different rules about scoping, precedence, and where they apply. Once you know which of the four you’re touching, the surprising behavior stops being surprising. Let’s go through them, and lead with the footguns rather than burying them.

## Footguns first

These account for most of the lost time. Read them before you write config.

**Env vars in `netlify.toml` are not available to functions or edge functions at runtime.** Reading them there returns `undefined`. Variables declared in `netlify.toml` are locked to the **Builds** and **Post processing** scopes only. Set runtime variables in the UI or with `netlify env:set`.

**Never put secrets in client-prefixed variables.** `VITE_`, `NEXT_PUBLIC_`, `PUBLIC_`, and friends are inlined into the client bundle by your framework. Marking them `--secret` does not protect them. They ship to the browser.

**`.env` is not read by the Netlify build system.** Import your variables into Netlify first with `netlify env:import`. The CLI reads `.env` only for local builds.

**Direct env injection into `netlify.toml` doesn’t work.** `key = "$VAR"` is unsupported, with one exception: signed proxy redirects. Use a build plugin or `sed` in the build command instead.

**`[[redirects]]` and `[[headers]]` are global and not context-aware.** You can’t scope them to a branch or deploy context. The workaround is a per-context build command that copies a different file into the publish directory.

**Proxy rewrites time out at 26 seconds**, and HTTP `307` is unsupported. Use `302`.

## Worked example: core structure

```
[build]  base = "project/"          # base directory  publish = "build-output/"  # relative to base, default /  command = "npm run build"  # runs in Bash shell  [build.environment]    NODE_VERSION = "24"
[context.production]         # production deploy context  command = "make publish"  environment = { NODE_VERSION = "24" }[context.deploy-preview]     # PR/MR previews  publish = "dist/"[context.branch-deploy]      # non-production branches  command = "echo branch"[context.dev.environment]    # local dev env vars ONLY  NODE_ENV = "development"[context.staging]            # a specific branch name  command = "echo staging"[context."feat/branch"]      # quote branches with special chars  command = "echo special"
```

Precedence, least to most specific: UI settings, then the base context-aware key, then `[context.production|deploy-preview|branch-deploy|dev]`, then `[context.branchname]`. Only `[build]` and `[[plugins]]` are context-aware. All paths are absolute relative to the base directory.

Netlify looks for the config file in this order: package directory, base directory, root.

## Redirects and rewrites

One rule per line in `_redirects`, or a `[[redirects]]` block in `netlify.toml`. Rules process top-down and the **first match wins**. File-based rules run before `netlify.toml` rules.

```
/home            /                301/my-redirect     /                302/store id=:id    /blog/:id        301/news/*          /blog/:splat/*               /index.html      200          # SPA fallback
```

```
[[redirects]]  from = "/old-path"  to = "/new-path"  status = 302              # default 301  force = true             # default false; shadow an existing URL  query = { id = ":id" }  conditions = { Language = ["en"], Country = ["US"], Role = ["admin"] }  [redirects.headers]    X-From = "Netlify"
```

Things that catch people:

-   **You can’t shadow an existing URL by default.** Append `!` in `_redirects` or set `force = true`.
-   **Splats only work at the end of a path segment.** `/jobs/*.html` won’t match. And you can’t exclude a path from a splat; order a more specific rule above it.
-   **`query = { id = ":id" }` matches URLs with only `id`** and no other parameters. If you need optional-parameter variants, list the most general last.
-   **You cannot add or remove a trailing slash with a redirect.** URLs are normalized before rules run, so you’d get an infinite loop. Pretty URLs, on by default, handle standardization.
-   **Country and Language conditions take no spaces:** `Country=au,nz`. `Country` is ISO 3166-1 alpha-2. `Language` matches the **first** `Accept-Language` entry. The `nf_country` and `nf_lang` cookies override.
-   **Domain redirects need separate HTTP and HTTPS rules** unless you’re forcing SSL, and the domain has to be assigned to the site.
-   **Role-based redirects with external auth are Enterprise only.**
-   **At 10,000+ redirects**, favor wildcards and placeholders. Serialization across `_redirects` plus `netlify.toml` can get large enough to fail the deploy; consider Edge Functions instead.

### Rewrites and proxies

Status `200` proxies rather than redirects:

```
/api/*            https://api.example.com/:splat        200/netlify-site/*   https://my-other-site.netlify.app/:splat  200
```

```
[[redirects]]  from = "/search"  to = "https://api.mysearch.com"  status = 200  force = true  headers = { X-From = "Netlify" }
```

No cross-team rewrites between Netlify sites. Infinite-loop rules where `from` equals `to` are ignored. Internal rewrites are limited to one hop. The proxy timeout is 26 seconds, so go async for anything longer. And **rewrites break relative-path assets**, so use absolute paths or a `<base>` tag. To proxy to another Netlify site, use its `.netlify.app` subdomain; rewrites into a separate password-protected site aren’t allowed.

### Signed proxy redirects

`netlify.toml` only, and the one place `$VAR`\-style injection is allowed:

```
[[redirects]]  from = "/search"  to = "https://api.mysearch.com"  status = 200  force = true  signed = "API_SIGNATURE_TOKEN_PLACEHOLDER"
```

The variable’s scope must include **Runtime**. Not supported for Netlify-to-Netlify proxying. Netlify sends the JWS as HMAC HS256 in the `x-nf-sign` header.

## Custom headers

```
/*  X-Frame-Options: DENY  cache-control: max-age=0  cache-control: no-cache          # multi-value collapses comma-joined
```

```
[[headers]]  for = "/*"  [headers.values]    X-Frame-Options = "DENY"    Basic-Auth = "someuser:somepassword anotheruser:anotherpassword"    cache-control = '''    max-age=0,    no-cache,    no-store'''
```

**Headers apply only to files Netlify serves from its own store.** Proxied content, functions, and edge or SSR pages have to return their own headers. That’s the single most common reason a header appears to do nothing.

Netlify controls some header names and ignores yours if you set them: `Content-Length`, `Content-Encoding`, `Location` (use redirects), `Set-Cookie` (may be overridden), `Server`, `Date`, `Age`, `Connection`, `Transfer-Encoding`, and others.

Basic-Auth headers are Pro and Enterprise. Cross-subdomain cookies aren’t possible on `*.netlify.app` because it’s on the Public Suffix List; you need a custom domain.

## Environment variables

Set runtime and scoped variables through the CLI, UI, or API rather than `netlify.toml`:

```
netlify env:set MY_KEY value --secret     # --secret marks an env var secretnetlify env:import .env                    # site-level, all scopes, all contextsnetlify env:list --plain --context production > .envnetlify env:unset MY_KEY
```

Keep any `.env` snapshot gitignored. Never commit it.

**Types.** Site variables belong to one site. Shared variables cover the whole team, are Pro and Enterprise, and only Team Owners can set them.

**Scopes** (Pro and Enterprise; default is all): **Builds**, **Functions** (which also covers Edge Functions and On-demand Builders), **Runtime** (forms and signed proxy redirects), and **Post processing** (snippet injection). Variables from `netlify.toml` are locked to Builds plus Post processing.

**Scope precedence resolves independently per scope.** This one is subtle and worth stating carefully: a site variable scoped only to Builds does **not** shadow a shared variable for the Functions scope. The shared value still applies there. A site variable beats a shared variable only within the scopes it actually carries.

**Deploy-context values:** `Production`, `Deploy Previews`, `Branch deploys` (overridable per branch with a `Branch` value, wildcard suffix like `release/*`), `Preview server`, `Local development`.

**Limits:** keys up to 255 characters, alphanumeric plus underscore, first character a letter (`KEY1` is valid; `1KEY` and `_KEY1` are not). Values up to 5,000 characters, with functions still bound by AWS limits. Reserved read-only names can’t be overridden.

### Injecting values into headers and redirects

Because `$VAR` isn’t supported directly, substitute at build time:

```
[build]  command = "sed -i \"s|HEADER_PLACEHOLDER|${PROD_API_LOCATION}|g\" netlify.toml && yarn build"
```

This works because `[[headers]]` and `[[redirects]]` are read _after_ the build. It isn’t available to build plugins. The alternative is mutating `netlifyConfig` in a local build plugin.

## Secrets Controller

Mark a variable secret with `--secret` on the CLI, `is_secret: true` via the API, or in the UI. The policy is enforced and not customizable:

-   Values are **write-only**. There’s no readable version after setting, and removing the flag won’t reveal it.
-   They must be set to explicit deploy contexts and scopes, and **cannot** carry the Post processing scope.
-   Only code running on Netlify reads unmasked values. Outside code gets masked. The `dev` context value is unmasked and exempt.
-   Secret scanning runs on the next build after you mark a variable secret. Resolve a detection by removing the value at the location named in the deploy log, then redeploy. Safelist false positives with `SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES` (comma-separated), then redeploy.

**Sensitive variable policy applies to public repos only.** Untrusted deploys from unrecognized authors default to **Require approval**. The alternatives are **Deploy without sensitive variables** or **Deploy without restrictions**. Not available for GitHub Enterprise Server or GitLab self-managed, which are treated as private.

## Common failure modes

**Your function reads `undefined` for a variable you definitely set.** It’s in `netlify.toml`. Move it to `netlify env:set` or the UI, with a scope including Functions.

**A header you set is being ignored.** Either it’s on a function, proxied, or SSR response (those set their own headers) or it’s one of the reserved names Netlify controls.

**Your SPA 404s on refresh.** You need the fallback with status **200**, not a redirect:

```
[build]  command = "npm run build"  publish = "dist"        # varies by framework[[redirects]]  from = "/*"  to = "/index.html"  status = 200            # required for pushState routing to avoid 404s
```

**`Uncaught SyntaxError: Unexpected token` after a deploy.** Hashed and code-split filenames combined with atomic deploys can break asset references. Disable hashed filenames, use permalinks, or add a service worker.

**Your monorepo builds the wrong thing.** Set the site’s subdirectory as the **package directory** and keep `netlify.toml` there, leaving base at the repo root. Note that **package directory is UI-only** (Build settings > Configure) and can’t be set in `netlify.toml`. Base directory _can_ be set in a root-level `netlify.toml` and overrides the UI.

**Your ignore command isn’t stopping builds.** Check the exit codes, which are the opposite of what many people assume: exit `0` means no changes and the **build stops**; exit `1` means changed and the build continues.

```
[build]  ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/blog"
```

It runs from the base directory on a fixed **Node.js 18** that you can’t change, your site’s `package.json` dependencies aren’t available, and referenced file paths must start with `./`. It also won’t cancel a build triggered by a build hook, whatever it returns.

**You set local env vars under `[dev]` and nothing happened.** `[dev]` has no `environment` property. Use `[context.dev.environment]`.

## Reference

### Functions config

```
[functions]  directory = "functions/"           # default: YOUR_BASE_DIR/netlify/functions  node_bundler = "esbuild"           # recommended; zisi is the JS default but esbuild is required for TypeScript  external_node_modules = ["package-1"]  included_files = ["files/*.md", "!files/skip.md"]
[functions."api_*"]                  # glob filter; values CONCATENATE across matches  external_node_modules = ["package-2"]
```

`esbuild` produces smaller, faster artifacts, and TypeScript functions always use it. `external_node_modules` only applies with `esbuild`. In `included_files`, `*` is a wildcard and `!` excludes; paths are absolute to base.

### Build variables settable in `[build.environment]`

`NODE_VERSION`, `NODE_ENV`, `NPM_VERSION`, `NPM_FLAGS`, `NPM_TOKEN`, `YARN_VERSION`, `PNPM_FLAGS`, `BUN_VERSION`, `RUBY_VERSION`, `PHP_VERSION`, `PYTHON_VERSION`, `GO_VERSION`, `HUGO_VERSION`, `NETLIFY_USE_YARN`, `CI`.

**Must be set in the UI or CLI, not `netlify.toml`** (which is read after clone): `AWS_LAMBDA_JS_RUNTIME`, `GIT_LFS_ENABLED`, `GIT_LFS_FETCH_INCLUDE`, `NETLIFY_BUILD_DEBUG`.

### Read-only build metadata

`NETLIFY`, `BUILD_ID`, `CONTEXT` (`production` / `deploy-preview` / `branch-deploy` / `dev`), `BRANCH`, `HEAD`, `COMMIT_REF`, `CACHED_COMMIT_REF`, `PULL_REQUEST`, `REVIEW_ID`, `URL`, `DEPLOY_URL`, `DEPLOY_PRIME_URL`, `DEPLOY_ID`, `SITE_NAME`, `SITE_ID`, `ACCOUNT_ID`.

Access them as `$VAR_NAME` in build and ignore commands, or `process.env.VAR_NAME` in Node scripts and plugins. The scope must include Builds.

### Env var scopes

Scope

Covers

Builds

Build commands, plugins, ignore commands

Functions

Functions, Edge Functions, On-demand Builders

Runtime

Forms, signed proxy redirects

Post processing

Snippet injection

Variables declared in `netlify.toml` get Builds and Post processing only.

### Plugins, extensions, and dev

```
[[plugins]]  package = "@netlify/plugin-lighthouse"  [plugins.inputs]    breeds = ["pomeranian"]
[[integrations]]           # extensions; install on team first  name = "abc-performance-extension"  [integrations.config]    output_path = "reports/perf.html"
[dev]                      # Netlify Dev — NOT run in Bash; no `environment` key here  command = "yarn start"  targetPort = 3000        # if command + targetPort both set, framework must be "#custom"  port = 8888  publish = "dist"  [dev.https]    certFile = "cert.pem"    keyFile = "key.pem"
```

`framework` accepts `#auto` (default), `#static`, or `#custom`. For Deploy-to-Netlify buttons, use `[template]` and `[template.environment]`.

Pretty URLs:

```
[build.processing.html]  pretty_urls = true
```

## FAQs

**How do I set up a redirect on Netlify?** Add a `[[redirects]]` block to `netlify.toml` with `from`, `to`, and `status`, or a line in a `_redirects` file in your publish directory. Rules match top-down and the first match wins; `_redirects` is processed before `netlify.toml`.

**Why is my environment variable undefined in a Netlify Function?** Because you declared it in `netlify.toml`. Those variables only get the Builds and Post processing scopes. Set it with `netlify env:set` or in the UI, with a scope that includes Functions, then redeploy.

**How do I add a SPA fallback so client-side routes don’t 404?** Redirect `/*` to `/index.html` with status **200**. A 301 or 302 will break pushState routing.

**Why isn’t my custom header being applied?** Headers in `netlify.toml` and `_headers` only apply to files Netlify serves from its own store. Functions, proxied content, and SSR pages must set their own headers. Also check whether it’s a reserved name Netlify controls.

**Can I scope redirects or headers to a specific branch?** Not directly. `[[redirects]]` and `[[headers]]` are global. The workaround is a per-context build command that copies a different rules file into the publish directory.

**Does `netlify.toml` override the Netlify UI settings?** Yes, on conflict. `_headers` and `_redirects` are processed before `netlify.toml` rules.

**Is it safe to put an API key in `VITE_API_KEY`?** No. Client-prefixed variables are inlined into your browser bundle. Marking them `--secret` does not change that. Keep secrets server-side and read them in a function.

**Why did my build not stop when my ignore command returned 0?** It should have. Exit `0` means no changes and stops the build; exit `1` continues. If it was triggered by a build hook, the ignore command won’t cancel it regardless.

**Where does Netlify look for netlify.toml?** Package directory, then base directory, then repo root.

## Related

-   [File-based configuration](https://docs.netlify.com/build/configure-builds/file-based-configuration/)
-   [Build configuration](https://docs.netlify.com/configure-builds/)
-   [Rate limiting](https://docs.netlify.com/manage/security/secure-access-to-sites/rate-limiting/)

This article is generated from Netlify’s open-source agent guidance at [netlify/context-and-tools](https://github.com/netlify/context-and-tools), the same reference our AI coding agents use.

* * *

Ready to configure something? Start at [netlify.new](https://netlify.new).