Cloud Tech

The Tiny Tool That Would Have Stopped a Production Deploy Gone Wrong

Problem this article addresses

Use direnv to load project-specific environment variables on entry, restore prior shell state on exit, and reduce wrong-account Terraform deploys.

Published Jul 26, 2026Victor NwokeReviewed Jul 26, 20268 min read

Technical claims are reviewed against the cited primary sources. Hands-on guides include execution or diagnostic evidence when the article makes a tested-result claim.

A colleague once ran terraform apply against production instead of staging.

The configuration was valid. The workspace name was not mistyped. The shell was simply carrying AWS_PROFILE=prod from work done hours earlier, and nothing in the new project made that stale state obvious.

That is the dangerous part of exported environment variables: they belong to a process, not a project directory. In a long-lived shell, changing directories does not reset them. Commands launched from that shell inherit the exported values until you overwrite or unset them.

This kind of mistake is not a Terraform knowledge problem. It is a context problem.

direnv is a small shell extension that addresses that exact failure mode. It loads an authorized environment when you enter a directory and reverses those changes when you leave. A staging repository can declare AWS_PROFILE=staging; a production repository can declare AWS_PROFILE=prod. The directory becomes part of the environment-selection mechanism.

If you want the shell model underneath this first, read Bash Fundamentals. If the command in question is Terraform, pair this guide with Terraform Basics.

The Problem Is Shell State, Not the Command

Suppose a shell already contains:

bash
export AWS_PROFILE=prod

AWS documents AWS_PROFILE as the environment variable that selects a named profile for AWS CLI commands. Terraform's S3 backend can also source its profile from AWS_PROFILE, and the AWS provider supports the standard AWS authentication sources.

Now you move into a staging repository:

bash
cd ~/work/staging-infrastructure
terraform plan
terraform apply

Nothing about cd changes AWS_PROFILE. Unless the repository, wrapper script, prompt, or operator checks the active identity, the shell continues using the exported value.

Terminal inside staging-infrastructure showing the stale AWS_PROFILE value prod before direnv is configured

A safe local reproduction of the failure mode: the shell is inside staging-infrastructure, but its inherited profile is still prod.

The same pattern applies whenever a tool reads identity, region, project, or runtime selection from the environment. The variable names differ. The failure mode is the same: process state outlives the task that created it.

Terminal tabs are separate shell processes

An export in one existing terminal tab does not normally rewrite the environment of another existing tab. The stale-state risk appears when the same long-lived shell moves between projects, or when a new shell inherits configuration from its parent or startup files.

What direnv Actually Does

The official direnv documentation describes this sequence:

  1. Before each prompt, direnv looks for an .envrc file in the current directory and its parent directories.
  2. If the file exists and has been authorized, direnv evaluates it in a Bash subprocess.
  3. It captures the exported environment difference and applies that difference to the current shell.
  4. When you leave the directory, direnv reverses the project-specific difference.

That subprocess detail matters. An .envrc can export variables and use direnv's standard-library helpers, but shell aliases and functions are not exported back into the interactive shell.

It also explains why direnv works across shells. Zsh, Bash, Fish, and other supported shells use different hook syntax, while .envrc is evaluated as Bash code and direnv returns an environment diff the host shell can apply.

The Five-Minute Setup

Installation has two parts: install the direnv binary, then add its hook to your shell. The official installation guide lists supported system packages and binary releases.

Confirm the binary is available:

bash
direnv version

Then I I add the documented hook for your shell.

Zsh

I add this to the end of ~/.zshrc:

bash
eval "$(direnv hook zsh)"

Bash

I add this to the end of ~/.bashrc:

bash
eval "$(direnv hook bash)"

The direnv setup guide specifically says the Bash hook should appear after prompt extensions such as git-prompt or RVM.

Restart the shell after adding the hook. For a new project, create .envrc:

bash
export AWS_PROFILE=staging

On the next prompt, direnv blocks the file because it has not been trusted yet. I review it, then authorize it:

bash
direnv allow .

The authorization step is intentional. An .envrc is executable shell code, so automatically executing every .envrc from every repository would be unsafe. If the file changes, direnv blocks the new version until it is allowed again.

Now verify the value:

bash
echo "$AWS_PROFILE"
aws sts get-caller-identity

Terminal showing direnv loading the project .envrc and changing AWS_PROFILE from prod to staging

The real direnv hook loads the authorized .envrc on entry and replaces the stale profile with the project-scoped value.

AWS documents aws sts get-caller-identity as returning the user or role, account ID, and ARN for the credentials currently in use. The profile name is helpful; the resolved account identity is the stronger check.

Unloading Means Restoring the Previous Environment

This is the nuance most short explanations miss.

direnv unloads the changes introduced by .envrc. It does not promise that every variable becomes empty when you leave. If a value existed before you entered the directory, direnv restores that previous value.

For example:

bash
export AWS_PROFILE=prod

cd ~/work/staging-infrastructure
echo "$AWS_PROFILE"
# staging

cd ..
echo "$AWS_PROFILE"
# prod

Terminal showing direnv unloading after leaving the project and restoring AWS_PROFILE to prod

Leaving the directory unloads the project environment and restores the shell's previous AWS_PROFILE value.

That behavior is correct: direnv records and reverses an environment diff.

It still prevents the original staging mistake while you are inside the staging repository, because the repository declares the expected profile. But the old production value returns outside that directory. I do not describe direnv as a tool that permanently cleans the entire shell.

A Safer AWS .envrc

Setting AWS_PROFILE is useful, but AWS configuration has precedence rules. AWS documents that environment variables override values loaded from a profile. Stale direct credential variables can therefore defeat the assumption that changing the profile changes the active credentials.

If a project is designed to authenticate through a named profile, make that expectation explicit:

bash
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN

export AWS_PROFILE=staging
export AWS_REGION=eu-west-2

This removes direct AWS credential variables inside the project environment and selects the named staging profile. When you leave, direnv restores the values that existed before entry.

Then I I verify the resolved identity before a mutating command:

bash
aws sts get-caller-identity \
  --query '{Account: Account, Arn: Arn}' \
  --output table

For production, keep an independent approval or account check in the deployment workflow. direnv reduces accidental context drift; it does not inspect a Terraform plan, validate the target account, or replace least-privilege credentials.

What Should Go in Git

An AWS profile name, region, project mode, or local tool path is configuration, not a credential. If every contributor should use the same non-secret values, committing .envrc gives the team one reviewable source of truth:

bash
export AWS_PROFILE=staging
export AWS_REGION=eu-west-2

I do not commit access keys, secret keys, session tokens, passwords, or API tokens in .envrc. A file being loaded through direnv does not make its contents secret. For the broader security model, see Secrets Management.

If each developer needs different local values, use a documented team convention:

text
.envrc           # ignored, local values
.envrc.example   # committed, variable names and safe placeholders

Example .envrc.example:

bash
export AWS_PROFILE=replace-with-your-staging-profile
export AWS_REGION=eu-west-2

The example file is a team workflow, not a file direnv loads automatically. Each developer creates the real .envrc, reviews it, keeps it out of version control, and runs direnv allow ..

Review sourced files too

direnv's standard-library documentation warns that files loaded through helpers such as source_env and source_env_if_exists are not independently checked by the security framework. Treat every sourced file as executable code and review it before authorizing the parent .envrc.

Where direnv Stops Helping

direnv is a narrow tool. That is a strength, but it creates clear boundaries:

  • It cannot prevent a deploy launched from the wrong directory.
  • It cannot prove that a local profile name maps to the same account on every machine.
  • It cannot stop a user from passing --profile prod explicitly on an AWS CLI command.
  • It cannot override a credential or provider setting explicitly configured at a higher precedence without you accounting for it.
  • It cannot review the Terraform plan or add a human approval gate.
  • It cannot protect production if staging credentials already have production permissions.

I use it as one layer in a safer deployment path:

  1. Scope non-secret project configuration with .envrc.
  2. I use short-lived, least-privilege cloud credentials.
  3. I verify the resolved account or subscription before mutation.
  4. I review a saved Terraform plan before applying it.
  5. Require CI approval for production changes.

HashiCorp's terraform apply documentation says that applying without a saved plan creates a new plan and asks for approval before executing it. That prompt is useful, but an approval is only as good as the context the operator reviews.

A Practical Team Checklist

I use this when adopting direnv:

  • Install direnv from an official package or release.
  • I add the official hook for the shell you actually use.
  • Restart the shell and confirm direnv version.
  • Put only non-secret, project-specific values in a shared .envrc.
  • I use .envrc.example when values must remain developer-specific.
  • I review every .envrc as executable code before running direnv allow.
  • Re-check the cloud identity with aws sts get-caller-identity.
  • I keep production approval and least-privilege controls outside direnv.

The setup is small because the problem is small: a shell does not know which project its environment variables belong to.

direnv gives it that missing directory context.

Set it up before your next context switch between projects, not after the wrong account appears in an apply log.

Official Documentation

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement