How to Fix n8n Code Node Not Reading Environment Variables ($env Undefined)

Introduction

You upgraded n8n, or you hardened your Docker setup, and a workflow that worked last week quietly broke: the Code node that used to read $env.MY_API_KEY now returns undefined, and an expression like ={{ $env.MY_API_KEY }} shows “access denied”. Nothing in the workflow changed, so nothing looks wrong, and that is exactly what makes this failure so frustrating. This tutorial is for self-hosted n8n users on 1.x and 2.x who need environment variables back in their Code nodes and expressions. You will learn how to reproduce the problem in sixty seconds, which of the three security settings is blocking you, and how to fix each one, including the safer allowlist alternative for shared instances.

Quick answer

If your n8n Code node returns undefined for $env.MY_VAR, set N8N_BLOCK_ENV_ACCESS_IN_NODE=false for JavaScript and expressions, or N8N_BLOCK_RUNNER_ENV_ACCESS=false for Python, then restart n8n. Always confirm with a real execution, since the editor preview can still show access denied.

Evidence

Environment variable access in n8n is locked down by design, not by accident. A self-hosting hardening checklist published in October 2026 reported that Censys had counted 114,000 publicly exposed n8n instances, which is the backdrop for why workflow code is sandboxed away from process secrets like N8N_ENCRYPTION_KEY and database credentials. The same checklist describes N8N_BLOCK_ENV_ACCESS_IN_NODE=true as the setting that “stops Code and Execute nodes from reading your process environment variables”, and adds that it is “enabled by default in recent n8n versions; set it explicitly so an upgrade never silently relaxes it”.

The official documentation is more conservative. The security variables reference describes N8N_BLOCK_ENV_ACCESS_IN_NODE as: “Whether to allow users to access environment variables in expressions and the Code node (false) or not (true).” That means the blocking behavior you see can come from the variable being set to true in your deployment, from a recent version changing effective defaults, or from one of the two sibling settings below. Do not assume; diagnose, using the steps in this guide.

For Python, the task runner variables reference is unambiguous: N8N_BLOCK_RUNNER_ENV_ACCESS defaults to true, and “For security reasons, environment variable access is blocked by default” for Python code tasks reading the runner’s environment via os.environ. JavaScript Code nodes and $env expressions ignore this variable entirely; they answer only to N8N_BLOCK_ENV_ACCESS_IN_NODE. That split is the single most common reason a fix “works in JavaScript but not in Python”.

Prerequisites

  • A self-hosted n8n instance (Docker or Docker Compose), with shell access to the host. These settings cannot be changed on n8n Cloud.
  • n8n 1.x or 2.x. The behavior described here applies to both, with 2.x being stricter out of the box.
  • Basic comfort editing docker-compose.yml and recreating containers.
  • Ten minutes and one test workflow you are willing to run in test mode.

Step-by-step guide

Step 1: Reproduce the failure in sixty seconds

Create a throwaway workflow with a single Code node in JavaScript mode:

return [{
  apiKey: $env.MY_API_KEY,
  nodeVersion: process.version
}];

Execute it. If apiKey comes back undefined while nodeVersion renders fine, the sandbox is fine but environment access is blocked. Also try the same variable in any expression field, for example ={{ $env.MY_API_KEY }} on an Edit Fields node: “access denied” in the preview confirms the block rather than a typo in the variable name. Note the gotcha documented below before trusting the preview.

Step 2: Find out which setting is blocking you

Run this against your n8n container:

docker exec <your-n8n-container> env | grep -Ei "N8N_BLOCK|N8N_RUNNERS|EXPRESSIONS_ALLOWED" | sort

Interpret the output:

  • N8N_BLOCK_ENV_ACCESS_IN_NODE=true present: JavaScript Code nodes and all $env expressions are blocked. Fix in Step 3.
  • Nothing matching at all: the variable may be defaulting in your build, or a security policy managed from the UI is in play. n8n supports managing the security policy from environment variables (N8N_SECURITY_POLICY_MANAGED_BY_ENV=true), which locks the matching UI controls, so check Settings on the instance too.
  • Python Code node affected: N8N_BLOCK_RUNNER_ENV_ACCESS defaults to true when unset. Fix in Step 4.
  • External task runner mode: Code runs in a separate runner process or container, so the variable must also exist in the runner’s environment, not just the n8n container. Fix in Step 5.

This diagnostic takes thirty seconds and prevents the most common wasted afternoon: toggling the JavaScript setting when the failing node is Python, or fixing the n8n container while an external runner stays untouched. If you are new to Code nodes in general, our n8n Code node JavaScript tutorial covers the sandbox basics this guide builds on.

Step 3: Restore access for JavaScript and expressions

Add the variable to your docker-compose.yml under the n8n service and recreate the container:

services:
  n8n:
    image: n8nio/n8n:2.8.3
    environment:
      - N8N_BLOCK_ENV_ACCESS_IN_NODE=false
docker compose down
docker compose up -d

Recreating (not just restarting) matters, because docker compose restart does not always pick up changed environment values; community troubleshooting threads confirm that a full down and up -d is the reliable path. Then re-run the reproduction workflow from Step 1 in test mode. The Code node should now return your variable’s value.

A note on safety, stated plainly: with the block off, any workflow on the instance can read every process environment variable, including N8N_ENCRYPTION_KEY and database credentials. On a single-operator box that is a reasonable tradeoff. On a shared or team instance, skip to Step 6 and keep the block on.

Step 4: Restore access for Python Code nodes

Python Code nodes execute inside the task runner, and they obey a different, stricter default. Set the runner variable on the n8n service (internal runner mode) so the runner processes inherit it:

services:
  n8n:
    image: n8nio/n8n:2.8.3
    environment:
      - N8N_BLOCK_RUNNER_ENV_ACCESS=false

Then test with a Python Code node:

import os
return [{"api_key": os.environ.get("MY_API_KEY")}]

os.environ.get returning None after this change usually means the variable is not in the runner’s environment at all (Step 5), not that the block is still on. Do not set N8N_RUNNERS_INSECURE_MODE=true as a shortcut for this problem; it disables all runner security measures and the docs explicitly discourage it for production.

Step 5: Handle the external task runner difference

If you run task runners in external mode, as described in our external task runners Docker setup guide, the Code node executes in a separate runner container with its own environment. Variables defined only on the n8n service never arrive there. Give the runner the variables too, either as container environment on the runner service or through the runner’s configuration, then restart the runner. After any runner change, re-run the Python reproduction from Step 4. If it still fails, compare docker exec output on both containers: the variable must literally exist in the runner’s env for os.environ to see it.

Step 6: The safer route, allowlist or credential overwrites

If your instance is shared, keep N8N_BLOCK_ENV_ACCESS_IN_NODE=true and expose only what workflows need. Community members report that N8N_EXPRESSIONS_ALLOWED_ENV_VARS accepts a comma-separated allowlist, for example:

environment:
  - N8N_EXPRESSIONS_ALLOWED_ENV_VARS=BROWSERLESS_TOKEN,STRIPE_KEY

This is community-reported rather than officially documented, so verify on your version with the Step 1 reproduction before relying on it. For secrets specifically, the documented route is credential overwrites: n8n injects credentials server-side from a file named by CREDENTIALS_OVERWRITE_DATA_FILE, so workflows get working credentials while the environment block stays fully on. That pattern is covered in depth in the Infisical guide to managing secrets and environment variables in n8n, and it is the right answer when several people share one instance.

Troubleshooting

Problem 1: The editor preview still shows “access denied” after the fix

Cause: The editor preview evaluates expressions without the execution context, and n8n users on 2.x report that the preview keeps showing “access denied” even when the setting is correct.

Fix: Ignore the preview and run the workflow in test mode. As one n8n support response on the community forum puts it: the values only resolve during actual execution, so do not let the preview throw you off. If the real execution returns the value, your fix worked.

Problem 2: The variable is set on the host but invisible inside n8n

Cause: A variable exported in your shell (export MY_API_KEY=...) never enters the container unless the compose file passes it. Containers only see what the environment: block or an env_file: provides.

Fix: Put the variable in docker-compose.yml or a referenced .env file:

# .env next to docker-compose.yml
MY_API_KEY=sk-live-xxxx
environment:
  - MY_API_KEY=${MY_API_KEY}
  - N8N_BLOCK_ENV_ACCESS_IN_NODE=false

Then docker compose down && docker compose up -d, and confirm with docker exec <container> env | grep MY_API_KEY.

Problem 3: The fix works in a JavaScript Code node but not in Python

Cause: The two languages are governed by two different settings. N8N_BLOCK_ENV_ACCESS_IN_NODE covers JavaScript and $env expressions only; Python is covered by N8N_BLOCK_RUNNER_ENV_ACCESS, which defaults to true.

Fix: Set N8N_BLOCK_RUNNER_ENV_ACCESS=false as in Step 4, recreate the container, and test with the os.environ.get snippet. If both languages fail, you likely have both blocks on; fix them independently.

Problem 4: Everything works, then breaks again after an n8n upgrade

Cause: Major versions change security defaults. v2.0, for example, disabled the Execute Command node by default through NODES_EXCLUDE, and hardening checklists warn that upgrades can silently change the effective value of environment-access settings unless you pin them.

Fix: Declare both variables explicitly in your compose file even when you want the permissive values, so the intent survives upgrades:

environment:
  - N8N_BLOCK_ENV_ACCESS_IN_NODE=false
  - N8N_BLOCK_RUNNER_ENV_ACCESS=false

After each upgrade, re-run the Step 1 reproduction before touching anything else; it takes a minute and catches silent regressions immediately. For a broader tour of what 2.x changed under the hood, the complete n8n guide keeps a running evidence section with version-specific notes.

FAQ

Why did my Code node stop reading environment variables after upgrading n8n?

Recent n8n versions tightened code-execution security. N8N_BLOCK_ENV_ACCESS_IN_NODE disables $env in expressions and JavaScript Code nodes when set to true, and v2.0 also changed related defaults such as disabling the Execute Command node via NODES_EXCLUDE. Hardening guides recommend setting the block explicitly so upgrades never silently relax it, which means many installs now behave as if blocked. Check the variable in your container, apply the fix above, and test with a real execution.

Is it safe to set N8N_BLOCK_ENV_ACCESS_IN_NODE=false?

Only if you trust every user who can create or edit workflows on the instance. With the block off, any workflow can read process environment variables, including N8N_ENCRYPTION_KEY and database credentials. On a single-operator instance that tradeoff is usually fine. On a shared instance, prefer the allowlist approach or credential overwrites so the block stays on.

How do I read environment variables in a Python Code node?

Python Code nodes run in the task runner, not the main process, and a separate setting governs them: N8N_BLOCK_RUNNER_ENV_ACCESS defaults to true and blocks os.environ access from Python code. Set it to false and restart. JavaScript Code nodes and expressions are controlled by N8N_BLOCK_ENV_ACCESS_IN_NODE instead, so check both if you use both languages.

Why does the editor still show “access denied” after I configured everything correctly?

This is a known preview quirk reported by n8n users on 2.x: the editor preview can keep showing “access denied” even when the setting is correct. The values resolve during an actual execution, so run the workflow in test mode and inspect the real output before concluding the fix failed.

Can I allow just one variable instead of opening all of them?

Yes, if you want the block to stay on for everything else. Community members report success with N8N_EXPRESSIONS_ALLOWED_ENV_VARS set to a comma-separated list such as BROWSERLESS_TOKEN, though this is not documented on the official variables page. For secrets specifically, n8n credential overwrites injected server-side via CREDENTIALS_OVERWRITE_DATA_FILE are the documented route that keeps the block fully on.

I use n8n Cloud. Can I change these settings?

No. n8n Cloud does not expose server environment settings, so neither the block nor the allowlist can be changed there. Use n8n’s built-in credentials or, on Enterprise and Pro Cloud plans, custom variables. The fixes in this tutorial apply to self-hosted instances where you control the container environment.

Conclusion

Environment variables disappearing from n8n Code nodes is almost never a bug in your workflow. It is one of three settings doing its job: N8N_BLOCK_ENV_ACCESS_IN_NODE for JavaScript and expressions, N8N_BLOCK_RUNNER_ENV_ACCESS for Python, or an external task runner whose environment never received the variable. Diagnose with docker exec env, fix the setting that matches your language and runner mode, and confirm with a real test execution rather than the editor preview. On shared instances, keep the blocks on and use allowlists or credential overwrites instead.

Next, if your Code nodes run slowly or time out rather than failing on variables, read our external task runners Docker setup guide to move execution off the main process properly.

Frequently asked questions

Why did my Code node stop reading environment variables after upgrading n8n?

Recent n8n versions tightened code-execution security. N8N_BLOCK_ENV_ACCESS_IN_NODE disables $env in expressions and JavaScript Code nodes when set to true, and v2.0 also changed related defaults such as disabling the Execute Command node via NODES_EXCLUDE. Hardening guides recommend setting the block explicitly so upgrades never silently relax it, which means many installs now behave as if blocked. Check the variable in your container, apply the fix below, and test with a real execution.

Is it safe to set N8N_BLOCK_ENV_ACCESS_IN_NODE=false?

Only if you trust every user who can create or edit workflows on the instance. With the block off, any workflow can read process environment variables, including N8N_ENCRYPTION_KEY and database credentials. On a single-operator instance that tradeoff is usually fine. On a shared instance, prefer the allowlist approach or credential overwrites so the block stays on.

How do I read environment variables in a Python Code node?

Python Code nodes run in the task runner, not the main process, and a separate setting governs them: N8N_BLOCK_RUNNER_ENV_ACCESS defaults to true and blocks os.environ access from Python code. Set it to false and restart. JavaScript Code nodes and expressions are controlled by N8N_BLOCK_ENV_ACCESS_IN_NODE instead, so check both if you use both languages.

Why does the editor still show 'access denied' after I configured everything correctly?

This is a known preview quirk reported by n8n users on 2.x: the editor preview can keep showing 'access denied' even when the setting is correct. The values resolve during an actual execution, so run the workflow in test mode and inspect the real output before concluding the fix failed.

Can I allow just one variable instead of opening all of them?

Yes, if you want the block to stay on for everything else. Community members report success with N8N_EXPRESSIONS_ALLOWED_ENV_VARS set to a comma-separated list such as BROWSERLESS_TOKEN, though this is not documented on the official variables page. For secrets specifically, n8n credential overwrites injected server-side via CREDENTIALS_OVERWRITE_DATA_FILE are the documented route that keeps the block fully on.

I use n8n Cloud. Can I change these settings?

No. n8n Cloud does not expose server environment settings, so neither the block nor the allowlist can be changed there. Use n8n's built-in credentials or, on Enterprise and Pro Cloud plans, custom variables. The fixes in this tutorial apply to self-hosted instances where you control the container environment.

TP
ToolPilot Team

We install, break, and fix automation tools so you don't have to. Every tutorial on ToolPilot is tested hands-on before publishing.

Keep reading