Skip to content

Zsh Without the Bloat: A Minimal .zshrc

Somewhere between your first Mac and your second job, configuring Zsh stopped being something you do and became something you install. Oh My Zsh has been the default answer since 2009: three hundred plugins, a hundred and forty themes, and — if you’re not careful — a shell startup time you could measure with a sundial.

Here’s what the install script doesn’t mention: the useful core of all that is about forty lines of configuration. This guide builds a fast, minimal .zshrc that keeps the parts people actually love — history that works, completion that doesn’t need a manual, the grey ghost text — and drops the rest. No framework, no magic, and a prompt that appears before your finger has left the Enter key.

Before touching anything, get a baseline:

Terminal window
time zsh -i -c exit

Run it a few times and take the best. This starts an interactive zsh, exits immediately, and tells you what your current setup costs per shell. Note the number — you’ll want it for the smug comparison at the end.

Credit where due: Oh My Zsh isn’t a scam. It’s a bundle, and bundles are how you end up paying for channels you don’t watch. Unpack it and you get:

  • Sensible defaults for history and completion — which we’re about to set directly, in six lines.
  • A prompt theme engine — replaced, better and cross-shell, by Starship.
  • Plugin loading — which, for the two plugins that matter, is two source lines.
  • The git plugin’s aliases — genuinely beloved, and the full list is public. Steal your favourites; we’ll get to that.
  • Three hundred other plugins and 140 themes you will never enable.

The real cost isn’t even the startup time — it’s the opacity. Most framework users can’t say what their shell is actually doing. The config below is one file you understand completely.

This is the whole thing. Create or replace ~/.zshrc:

Terminal window
# ~/.zshrc — the whole thing
## History
HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt SHARE_HISTORY # one history shared across all sessions
setopt HIST_IGNORE_DUPS # no consecutive repeats
setopt HIST_IGNORE_SPACE # " secret-command" never touches history
setopt HIST_REDUCE_BLANKS
## Completion: rebuild the cache daily, not once per shell
setopt EXTENDED_GLOB # the glob qualifier below is useless without it
autoload -Uz compinit
if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then
compinit
else
compinit -C
fi
zstyle ':completion:*' menu select # arrow through completions
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' # case-insensitive
## Quality of life
setopt AUTO_CD # type a directory name to cd to it
setopt INTERACTIVE_COMMENTS # allow # comments at the prompt
setopt NO_BEEP
bindkey -e # readline-style keys, whatever $EDITOR says
## Tools
eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
eval "$(fzf --zsh)"
## Plugins (install below; keep syntax-highlighting last)
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

The lines that earn their keep, in order of subtlety:

  • The compinit dance. By default zsh audits your completion directories on every shell start. The glob qualifier here runs the full rebuild only when the cache is more than a day old, and uses compinit -C — audit skipped — the other 99% of the time. Note that EXTENDED_GLOB is load-bearing: without it, (#qN.mh+24) is not a glob qualifier but an ordinary non-empty string, the fast path never runs, and nothing anywhere complains. Worth tens of milliseconds a shell on a tidy machine, and rather more once your fpath has accumulated a few years of Homebrew completions.
  • matcher-list. Case-insensitive completion: cd Doc<TAB> finds Documents. The way it should have worked all along.
  • SHARE_HISTORY. One history across every window, tab and tmux pane, appended as you go. Pairs beautifully with Ctrl+R — and even better with fzf’s fuzzy version.
  • HIST_IGNORE_SPACE. Prefix a command with a space and it never enters history. For commands containing things they shouldn’t.
  • bindkey -e. Zsh takes a worrying number of cues from $EDITOR; if that’s vim, you get vi key bindings at the prompt and a confusing afternoon. This pins the readline-style bindings from our shortcuts cheat sheet.
A zsh prompt reading vim src/config.py, with the completion menu below it listing config.py, main.py, server.py and widgets/, and config.py highlighted.
What menu select buys: Tab twice after vim src/ and the candidates become a menu you can arrow through. · zsh 5.9 in Ghostty 1.3.1

The tool init lines assume you’ve installed Starship, zoxide and fzf — delete any you’re not using, though you’d be missing out. If you work across several projects, direnv earns a line here too: its hook loads each project’s environment variables as you cd in, and removes them as you leave.

Two plugins earn their place in a minimal setup:

  • zsh-autosuggestions — the grey ghost text that completes commands from your history. The single plugin people actually miss.
  • zsh-syntax-highlighting — commands turn green when valid and red when you’ve typed rubbish. A linter for your prompt.

Install them directly:

Terminal window
brew install zsh-autosuggestions zsh-syntax-highlighting

Then adjust the two source lines in your .zshrc to point at Homebrew’s copies:

Terminal window
source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

One rule: syntax-highlighting must be sourced last — it hooks the line editor and expects to be the final word. The config above already has them in the right order.

If you later outgrow manual sourcing and want pinning and lazy loading, antidote and sheldon are the grown-up plugin managers. But two plugins don’t need a manager any more than two books need a librarian.

The Oh My Zsh git plugin’s alias list is its most-loved feature, and it’s just a text file. Raid it for your favourites:

Terminal window
alias gst='git status'
alias gco='git checkout'
alias gd='git diff'
alias gp='git push'
alias gl='git pull'

Git keeps an alias system of its own as well, which lives in ~/.gitconfig, works in every shell and completes after git . The git config guide has a set worth stealing, alongside the settings that stop git pull demanding an opinion.

For ls and cat, skip the aliases entirely and install the modern replacements from our CLI starter pack — eza and bat are worth the upgrade. grep is the exception: leave it be, type rg instead, and give ripgrep its defaults through its own config file, which needs only an export RIPGREP_CONFIG_PATH= line here.

If your Oh My Zsh setup starts in 100ms and sparks joy, keep it. The point of all this isn’t that frameworks are wicked — it’s that they’re optional, and that forty understood lines beat four hundred inherited ones. Run the time command again now, and let the number decide.

That’s the shell itself sorted. Complete the stack: a prompt that earns its keep, the modern CLI starter pack for the tools you’ll be typing at it, the keyboard shortcuts worth memorising now that bindkey -e has made them all work, and tmux to keep your sessions alive when the Wi-Fi won’t. And once it’s all in place, put it in git before the disk dies. And when the shell stops being somewhere you type commands and becomes somewhere you write them, strict mode is what stops those scripts failing quietly.