Alt: “Your Linux setup is leaking. These 30+ environment variables are why.”
You’ve been writing code on Linux for years. Maybe you run it in Docker, on a VPS, on your actual machine like a person with good taste. You know your way around a terminal. You feel comfortable there.
And yet.
Somewhere between your first apt install and your current career, you quietly decided that environment variables were a "good to know someday" topic. You learned PATH. You learned HOME. You maybe, on a heroic day, looked up what SHELL does. Then you moved on.
That’s fine. Until you’re debugging a production issue at an hour you’d rather not name, and the answer is buried in a variable you’ve never heard of. Or worse until someone who has done their homework walks into a system and does things you can’t explain because you never learned the control plane sitting right under your nose.
Here’s the uncomfortable truth: environment variables aren’t a Linux curiosity. They’re the configuration layer for every process, every shell session, every tool you run. Hackers the ethical, CTF-grinding, red-team-report-writing kind treat them like first-class knowledge. Most developers treat them like a footnote.
This article fixes that. We’re going through 30+ of them across three tiers: the essentials you should already know cold, the power-user variables most people skip entirely, and the ones that show up in every serious security engagement. By the end, you’ll have a mental model for this stuff that actually sticks.
TL;DR: There are dozens of Linux environment variables that shape how your system behaves. Most developers know three. This guide covers 30+, split into essentials, power-user config, and security-critical variables with real examples and the context to actually use them.
Tier 1: The essentials the 10 you should know cold
Most developers exist in a comfortable relationship with about three environment variables. PATH gets them where they need to go. HOME tells their tools where to store things. USER shows up occasionally in a script. That’s the whole map.
The problem is that “knowing” PATH and actually understanding what it does are two very different things. And when something breaks a command not found, a tool opening the wrong editor, logs full of encoding garbage the answer is almost always in one of these ten variables. You just didn’t know to look there.
Let’s close that gap.
PATH
The most consequential variable on your system. When you type any command, Linux doesn’t search every directory it walks through PATH left to right and stops at the first match.
echo $PATH
# /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH=/usr/local/bin:$PATH
Order matters more than most people realize. If you’ve ever installed a tool and gotten an older version back, PATH ordering is your suspect. The directory that appears first wins, every time.
HOME
The absolute path to your current user’s home directory. Nearly every application uses this to figure out where to read config, write cache, and store data.
echo $HOME
# /home/devtips
Change HOME and you change where everything lands. Useful to know when you’re scripting something that needs to behave differently across users.

USER
The username of whoever is running the current session. Simple on the surface, essential in any script that needs to branch based on who’s executing it.
echo $USER
# devtips
Don’t hardcode usernames in scripts. Read USER instead. Your future self will thank you when someone else runs it.
SHELL
The path to your active shell binary. This determines your scripting behavior, your tab completion, and your default prompt.
echo $SHELL
# /bin/bash
Never assume bash. In a lot of modern setups especially containers and minimal server installs you’re on dash, sh, or zsh. SHELL tells you what you’re actually dealing with.
PWD
Your current working directory, updated automatically every time you cd. More useful in scripts than in interactive use.
echo $PWD
# /home/devtips/projects
Use it to build absolute paths inside scripts instead of relying on relative paths that break the moment someone runs the script from a different directory.
HOSTNAME
The network name of the machine you’re on. The one you actually want in scripts that need to behave differently across dev, staging, and prod.
echo $HOSTNAME
# ubuntu-server
I’ve seen scripts that hardcode machine names. I’ve seen what happens when those machines get renamed. Set HOSTNAME checks in your scripts and stop praying.
LANG
Controls language, character encoding, and locale behavior across the entire system.
echo $LANG
# en_US.UTF-8
export LANG=en_US.UTF-8
Mismatched locales are responsible for some of the most baffling, hard-to-reproduce bugs in production. Logs showing up as question marks. Sorting behaving wrong. String comparisons failing in ways that make no sense. Always set this explicitly in production scripts don’t inherit whatever the system happens to have.
TERM
Tells applications what kind of terminal they’re dealing with, which determines what escape codes they use for colors, cursor movement, and formatting.
echo $TERM
# xterm-256color
If your terminal output looks garbled, colors aren’t rendering, or a tool is behaving like it’s running blind TERM is your first check. A lot of SSH sessions inherit a wrong TERM value and everything downstream breaks quietly.
EDITOR
Defines which text editor programs open when they need you to write something git commit messages, crontab entries, anything that spawns an interactive editor.
export EDITOR=vim
Set this once in your ~/.bashrc and every tool that respects it falls in line. Not setting it means you're at the mercy of whatever the system default is. On a lot of servers, that's ed. You do not want to meet ed unprepared.
TZ
Sets the timezone for the current session and any process that inherits the environment.
export TZ=America/New_York
# or
export TZ=Asia/Karachi
This one bites teams constantly. Two servers, same codebase, logs showing timestamps an hour apart someone forgot to standardize TZ across environments. In containerized systems especially, never assume the timezone is what you think it is. Set it explicitly and move on.
Those are your ten. If you can explain what each one does without looking it up, you’re already ahead of most people running Linux daily. If two or three were new good, now they’re not.
Tier 2: Power user variables most devs sleep on
Here’s where the gap actually opens up. Tier 1 variables are the ones you stumble into eventually they show up in error messages, Stack Overflow answers, and “getting started with Linux” guides. You learn them by accident.
Tier 2 doesn’t work that way. Nobody’s going to hand you these. They live in the corners of documentation pages, in experienced engineers’ dotfiles, in the kind of config that makes you think “wait, you can just do that?” the first time you see it. Most developers go years without touching them. Power users configure them on day one.
PS1
Your bash prompt is programmable. PS1 is the variable that controls exactly what it displays.
echo $PS1
export PS1="[\u@\h \W]\$ "
# Output: [devtips@ubuntu projects]$
\u is your username, \h is hostname, \W is the current directory. You can layer in colors, git branch names, exit codes from the last command, timestamps basically anything. Think of the default prompt like a stock game HUD. PS1 is where you remap everything to actually make sense for how you work. If you're still on the default, you're leaving information on the table every time you open a terminal.
HISTSIZE
Controls how many commands are kept in memory during your active session.
bash
echo $HISTSIZE
# 1000
export HISTSIZE=10000
The default on most systems is 1000. That sounds like a lot until you’re trying to find a command you ran three days ago and it’s gone. Power users set this to something large ten thousand, fifty thousand and treat their history like a searchable log of everything they’ve done. Pair it with Ctrl+R for reverse history search and you've got a lightweight audit trail of your own work.
We’ll revisit HISTSIZE in Tier 3 for very different reasons.
HISTFILESIZE
The on-disk companion to HISTSIZE. Controls how many commands get saved to ~/.bash_history when your session ends.
bash
echo $HISTFILESIZE
# 2000
export HISTFILESIZE=20000
These two variables are separate knobs and that trips people up. HISTSIZE is what’s held in memory during your session. HISTFILESIZE is what gets written to disk afterward. You can have a large in-session history and a small file, or vice versa. Set both deliberately instead of inheriting whatever your distro shipped with.

MANPATH
Defines where the man command looks for manual pages.
export MANPATH=/usr/local/share/man:$MANPATH
This one matters the moment you start installing tools to non-standard locations custom builds, things in /opt, tools you've compiled yourself. If man yourtool returns nothing, the manual exists somewhere your system doesn't know to look. Add the right path to MANPATH and it works immediately. Most people just Google the man page instead. Setting MANPATH is faster and works offline.
DISPLAY
Used by the X Window System to specify which display server to connect to.
echo $DISPLAY
# :0.0
export DISPLAY=:0.0
You won’t think about this variable until the exact moment you need it. That moment is usually: you SSH into a remote machine, try to open something with a GUI, and get a cryptic error about not being able to connect to a display. DISPLAY is what tells the application where to render. Get it right and the GUI opens on your local screen. Get it wrong and you’re reading X11 error messages at a time of day that tests your patience. Enable X11 forwarding in your SSH config and set DISPLAY accordingly.
Points to the location of the current user’s mail spool where local system mail lands.
echo $MAIL
# /var/mail/devlink
Nobody thinks about this one. Cron jobs do. When a scheduled task fails or produces output, it doesn’t throw an error into the void it sends local mail. If MAIL isn’t set or nobody’s checking it, those failure messages have been quietly piling up unseen. This is genuinely how some cron jobs silently die for months before anyone notices. Check your mail spool occasionally. You might find a graveyard.
OSTYPE
Tells you what operating system you’re running on at the shell level.
echo $OSTYPE
# linux-gnu
The place this earns its keep is cross-platform shell scripting. If you’re writing a script that needs to run on both Linux and macOS and eventually you will be OSTYPE lets you branch cleanly without spawning a uname subprocess. Linux returns linux-gnu, macOS returns darwin, BSD variants have their own values. One variable, clean conditional logic, no forks.
COLORTERM
Signals to applications that your terminal supports true color full 24-bit RGB rather than the 256-color or 8-color fallback modes.
echo $COLORTERM
# truecolor
export COLORTERM=truecolor
This is one of those variables where not setting it correctly causes problems that look like something completely different. Your terminal supports full color. Your tool supports full color. But COLORTERM isn’t set, so the tool falls back to 256 colors and everything looks slightly off in a way you can’t quite name. Set it explicitly in your dotfiles and stop wondering why your color scheme looks duller on one machine than another.
Those eight variables separate the people who use Linux from the people who actually configure it. None of them are secrets they’re all in the documentation. But documentation doesn’t tell you why they matter or when you’ll need them. That’s what experience does, and now you’ve got a head start.
Tier 3: The hacker toolkit
Disclaimer: Everything in this section is for ethical hacking, penetration testing, CTF challenges, and defensive security awareness. Understanding what attackers use is how defenders build better detection. Use these only on systems you own or have explicit written permission to test.
This is where the article gets interesting.
Tiers 1 and 2 are about configuration. Tier 3 is about control. These variables don’t just shape how your shell looks or which directories get searched they touch process loading, traffic routing, credential handling, library injection, and session forensics. They’re the reason experienced security engineers audit environment variables on any box they’re responsible for, and why attackers learn them before almost anything else.
None of this is exotic. It’s all documented. It’s all standard Linux. That’s precisely what makes it dangerous.

HISTSIZE=0 and HISTFILESIZE=0
export HISTSIZE=0
export HISTFILESIZE=0
Set both to zero at the start of a session and you get complete command history suppression. Nothing gets stored in memory, nothing gets written to disk when the session ends. Standard OPSEC in any authorized red team engagement.
Why defenders care: if you’re doing incident response on a compromised machine and find these set early in a session especially in .bashrc or /etc/profile someone was thinking about logging before they started working. That's not accidental configuration. That's a signal.
http_proxy and https_proxy
export http_proxy="http://10.10.10.10:8080"
export https_proxy="http://10.10.10.10:8080"
Any process that respects these variables routes its traffic through the specified proxy. This is how security testers intercept application traffic through tools like Burp Suite during an engagement without touching application code, without modifying config files, without restarting services. Set the variable, run the process, read the traffic.
The lowercase versions (http_proxy) are respected by most command-line tools. Some applications only check the uppercase versions. In practice, set both.
SSL_CERT_FILE and SSL_CERT_DIR
export SSL_CERT_FILE=/path/to/ca-bundle.pem
export SSL_CERT_DIR=/path/to/ca-certificates
Processes that read these variables trust the certificates you point them at. In an authorized testing context, this is how you get an application to trust your proxy’s self-signed certificate so you can inspect encrypted HTTPS traffic without the tool throwing certificate errors and refusing to connect.
Combined with http_proxy, these two variables give you a complete traffic interception setup without touching a single config file inside the application.
LD_PRELOAD
export LD_PRELOAD=/tmp/custom.so
This is the most powerful variable on this list. When set, the dynamic linker loads your specified shared library before anything else before the C standard library, before every other dependency the binary has. Functions in your library override functions in any other library the process loads.
The implication: you can intercept system calls, hook functions, and fundamentally alter the behavior of a running process entirely from outside its source code. No recompilation. No patching. Just an environment variable.
In CTF challenges and authorized penetration tests, LD_PRELOAD shows up in privilege escalation paths, sandbox bypasses, and function hooking scenarios. It’s also widely used legitimately memory profilers, debugging tools, and performance analyzers all use this same mechanism.
Why defenders care: monitor for LD_PRELOAD pointing to paths in /tmp or world-writable directories. Legitimate tools don't load libraries from there. Something else does.
LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/tmp/mylib:$LD_LIBRARY_PATH
Defines the directories the dynamic linker searches for shared libraries. By prepending a custom directory, you can substitute your own version of any library a binary depends on the binary loads your library instead of the real one and never knows the difference.
This is the environment variable equivalent of DLL hijacking on Windows. Same concept, same impact, different operating system. The attack works because most binaries trust that the libraries they load are the ones they expect.
SUDO_ASKPASS
export SUDO_ASKPASS=/tmp/fake-prompt
sudo -A whoami
When sudo is called with the -A flag, instead of prompting for a password in the terminal, it executes whatever program is set in SUDO_ASKPASS and uses that program's output as the password. In authorized social engineering simulations, this technique demonstrates how applications and GUI wrappers around sudo can be leveraged to intercept credential input without the user realizing the prompt they're responding to isn't the real one.
LD_DEBUG
export LD_DEBUG=libs
./somebinary
A reconnaissance variable. Setting it makes the dynamic linker print detailed output about every library being loaded the full path, the search order, which directories were checked, which version was found. No special permissions required. No tools to install.
In authorized engagements, this is how you identify which libraries a binary depends on and whether any of them are loaded from locations that could be hijacked with LD_LIBRARY_PATH. It turns library loading from a black box into a visible, auditable process.

IFS
export IFS=$'\n'
IFS Internal Field Separator tells bash how to split strings into tokens. The default is space, tab, and newline. Changing it changes how every command and script in your session parses its inputs.
In exploit development and CTF challenges, subtle IFS manipulation breaks input validation in scripts that weren’t written with this in mind. A script that sanitizes space-separated input suddenly behaves differently when the separator changes. It’s a small variable with outsized consequences in poorly written shell code which, in production environments, is not rare.
GDBINIT
export GDBINIT=/tmp/custom-gdbinit
GDB the GNU Debugger reads initialization commands from the file specified here on startup. In authorized assessments, this demonstrates how developer tooling itself can become an execution vector when environment variables aren’t controlled. CI pipelines, developer workstations, build servers anything that invokes GDB in an environment where GDBINIT can be influenced is a potential target.
TMOUT
export TMOUT=1
Sets bash to automatically terminate after the specified number of seconds of inactivity. Setting it to 1 closes a shell almost immediately after it goes idle. In an authorized engagement context, this is how you ensure a session closes cleanly without leaving an open shell exposed on a system you’re no longer actively using.
For defenders, it’s also worth setting in /etc/profile on any server where unattended sessions are a risk. Idle shells with elevated privileges sitting open are an opportunity nobody needs to create.
XDG_CONFIG_HOME
export XDG_CONFIG_HOME=/tmp/custom-config
Applications that follow the XDG Base Directory Specification read their config from this path. Redirect it to a controlled directory and you supply a completely custom configuration to any XDG-compliant application without modifying a single file on the real filesystem. Clean, contained, reversible.
Thirteen variables. Every one of them is in the man pages, in the official documentation, available to anyone who reads carefully enough. The difference isn’t access to secret knowledge it’s whether you took the time to understand what was already there.
How to audit and manage your environment properly
You’ve now got 30+ variables in your head. The natural next question is: what’s actually set on my system right now, and how do I control it properly?
Most people interact with environment variables reactively they set something when a tool breaks, forget where they set it, and spend twenty minutes debugging the wrong file six months later. The fix is understanding the three distinct layers your environment is built from, and having a handful of commands you can reach for without thinking.
Viewing what’s currently set
Four commands, four slightly different outputs:
# Everything exported to the current environment
printenv
# A specific variable
printenv PATH
# All exported variables (similar to printenv)
env
# All shell variables including unexported ones - verbose
set | less
printenv and env show you what's exported — what child processes will inherit. set shows everything including shell-local variables that don't get passed down. For most auditing purposes, printenv is what you want. For thoroughness, set | less and scroll.
Setting variables know your layers
This is where most confusion lives. There are three distinct scopes and they don’t interact the way people assume:
# Current session only — gone when you close the terminal
export MY_VAR="value"
# Permanent for your user - survives reboots, applies to new sessions
echo 'export MY_VAR="value"' >> ~/.bashrc
source ~/.bashrc
# System-wide for all users - requires root
echo 'MY_VAR="value"' >> /etc/environment
The hierarchy goes: system (/etc/environment) → user shell config (~/.bashrc, ~/.bash_profile) → current session (export). Each layer can override the one above it. When a variable is behaving unexpectedly, you're almost always looking at a conflict between two of these layers something set in /etc/environment getting overridden in .bashrc, or a session export shadowing both.
I’ve personally spent an embarrassing amount of time debugging a “why is this variable always wrong” issue that turned out to be set correctly in .bashrc, overridden in .bash_profile, and then overridden again by a script that exported it fresh on every run. Check all three layers before assuming the system is broken.
Removing and locking variables
# Remove a variable from the current session
unset MY_VAR
# Lock a variable so it can't be modified in the current session
readonly SECURE_VAR="value"
unset is straightforward. readonly is underused once set, any attempt to modify or unset that variable in the current session returns an error. Useful for variables that should never change after initialization, like paths to critical binaries in a script.
Passing variables to a single command without exporting
MY_VAR="value" some-command
This sets the variable in the environment of some-command only it doesn't persist to your shell, doesn't affect other processes, disappears immediately after the command finishes. Useful for one-off overrides without polluting your session. A lot of developers don't know this syntax exists and reach for export when they don't actually need it.
Auditing for suspicious variables
On any machine you’re responsible for especially one you’ve inherited or that’s been flagged for investigation:
# Check startup files for variables that shouldn't be there
grep -r "LD_PRELOAD|HISTSIZE=0|SUDO_ASKPASS" <br> /etc/profile.d/ ~/.bashrc ~/.bash_profile ~/.profile
If any of those turn up unexpectedly, don’t assume it’s a misconfiguration. Investigate. Their presence in startup files especially HISTSIZE=0 and LD_PRELOAD pointing to /tmp is a meaningful signal, not noise.
That’s the full management toolkit. View, set, scope, lock, audit. Five operations that cover everything you’ll need in day-to-day work and in the more interesting situations this knowledge puts you in reach of.
Security rules most hardening guides forget
Configuration guides cover firewalls. They cover SSH keys. They cover fail2ban and port knocking and a dozen other things that are genuinely important. Environment variables don't make the list often, which is exactly why this attack surface stays underappreciated.
A few rules that actually matter:
Never store secrets in plain environment variables. They show up in printenv. They show up in /proc/<pid>/environ readable by any process running as the same user. They show up in crash dumps, in CI logs when a build fails mid-run, and in container inspection output if your orchestration config is even slightly misconfigured. Use a secrets manager. Pass secrets through files with tight permissions, not shell exports.
Lock down critical variables in scripts:
readonly PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
readonly SECURE_VAR="value"
Harden your history file:
chmod 600 ~/.bash_history
The default permissions on bash_history are often more permissive than they should be. Other users on a shared system can read your command history. One command does the job.
Audit environment files on every box you manage:
grep -r "LD_PRELOAD|HISTSIZE=0|SUDO_ASKPASS" <br> /etc/profile.d/ ~/.bashrc ~/.profile ~/.bash_profile
Run this on any system you’ve inherited, any container base image you didn’t build yourself, any machine that’s been flagged in an incident. Unexpected results here aren’t curiosities they’re findings.
Conclusion
Here’s a slightly uncomfortable opinion: most Linux security hardening guides are written for people who already know this stuff. They assume you understand the environment variable layer, so they skip it entirely and go straight to network rules and access controls. That assumption leaves a real gap one that shows up repeatedly in CTF writeups, in post-incident reports, and in the gap between developers who use Linux and engineers who understand it.
Environment variables are not a trivia topic. They’re the control plane for every process running on your system. The 30+ variables in this article aren’t exhaustive they’re the ones that matter most, the ones that explain the most behavior, and the ones that show up when things go wrong or when someone with bad intentions goes right.
The terminal is the most honest interface on any system. It hides nothing if you know what to ask. Start asking better questions.
As containers and srverless architectures continue to take over infrastructure, environment variables are increasingly the primary configuration layer injected at runtime, scoped per service, and almost never audited properly. That makes this knowledge more relevant every year, not less.
Go through this list on a test machine. Run each export. Watch what changes. The best way to internalize this is to break something deliberately in a safe environment and understand exactly why it broke.
And if you’ve got a cursed environment variable story a HISTSIZE=0 you found where it shouldn’t be, an LD_PRELOAD incident, a production outage that traced back to TZ being unset drop it in the comments. I genuinely want to hear it.
Helpful resources
- Linux man page: ld.so full documentation on LD_PRELOAD, LD_LIBRARY_PATH, LD_DEBUG behavior
- GTFOBins practical reference for how environment variables feature in privilege escalation paths
- Arch Wiki: Environment Variables one of the clearest practical guides on scoping and management
- XDG Base Directory Specification official spec for XDG_CONFIG_HOME and related variables
- OWASP Secrets Management Cheat Sheet why plain environment variables are the wrong place for credentials
Top comments (0)