Articles / Security

  • linux
  • bash
  • zsh
  • shell
  • secrets
  • security

Why Your Shell History Is a Secrets Leak, and How to Stop It

Run this on your own machine and see how you feel about the result:

grep -Ei 'token|passw|secret|api[_-]?key|bearer|AKIA' ~/.bash_history ~/.zsh_history 2>/dev/null

If it prints nothing, you are either very disciplined or you have history switched off. Most people get a few lines back: a curl -H "Authorization: Bearer ...", a mysql -pHunter2, an export AWS_SECRET_ACCESS_KEY=.... Each one is sitting in a plain text file, usually for years.

What is actually being stored

Your shell records every command line you type, exactly as typed. It has no idea which words are secrets. A file like ~/.bash_history is a flat log of everything, with no expiry.

Bash creates that file with mode 0600, so other users cannot read it. That is the good news. The bad news is everything else that can read your files as you:

  • backups and home-directory syncs (dotfile repos are a classic, one careless git add . away)
  • any process running under your account, including a compromised npm package or editor plugin
  • a stolen laptop with an unencrypted disk
  • screen shares, where someone presses the up arrow at the wrong moment

Attackers who land on a box check history early, because it is a free map of what you use and what you paste.

Quick detour: when does the file get written?

Quick detour, because this bit surprises people. Bash keeps history in memory and only writes it to the file when the shell exits cleanly. Zsh behaves differently depending on options.

So a secret you typed is already in memory and in the next saved file, even if you "delete" it from the file afterwards. Editing ~/.bash_history by hand while a shell is open is pointless, because the running shell will append its own copy on exit. Use history -d N inside the session, then history -w to rewrite the file.

Stop new secrets landing: the leading space

The cheapest fix is the oldest. In bash, set this in ~/.bashrc:

HISTCONTROL=ignoreboth   # ignorespace + ignoredups

Now any command starting with a space is not saved. Zsh needs setopt HIST_IGNORE_SPACE. Many distributions already enable this, so check with echo $HISTCONTROL before assuming. The habit is the hard part: it only works if you remember the space every time.

Better: do not put the secret on the command line at all

A leading space hides the command from history, but the secret still appears in the process list while it runs. Anyone on the machine can see another user's arguments with ps or by reading /proc/PID/cmdline, unless /proc is mounted with hidepid. So prefer ways that never use argv:

  • read from stdin: read -rs TOKEN in bash, then pass it via a here-string or the environment
  • use a file with tight permissions: curl -H @file style options, or --netrc-file
  • use the tool's own prompt: mysql -p with no value asks you
  • pull from a password manager or secret store at run time

For example, a token typed silently and handed to curl without it touching history or argv:

read -rs -p "Token: " TOKEN; echo
curl -sS --config - https://api.example.com/me <<EOF
header = "Authorization: Bearer $TOKEN"
EOF
unset TOKEN

The heredoc is expanded inside the shell and fed to curl on stdin, so the token is not in curl's arguments. The history line contains only the variable name.

Filtering by pattern with HISTIGNORE

Bash also has HISTIGNORE, a colon-separated list of glob patterns for commands that should never be saved:

HISTIGNORE='*TOKEN=*:*PASSWORD=*:*Bearer*:export *KEY*'

This catches the obvious shapes, and it is a safety net, not a guarantee. A secret in a form your patterns do not anticipate sails straight through. Treat it as a second layer behind the habits above.

Cleaning up what is already there

Start with the grep at the top, then remove the lines from the file. Do it with no other shells open, or they will write the entries straight back on exit. Then do the part people skip: rotate the secret. A token that sat in a plain text file for two years should be treated as exposed, whether or not you delete the line now.

Also check the neighbours. ~/.python_history, ~/.mysql_history, ~/.psql_history and ~/.rediscli_history all record what you typed into those tools, and nobody remembers to look in them.

The nuclear option, and why I do not love it

You can turn history off for a session with set +o history or unset HISTFILE, or point HISTFILE at /dev/null for good. That solves the leak by destroying a useful tool. History is genuinely valuable for working out what you did last Tuesday, and a disabled history just pushes people to keep notes in worse places.

My preference is the middle road: ignorespace on, HISTIGNORE as a net, secrets via stdin or a manager, and full disk encryption underneath for the case where none of that was enough.