Project-local shell history: the small change that stopped my worst typo

How I switched to per-project shell history so I stopped re-running the wrong commands on prod — the simple setup, the one mistake that still trips me, and why it's worth the tradeoffs.

Written by: Arjun Malhotra

Open laptop on a wooden table showing code in a terminal, with hands typing.
Photo by Glenn Carstens-Peters on Unsplash

It was 1:20 a.m., my home UPS had started its third automatic restart that week, and I was SSHing into the staging box over flaky Airtel broadband. I wanted to re-run a migration that had failed earlier. I pressed the up arrow out of habit to recall the last psql command I’d used on my local machine and hit Enter.

The command ran. Not on staging. On production.

Three hours and awkward Slack messages later, we had a partial rollback, a hotfix branch pushed, and one customer refund of about ₹4,500 that I still think about. The mistake wasn’t dramatic. It was gloriously, boringly human: my shell history gave me the wrong command at the worst possible time.

After that night I stopped defensively tapping my foot and started treating my shell history like a safety boundary.

Why project-local history For years I assumed my global shell history was helpful. It is — until it isn’t. The common problems I saw:

Project-local history does one obvious thing: it keeps the list of typed commands tied to the repository you’re in. If I’m in repo-x, my history file is repo-x/.history; in repo-y, it’s repo-y/.history. No more accidental command resurrection from the wrong project.

How I set it up (simple, low hassle) I wanted a setup that worked with zsh and bash, didn’t require me to install a bunch of tooling, and survived my usual laptop reboots and slow hotel Wi‑Fi. This is what I landed on.

  1. Decide where to store per-project history I use the repo root’s .git dir for convenience: .git/.shell_history (If you prefer not to touch .git, use .cache/shell_history or $XDG_STATE_HOME/project-shell-history.)

  2. Hook into the shell initialization Zsh (in ~/.zshrc):

if [ -n “$PWD” ]; then PROJECT_ROOT=$(git rev-parse —show-toplevel 2>/dev/null) if [ -n “$PROJECT_ROOT” ]; then export HISTFILE=“$PROJECT_ROOT/.git/.shell_history” export HISTSIZE=5000 export SAVEHIST=5000 else export HISTFILE=“$HOME/.shell_history” fi fi

Bash is similar; adjust HISTFILE and history options accordingly.

  1. Keep the history file shared across tabs I enable immediate append so if I open multiple shells in the same repo, they all write to the same file:

shopt -s histappend # bash export PROMPT_COMMAND=“history -a; history -n; $PROMPT_COMMAND” # append and reload

  1. A tiny guard for dangerous commands I added a wrapper that prompts for confirmation if a command contains psql -c ‘drop’ or rm -rf and I’m in a repo marked as “production-affecting”. We have a file PROD_PROTECT in repos with DB migrations. The script checks for that file and prompts once per shell session.

Why this stuck

The honest failure I ran into I implemented this and felt smug for a week — until I pushed a .git/.shell_history accidentally. Yes, I committed the file. So the very thing I used to avoid leaking commands almost leaked commands to the remote repo. Simple fix: add .git/.shell_history to .gitignore and set the Git exclude:

echo “.git/.shell_history” >> .gitignore git update-index —assume-unchanged .git/.shell_history # just in case

Lesson: never assume little files won’t be committed. Double-check your gitignore and your team’s workflow. I also learned that some CI runners clone with a clean working tree, so their history file is empty — fine for CI, awkward for local tests where I wanted a shared history across teammates. That tradeoff made me decide: per-project history is for local safety, not for shared operational playbooks.

Tradeoffs and annoyances

When to not use it If you live in a world of many micro-repos and you constantly switch contexts inside one parent directory, per-repo history can feel fragmented. Also, if you share a single dev machine between multiple people (rare for me), per-project history won’t protect against someone else’s mistakes.

A simple habit that goes with it Project-local history is a safety net, not a strategy. Combine it with two tiny habits that cost almost nothing:

Takeaway I like small, local safety boundaries. Project-local shell history stopped me from resurrecting the wrong command at 1:20 a.m. and saved my team at least one painful weekend. It won’t stop every risky action, and I still mess up, but it changed the kinds of mistakes I make from catastrophic to fixable.

If you do one thing tonight: add a project-local HISTFILE and add that file to .gitignore. It takes five minutes. It might save a refund, a patch, or a sleepless sprint review.