Every developer has done it. You ran a complex command ten minutes ago -- maybe a multi-flag curl, a gnarly find pipeline, or an awk one-liner that took three tries to get right. Now you need it again. You press the up arrow. And you press it again. And again. Forty commands later, you give up and retype the whole thing from scratch.
Your shell history is one of the most powerful productivity tools you already have installed. Most developers barely scratch the surface with Ctrl+R. This post goes deeper -- into history expansion, fuzzy search with fzf, per-directory history, and the configuration tweaks that make your history actually useful.
If you haven't already, check out our first post on 10 Terminal Shortcuts That Will Save You Hours Every Week for the foundational shortcuts that pair well with everything here.
History Expansion: Your Shell's Built-In Recall System
Before you install anything, your shell already has a rich history expansion system. Most developers know !! (repeat last command), but that's just the beginning.
The Essentials
# Repeat the entire last command
sudo !!
# Expands to: sudo <whatever you just ran>
# Grab the last argument of the previous command
mkdir -p src/components/analytics/dashboard
cd !$
# Expands to: cd src/components/analytics/dashboard
# Grab the first argument of the previous command
ls -la /var/log/nginx/access.log
cat !^
# Expands to: cat /var/log/nginx/access.log
Going Further
The real power is in the less-known expansions:
# Run the command from 3 commands ago
!-3
# Run the most recent command starting with "docker"
!docker
# Run the most recent command containing "deploy"
!?deploy?
# Grab ALL arguments from the last command
echo one two three
printf "%s\n" !*
# Expands to: printf "%s\n" one two three
# Grab a specific argument by position (0-indexed)
tar -czf backup.tar.gz /home/user/documents
echo !:2
# Expands to: echo backup.tar.gz
Safe Mode: Verify Before Executing
History expansion can be dangerous if you're not sure what it will expand to. Add :p to print the expansion without running it:
# Preview what !docker would run
!docker:p
# Prints: docker compose up -d --build
# Now press up arrow to edit or Enter to run
This is critical for production environments. Never run !rm blind on a server. Always !rm:p first.
Fuzzy History Search with fzf
Ctrl+R is fine for commands you remember part of. But when you're searching through thousands of history entries, you need something smarter. fzf transforms your shell history into a fuzzy-searchable, interactive list.
Installation
# macOS
brew install fzf
$(brew --prefix)/opt/fzf/install
# Ubuntu/Debian
sudo apt install fzf
# From source (any platform)
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
Once installed, Ctrl+R gets replaced with fzf's interactive fuzzy finder. The difference is night and day:
- Standard
Ctrl+R: Matches one result at a time, exact substring only, cycles backward - fzf
Ctrl+R: Shows all matches at once, ranks by fuzzy relevance, lets you scroll and filter in real time
Advanced fzf History Usage
# Set fzf defaults for history search in your .zshrc or .bashrc
export FZF_CTRL_R_OPTS="
--preview 'echo {}'
--preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--header 'Press CTRL-Y to copy to clipboard, CTRL-/ to toggle preview'
"
This gives you a preview pane, clipboard copy support, and a help header -- all inside your history search.
Combining fzf with History Commands
# Search history and immediately edit the selected command before running
fzf_history_edit() {
local selected
selected=$(fc -rl 1 | fzf --no-sort --tac | sed 's/^ *[0-9]* *//')
if [[ -n "$selected" ]]; then
print -z "$selected"
fi
}
alias hh='fzf_history_edit'
Now hh opens your full history in fzf, and the selected command is placed on your command line ready to edit before execution.
hstr: A Dedicated History Manager
If you want a focused history tool without fzf's broader scope, hstr is purpose-built for shell history management. It offers three search modes (exact, regex, fuzzy), bookmarking, and the ability to delete specific entries.
# macOS
brew install hstr
# Ubuntu/Debian
sudo apt install hstr
# Configure it to replace Ctrl+R
hstr --show-configuration >> ~/.bashrc
source ~/.bashrc
hstr is particularly good at one thing fzf doesn't do out of the box: letting you delete embarrassing or dangerous commands from your history. Select an entry and press Del to remove it permanently.
What if you didn't have to search at all? RewriteCmd takes a different approach: instead of digging through history for a command you ran before, you describe what you want in plain English and get the exact command back. No memorization. No searching. Just results.
curl -fsSL https://rewritecmd.com/install | sh
Per-Directory History
Here's a scenario: you work on five different projects. Each project has its own set of commands -- different Docker setups, different test runners, different deploy scripts. But your shell history is one global list. When you're in your Python project and press Ctrl+R, you're wading through Node.js commands from two hours ago.
Per-directory history solves this. Your shell records separate history for each directory you work in.
Zsh: zsh-per-directory-history
# Install via your Zsh plugin manager (e.g., zinit)
zinit light jimhester/per-directory-history
# Or clone manually
git clone https://github.com/jimhester/per-directory-history.git \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/per-directory-history
Once enabled, Ctrl+R searches your current directory's history by default. Press Ctrl+G to toggle between per-directory and global history. This means when you're in ~/projects/api-server, your history search only shows commands you've run in that directory.
Bash: Manual Approach
Bash doesn't have a plugin ecosystem like Zsh, but you can wire up per-directory history with a function:
# Add to ~/.bashrc
__per_dir_history() {
local dir_hash
dir_hash=$(echo -n "$PWD" | md5sum | cut -d' ' -f1)
export HISTFILE="$HOME/.bash_history_dirs/$dir_hash"
mkdir -p "$HOME/.bash_history_dirs"
history -r
}
PROMPT_COMMAND="__per_dir_history;$PROMPT_COMMAND"
This switches your HISTFILE every time you cd, giving each directory its own history file.
Tuning Your History Configuration
The default history settings on most systems are far too conservative. Here's what to change and why.
Bash Configuration
# Add to ~/.bashrc
# Store 100,000 commands in memory and on disk
HISTSIZE=100000
HISTFILESIZE=200000
# Ignore duplicate commands and commands starting with a space
HISTCONTROL=ignoreboth
# Append to history file instead of overwriting (critical for multiple terminals)
shopt -s histappend
# Save multi-line commands as a single entry
shopt -s cmdhist
# Record timestamps for each command
HISTTIMEFORMAT="%F %T "
# Ignore trivial commands
HISTIGNORE="ls:cd:pwd:exit:clear:history"
# Write history after every command (not just on shell exit)
PROMPT_COMMAND="history -a; $PROMPT_COMMAND"
Zsh Configuration
# Add to ~/.zshrc
HISTSIZE=100000
SAVEHIST=200000
HISTFILE=~/.zsh_history
# Share history across all terminal sessions
setopt SHARE_HISTORY
# Don't store duplicate commands
setopt HIST_IGNORE_ALL_DUPS
# Remove older duplicate when a new duplicate is added
setopt HIST_IGNORE_DUPS
# Don't record commands starting with a space
setopt HIST_IGNORE_SPACE
# Write history incrementally (not just on exit)
setopt INC_APPEND_HISTORY
# Record timestamps
setopt EXTENDED_HISTORY
# Remove extra blanks from commands
setopt HIST_REDUCE_BLANKS
The single most impactful change: increase HISTSIZE to 100,000+ and enable histappend (Bash) or INC_APPEND_HISTORY (Zsh). Without these, you lose history every time you close a terminal, and you cap out at 500-2,000 entries by default. That's maybe two days of work for an active developer.
History Tools Compared
| Feature | Ctrl+R (built-in) |
fzf | hstr | RewriteCmd |
|---|---|---|---|---|
| Fuzzy matching | No (exact substring) | Yes | Yes | N/A (natural language) |
| Visual interface | No (inline) | Yes (full-screen) | Yes (full-screen) | Yes (inline) |
| Delete entries | No | No | Yes | N/A |
| Bookmark commands | No | No | Yes | N/A |
| Per-directory history | No | No | No | N/A |
| Works without history | No | No | No | Yes |
| Handles commands you've never run | No | No | No | Yes |
| Install required | No | Yes | Yes | Yes |
| Learning curve | Low | Low | Low | None |
The key differentiator in the last two rows: every history tool assumes you've run the command before. When you need a command you've never typed -- or one you ran six months ago on a different machine -- history search hits a wall. That's where a tool like RewriteCmd fills the gap: describe the task, get the command.
Putting It All Together
Here's the setup I'd recommend for most developers:
- Tune your history config (5 minutes, one-time) -- increase sizes, enable append mode, add timestamps
- Install fzf (2 minutes) -- replaces
Ctrl+Rwith something dramatically better - Learn five history expansions --
!!,!$,!string,!:2, and!cmd:p - Add per-directory history if you work across multiple projects
- Install RewriteCmd for the commands that aren't in your history
Each layer handles a different scenario. History expansion is instant for patterns you use constantly. fzf handles the "I ran this recently but don't remember the exact syntax" case. Per-directory history keeps context clean. And RewriteCmd handles everything else -- the command you need but have never typed before.
# Get RewriteCmd
curl -fsSL https://rewritecmd.com/install | sh
What's your history setup? Are you still on vanilla Ctrl+R, or have you customized your workflow? Share your .bashrc / .zshrc history config -- we're always looking for tricks we haven't seen.