Skip to content

Environment Variables

An environment variable is a named string the operating system hands to a program at the moment it starts. The program gets a copy. Anything that program launches inherits a copy of that copy.

That is the whole model, and one half of it causes nearly all the confusion:

A running program cannot see changes you make after it started.

Not “usually doesn’t.” Cannot. Setting a variable does not reach backwards into anything already running — not your editor, not your dev server, not the terminal you opened ten minutes ago.

Two things commonly called environment variables that are not. A shell variable (GREETING=hi in Bash) is visible to your shell and not to programs it launches; export is what promotes it. A .env file is not known to the operating system at all — a library inside your app reads it, which is why things outside your app cannot see those values.

A value that is different on your machine than on someone else’s: a database URL, a port, an endpoint that is localhost for you and something else in production.

A secret you do not want in the repository — an API key, a token.

A switch you want to flip without editing code: a log level, a feature toggle.

The common thread is that the code is the same everywhere and the value is not.

One question: would putting this value in git be wrong, or meaningless?

Wrong means it is a secret. Meaningless means it is only true on one machine. Either answer points to an environment variable.

If the value is the same for everybody and stays that way, it is a constant — put it in the code where people can find it.

Then one follow-up, because it decides where you set it: does this need to survive closing the terminal? If no, set it for the session. If yes, set it in your user profile — and expect to open a new terminal before you can see it.

You set it and the running thing never sees it. The dev server was started before you set the variable, so it holds the older environment. It presents as the variable being “not set” in code while your terminal plainly shows it. Restart the process. If you set it permanently, restart the terminal and then the process — and note that a terminal inside your editor inherited from the editor, which inherited from whenever you opened it.

You set it permanently and check it in the same window. You will see the old value, conclude it did not work, and set it again. This is the most common way to lose an afternoon to this topic.

It works in one shell and not the other. PowerShell and Git Bash are separate processes. A session variable set in one was never handed to the other.

Case. Windows treats Path and PATH as the same name. Bash does not, and neither does anything Unix-shaped. Code that reads API_KEY will not find Api_Key the first time it runs in CI or a container, and it worked fine locally.

Quoting. export MSG=hello world sets MSG to hello and then tries to run a command called world.

Treating them as secure. They are not encrypted and not private. Every child process inherits them, printenv lists them, and CI logs and crash reports print them cheerfully. Keeping a secret out of your repository is worth a lot; it is not a vault.

Two minutes. The point is not the greeting — it is watching a value fail to cross a boundary you did not know was there.

PowerShell. Set it, read it, then have a child process read it:

Terminal window
$env:GREETING = "hello from PowerShell"
$env:GREETING
powershell -NoProfile -Command '"child sees: " + $env:GREETING'

Now make it permanent and read it back in the same window:

Terminal window
[Environment]::SetEnvironmentVariable("GREETING", "persisted", "User")
$env:GREETING
What you should see

The first block prints the greeting twice — the child inherited it, because the parent was already holding it when the child started.

The second block still prints hello from PowerShell, not persisted. You wrote the new value to your Windows user profile, and this session is still holding the copy it made when it started. Open a new terminal and it will say persisted.

Git Bash. Run this, then run it again with export in front of the first line:

Terminal window
GREETING="hello from Git Bash"
echo "$GREETING"
bash -c 'echo "child sees: $GREETING"'
What you should see

Without export, echo prints the greeting and the child prints nothing. With export, both print it.

Without export you have a shell variable — your shell can read it, but it is not part of the environment handed to programs your shell starts. export is the word that moves it from my shell knows this to everything I start knows this.

Note that echo worked both times. The check most people reach for is the one that passes either way.

Now cross them. With GREETING set in your PowerShell session, open Git Bash:

Terminal window
echo "${GREETING:-(not set)}"
What you should see

(not set). Same machine, same folder, same project — different process, and session variables were never shared between them. Only the persisted one will show up, and only in a window opened after you set it.

To see everything at once: Get-ChildItem Env: in PowerShell, printenv in Git Bash.

Three ways to take this further. Paste them as they are, or edit in your own project’s details — the context is what makes the answer worth having.

Verify the model. “I now think an environment variable is a copy the operating system hands a program when it starts, which is why a running program never sees later changes and why export matters in Bash. Is that the whole picture, or is there something that matters on a real project that I am missing?”

Break it. “Give me a sequence of commands I can actually run where I set an environment variable, verify it correctly in my terminal, and a program still does not see it. Show me each command and what it prints, so I can reproduce it rather than take your word for it.”

Map around it. “I can configure a project with environment variables now. What do teams reach for when environment variables stop being enough, and what problem does each of those actually solve?”

A program gets its environment once, when it starts. Set the value before you start the thing, or the thing will never know.