Skip to content

Environment Variables and PATH

💡 Learning Guide: Every time you type git or python in the terminal, the system has to find where that program is located. Every time your code calls a large model API, the program needs to know which key to use. Both of these tasks rely on the same underlying mechanism — environment variables.


0. Every Program Carries a Set of Configuration

Every running program holds a set of "key=value" configurations called environment variables. The program can read these configurations at any time to learn about its current runtime environment.

Click on any variable in the list below to "view" its value in the terminal:

Environment Variable BrowserClick any variable row to inspect its value and purpose in the terminal
VariableExample value
HOME/Users/alice
USERalice
SHELL/bin/zsh
PATH/usr/local/bin:/usr/bin:/bin
PWD/Users/alice/projects
LANGen_US.UTF-8
NODE_ENVdevelopment
OPENAI_API_KEYsk-••••••••••••••••
bash
← Click any variable on the left to inspect it
$
Core concept:Environment variables are key=value configuration owned by each process. A program inherits a copy from its parent process at startup. You can inspect them with echo $VARIABLE and set them with export KEY=value.

1. PATH: How Shell Finds the Command You Typed

PATH is a special environment variable that stores a list of directory paths (separated by colons). When you type git, Shell searches through these directories one by one for an executable file named git — stopping as soon as it finds the first match.

bash
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

Select a command and observe how Shell searches through directories step by step:

PATH Search ProcessEnter a command name and see how the shell searches directories
Choose command:
Current PATH:
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
/usr/local/bin
Waiting
/usr/bin
Waiting
/bin
Waiting
/usr/sbin
Waiting
/sbin
Waiting
Core mechanism:After the shell receives a command name, it searches directories in PATH order. The first match wins and the search stops. That makes PATH order important: earlier directories have higher priority.

Three key rules:

  • Directories listed earlier in PATH have higher priority
  • Searching stops at the first match — no further directories are checked
  • If none of the directories contain the command → command not found

2. Why Do You Need to Restart the Terminal After Installing a Tool?

When you install tools like nvm, Homebrew, or conda, the installation script automatically appends a line to ~/.zshrc to add its directory to PATH:

bash
# Content automatically written by the installer (example)
export PATH="/usr/local/opt/python@3.12/bin:$PATH"

This line only executes when a new Shell starts. Already-open terminal windows are unaffected, so:

bash
# Take effect immediately without restarting
source ~/.zshrc

Common scenarios with AI development tools:

bash
# Ollama / pipx installed but getting "command not found"
which ollama          # Check actual installation location

# pip-installed CLI tool paths (add to PATH)
# macOS: ~/Library/Python/3.x/bin
# Linux: ~/.local/bin
export PATH="$PATH:$HOME/.local/bin"

# Recommended: use pipx to install CLI tools, manages PATH automatically
pipx install aider-chat

3. Variable Scope: Who Can See This Variable?

Environment variables are not broadcast to all programs — each process holds its own copy, inherited from the parent process. Modifying your own copy does not affect the parent process.

The diagram below shows three levels. Export a new variable at the "User Level" and see whether it appears at the "Process Level":

Three Levels of Environment VariablesVariables flow one way from outer scopes to inner scopes; child processes inherit a copy from parents
🖥️
System level /etc/environment
Visible to all users and processes; configured by an administrator
PATH=/usr/local/bin:/usr/bin:/bin
LANG=zh_CN.UTF-8
TZ=Asia/Shanghai
▼ Child process inherits the parent environment
👤
User level ~/.zshrc
Affects only the current user and is loaded when the login shell starts
HOME=/Users/alice
SHELL=/bin/zsh
NVM_DIR=$HOME/.nvm
=
▼ Start a child process, such as node app.js
⚙️
Process level (currently running program)
Inherits variables from upper levels, disappears on exit, and does not modify the parent process
PATH=/usr/local/bin:/usr/bin:/bin
LANG=zh_CN.UTF-8
TZ=Asia/Shanghai
HOME=/Users/alice
SHELL=/bin/zsh
NVM_DIR=$HOME/.nvm
NODE_ENV=development
PORT=3000
One-way flow:Variables are inherited downward only. Changing a variable in a child process does not affect its parent. Variables set with export in a terminal also disappear when that terminal closes.

4. export: Determining Whether Child Processes Can Read a Variable

When setting a variable, including or omitting export makes a completely different difference:

export Decides Whether Child Processes Can See a VariableToggle the switch and observe whether the child process can read a variable set by the parent
Parent process (Shell)
$MY_VAR="hello"
$echo $MY_VAR
hello
$bash -c 'echo $MY_VAR'
Start child process
Variable not inherited
Child process (bash -c ...)
$echo $MY_VAR
(empty output)
#A child process cannot modify parent variables
Without export: The variable exists only in the current shell, so the child process reads an empty string.

To make a variable persist across sessions, write the export statement into a configuration file:

bash
# macOS (zsh)
echo 'export MY_VAR="value"' >> ~/.zshrc
source ~/.zshrc       # Takes effect immediately, no need to reopen the terminal

# Linux (bash)
echo 'export MY_VAR="value"' >> ~/.bashrc
source ~/.bashrc

5. API Keys: Never Hardcode Them in Your Source Code

When calling APIs from OpenAI, Anthropic, DeepSeek, and others, your key is essentially your "ID card + credit card." If it leaks, others can spend your quota — and you foot the bill.

The most common mistake is hardcoding the key directly in source code:

Hard-coded Keys vs Environment VariablesThe same feature, two implementations, completely different security outcomes
Dangerous: key written in code
# Python
import openai
 
client = openai.OpenAI(
api_key="sk-proj-abc123..."
)
💀After git push, the key is public on GitHub
💀Crawlers can find it quickly and generate costs
💀GitHub Secret Scanner may revoke the key automatically
💀Deleting the commit is not enough because Git history keeps it
Correct: read from environment variable
# Python
import openai, os
 
client = openai.OpenAI(
api_key=os.environ.get("OPENAI_API_KEY")
)
The code contains no secret and can be open-sourced safely
Development, testing, and production can use different keys
If a key leaks, regenerate it without changing code
Team members can use separate keys without affecting each other
Golden rule:A secret string in code means the secret is already leaked. GitHub Secret Scanner can detect prefixes such as sk- shortly after a push and notify providers to revoke them. Even if you delete the commit, Git history still contains it.

6. Local Development: Managing Keys with .env Files

During local development, store keys in a .env file in the project root directory. Your code reads them via the dotenv library. .env must be added to .gitignore and never committed to Git.

Configure on the left, read on the right — switch languages to see both approaches:

.env File + Code ReadingConfiguration on the left, code on the right; the variable name is the only link
📄 .env Do not commit
# Local development config, do not commit to Git
OPENAI_API_KEY=sk-proj-abc123...
DATABASE_URL=postgresql://localhost/dev
PORT=3000
NODE_ENV=development
📋 .env.example Can commit
# Copy to .env and fill in real values
OPENAI_API_KEY=(leave empty)
DATABASE_URL=(leave empty)
PORT=(leave empty)
NODE_ENV=(leave empty)
💻 main.py
# pip install python-dotenv openai
from dotenv import load_dotenv
import os, openai
 
load_dotenv() # Read .env file
 
client = openai.OpenAI(
api_key=os.environ.get("OPENAI_API_KEY")
)
 
db = os.environ.get("DATABASE_URL")
port = int(os.environ.get("PORT", 8000))
Values actually read by the program
OPENAI_API_KEYsk-proj-abc123...
DATABASE_URLpostgresql://localhost/dev
PORT3000
Workflow:load_dotenv() / import 'dotenv/config' reads the .env file at startup, injects its key-value pairs into the process environment, and the code reads them with os.environ or process.env. The two sides are connected only by variable names.

7. Production: Let the Runtime Platform Inject Keys

.env is a convenience tool for the development phase. On servers and cloud platforms, the runtime environment should be responsible for injecting keys. The code itself should be completely unaware of where the keys are stored:

How Production Injects Secrets.env is a development convenience; servers should not rely on it
/etc/systemd/system/myapp.service
# Recommended: use a separate secret file with controlled permissions
[Service]
EnvironmentFile=/etc/myapp/secrets.env
ExecStart=/usr/bin/node /app/index.js
# Set file permissions so only the owner can read it
sudo chmod 600 /etc/myapp/secrets.env
sudo chown deploy:deploy /etc/myapp/secrets.env
# Reload configuration and restart the service
sudo systemctl daemon-reload
sudo systemctl restart myapp
After chmod 600, only the deploy user can read the secret file; other accounts cannot access it
Secrets are separated from code, so rotating a key does not require redeploying code
Avoid writing Environment="KEY=val" directly in the systemd file; it requires reloads and leaves plaintext in config
Principle:.env files are convenient for local development. In production, the runtime platform should inject environment variables, while application code stays unaware of where secrets live or how they arrived.

8. Practical Troubleshooting

command not found

bash
# Step 1: Check if it's in PATH
which python3         # If there's output, it was found

# Step 2: Find the program's actual location (macOS)
brew list python | grep bin

# Step 3: Add the directory to PATH
export PATH="/found/path:$PATH"
source ~/.zshrc       # Remember to source after writing to config file

Installed Two Versions, Not Using the One I Want

bash
which python
# /usr/bin/python ← Old system version, earlier in PATH

# Put the new version's directory at the front of PATH
export PATH="/usr/local/bin:$PATH"

which python
# /usr/local/bin/python ← New version, now takes priority

Variable Is Set, But the Program Can't Read It

CauseSolution
Forgot exportAdd export and try again
Modified ~/.zshrc but it didn't take effectRun source ~/.zshrc
Using .env but didn't install dotenvpip install python-dotenv / npm install dotenv
On server, variable only works in SSH sessionUse systemd EnvironmentFile instead

Quick Glossary

TermMeaning
PATHA list of directories where Shell searches for executables, colon-separated; order determines priority
exportMarks a variable as inheritable so child processes automatically get a copy
sourceRe-executes a configuration file in the current Shell, making changes take effect immediately
whichShows the executable file path for a command (the result of a PATH search)
.envA project-local configuration file for development keys; must be added to .gitignore
.env.exampleA template with complete variable names but empty values; safe to commit to Git
chmod 600File permission: only the owner can read and write; suitable for protecting key files
Secret ScannerGitHub and other platforms automatically scan for leaked keys and notify vendors to revoke them