Stockio

direnv and .env Files: Managing Local Environment Variables

Make local project configuration predictable with .env templates, explicit loading rules, direnv approval, and checks that catch missing values before startup.

By Ravi Desai

DevOps & Security Engineer ·

On this page

Switching between projects should not leave yesterday's API address in today's terminal. Local environment configuration becomes manageable when you separate stored values, loading behavior, and validation instead of treating a .env file as something every tool automatically understands.

Distinguish a file from the process environment

A .env file is a text convention for storing key-value pairs. The process environment is the set of string values inherited by a running program. A file sitting in the repository changes nothing until a shell tool, runtime, framework, or library reads it.

When troubleshooting configuration, build a sanitized JSON object containing only safe diagnostic fields, such as the selected mode and whether required values exist. Inspect that object with the JSON formatter to spot inconsistent names or unexpected shapes. Do not format a full environment dump: it commonly contains credentials and unrelated machine information.

Write down which component loads each file. Perhaps direnv loads values into your terminal, while a test runner uses its own loader. The same filename does not guarantee identical parsing or precedence. Keep this article's focus on local workflow; production secret delivery requires a separate operational policy.

The Node.js environment-variable documentation describes Node's supported format and native loading features. Other ecosystems may differ, especially around expansion, multiline values, and comments.

Create a useful template without committing secrets

Commit a .env.example containing documented names and safe local defaults. Exclude actual local values before creating the real file. A narrowly scoped ignore policy is easier to understand than ignoring every file whose name contains the word environment.

APP_PORT=3000
API_BASE_URL=http://127.0.0.1:4000
ENABLE_REQUEST_LOGS=false
LOCAL_ACCESS_TOKEN=replace-me
.env
.env.local
.env.*.local
.direnv/

Copy the example to .env, then replace placeholders with appropriate local values. These ignore rules intentionally do not cover every possible naming scheme: add exact patterns for any additional files the project actually uses. Confirm exclusion with git check-ignore -v .env before entering a real credential.

An ignore rule does not stop tracking a file already committed. Check the index, remove accidental tracking appropriately, and rotate exposed credentials. Deleting a token from the newest commit does not invalidate copies in history or existing clones.

Keep the example synchronized with the application's requirements. Describe whether each value is required, its expected form, and whether a default exists. A placeholder that looks like a valid production endpoint is less helpful than a visibly local example that fails safely.

Choose one loading path and document precedence

For a supported modern Node.js version, a direct command can load a file without an extra package:

node --env-file=.env src/server.js

Verify support in the runtime version used by the repository. With Node's native option, an existing process environment value takes precedence over the same name in the file. If a parent terminal already exports APP_PORT, editing .env may therefore appear ineffective.

Frameworks often define additional filenames and environment-specific rules. Do not assume .env.local, test-mode files, or interpolation work identically in all tools. Read the specific loader's documentation and record the agreed command in package scripts or contributor instructions.

Avoid loading the same file through multiple systems without a reason. A framework loader combined with direnv and an application-level dotenv call can create a difficult precedence puzzle. Select a single primary loader for each execution path, then test what happens when a variable is already present.

Environment variables are strings. The text false is still truthy in JavaScript, and an empty string differs from a missing value. Treat parsing as application code rather than relying on shell conventions to produce correctly typed settings.

Use direnv for directory-scoped terminal values

Direnv integrates with the shell and applies environment changes when you enter a trusted directory. Install it through your normal package manager and add the hook appropriate to your shell, following the official hook instructions. Open a new shell or reload the relevant startup configuration afterward.

A minimal project .envrc can load the local file:

dotenv .env

Review the file, then run direnv allow inside the project. Direnv requires approval because .envrc executes code; it is not merely a list of inert settings. A repository can put arbitrary shell commands there. Review changes before approving again, including any files or helper scripts that the configuration invokes.

The direnv standard library documents dotenv and other helpers. Do not substitute source .env casually: sourcing treats the file as shell code and may interpret content that a dotenv parser would handle differently.

Leave the directory and verify a harmless project-specific variable returns to its previous state in the current shell. Existing child processes do not receive retroactive updates. A server launched before changing a value must usually be restarted, and an editor launched elsewhere may not inherit your terminal's environment.

Validate values and debug without revealing them

Fail at startup when required configuration is missing or malformed. For a small Node program, explicit parsing is enough to establish the principle:

const rawPort = process.env.APP_PORT ?? "3000";
const port = Number(rawPort);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("APP_PORT must be an integer from 1 to 65535");
}

const rawLogs = process.env.ENABLE_REQUEST_LOGS ?? "false";
if (!["true", "false"].includes(rawLogs)) {
throw new Error("ENABLE_REQUEST_LOGS must be true or false");
}
const requestLogsEnabled = rawLogs === "true";

Use URL parsing and explicit protocol restrictions where appropriate, and reject placeholder secrets for operations that actually require authentication. Startup validation turns an obscure downstream failure into an actionable local error.

Debug presence and source, not credential contents. Report “token configured” rather than printing the token, and avoid logging the complete process environment. Check for an inherited value, a different working directory, an unapproved .envrc, or a long-running process before assuming the file parser is broken.

Remember that frontend build tools may deliberately expose specially prefixed variables to browser bundles. Those values are public configuration, not secrets. Local storage in an ignored file does not make a value private once bundled into client code.

Document a smoke test: start from a clean terminal, enter the directory, run the normal command, change a harmless value, restart, and leave the directory. Put the instructions alongside development documentation and link related terminal guidance from the dev-tools category. Predictability matters more than clever configuration shortcuts.

Key takeaways

  • A .env file needs an explicit loader; it does not configure processes by itself.
  • Commit examples and ignore real local values before entering credentials.
  • Document precedence and avoid competing loaders for the same execution path.
  • Review .envrc as executable code before approving it.
  • Validate strings at startup and troubleshoot without dumping secrets.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in Dev Tools & AI Coding
Dev Tools & AI Coding

8 Best AI Code Review Tools

Compare eight AI-assisted code review tools for pull requests, security checks, and repository workflows, with practical cautions about false alarms, context, and human responsibility.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

15 Best VS Code Extensions for Web Development

Find useful VS Code extensions for formatting, debugging, writing and remote work. This practical list explains what each add-on does, who benefits, and when built-in features are enough.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Turborepo Monorepo With pnpm Workspaces

Set up pnpm workspace boundaries and Turborepo task caching with explicit package dependencies, reliable outputs, and a structure that stays understandable as teams grow.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Chrome DevTools Performance Panel: From Trace to Fix

Record focused Chrome Performance traces, identify expensive main-thread work, and verify targeted fixes with repeatable scenarios instead of guessing from a flame chart.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

AI Tailwind CSS: Get Clean Styles From AI Assistants

AI assistants can produce a convincing interface quickly, but the stylesheet often reveals how little was specified. Arbitrary spacing, duplicated utilities and brittle positioning accumulate when every prompt.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

AI Pair Programming for Junior Developers: Learn First

Use AI as a coding tutor with hint ladders, behaviour predictions, regression tests, and independent teach-back checks that build junior developers’ skills instead of hiding gaps.

Includes: JSON formatterSofia MarchettiSofia Marchetti