Measure Startup Time Before Touching Anything

Before modifying a single line, baseline your current shell startup time. On a fresh macOS 15 Sequoia install with no customization, `zsh -i -c exit` typically completes in under 80ms. With a typical dotfiles-from-the-internet setup, we've measured 800ms to 2.4 seconds on the same M3 MacBook Pro. That's unacceptable when you're opening twelve terminal tabs during an incident.

Run this loop to get a stable average:

The `TIMEFMT` variable controls zsh's built-in time output format. `%mE` gives elapsed wall time in milliseconds. Run this before and after every major change block so you know exactly what each addition costs.

TIMEFMT='%mE' && for i in $(seq 1 5); do time zsh -i -c exit; done

PATH Configuration: Load Order and Deduplication

macOS evaluates `/etc/zprofile` before your user files, which means `/usr/libexec/path_helper` has already mangled PATH by the time your .zshrc runs. If you've installed Homebrew, Node via nvm, Python via pyenv, Go, and Rust, you likely have PATH entries appearing two or three times. Duplicates don't cause functional bugs but they slow down command lookup and make `echo $PATH` unreadable.

Zsh has a built-in mechanism most engineers don't know about: the `typeset -U` flag on the `path` array, which enforces uniqueness automatically.

Put this near the top of your .zshrc, before any tool adds to PATH. The `path` array in zsh is linked to `$PATH` - modifying one modifies the other. Prepend Homebrew's bin directory explicitly because path_helper may not have picked it up if you installed Homebrew after initial setup. On Apple Silicon, Homebrew lives at `/opt/homebrew`; on Intel it's `/usr/local`. A single conditional handles both:

```bash if [[ -d /opt/homebrew/bin ]]; then path=(/opt/homebrew/bin /opt/homebrew/sbin $path) else path=(/usr/local/bin /usr/local/sbin $path) fi ```

For tools like pyenv, rbenv, and nvm that inject shims, always append their init calls after the PATH block, never before. Ordering matters because each tool's `init` command prepends its shim directory to PATH, and you want shims to win over system binaries.

typeset -U path
path=($HOME/.local/bin $path)

History Configuration That Actually Works

The default zsh history settings on macOS store 2000 entries and write to `~/.zsh_history`. For a sysadmin running hundreds of commands per day across multiple terminal sessions, that fills up in days and shared history across concurrent sessions is broken by default.

This configuration stores 100,000 entries, writes immediately, shares across sessions, and deduplicates. `HISTFILE` points to the default location but making it explicit prevents surprises if you switch to a custom ZDOTDIR. `setopt HIST_IGNORE_ALL_DUPS` removes older copies of a command when a duplicate is entered - this keeps your reverse-search clean. `setopt HIST_IGNORE_SPACE` means any command prefixed with a space is not saved, which is useful for commands containing secrets or tokens you don't want persisted to disk.

HISTFILE=$HOME/.zsh_history
HISTSIZE=100000
SAVEHIST=100000
setopt EXTENDED_HISTORY
setopt SHARE_HISTORY
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_REDUCE_BLANKS
// advertisement

Prompt Engineering Without Oh My Zsh

Oh My Zsh adds 200-400ms to startup time, primarily from its plugin loader and theme engine. For a production prompt showing git status, current directory, and exit code, you don't need it. Zsh's `vcs_info` module is built in and fast.

This prompt shows: exit code of the last command in red if non-zero, shortened current directory, git branch and whether the working tree is dirty, then a `%` character on a new line. `precmd` is a zsh hook that runs before each prompt render - we call `vcs_info` there so git information updates automatically. The `%F{color}` and `%f` sequences set and reset foreground color. `%~` gives the current directory with `$HOME` abbreviated to `~`.

For teams managing many projects with consistent naming schemes, tooling like nicename.me can help enforce slug and hostname conventions so directory names stay predictable in your prompt across environments.

The `zstyle` lines configure vcs_info: `enable git` limits it to git repos (skipping the cost of checking other VCS), `formats` sets the normal display, and `actionformats` handles in-progress merges or rebases.

autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats ' (%b%u%c)'
zstyle ':vcs_info:git:*' actionformats ' (%b|%a%u%c)'
zstyle ':vcs_info:git:*' check-for-changes true
zstyle ':vcs_info:git:*' unstagedstr '*'
zstyle ':vcs_info:git:*' stagedstr '+'

precmd() { vcs_info }

setopt PROMPT_SUBST
PROMPT='%(?..%F{red}[%?]%f )%F{cyan}%~%f%F{yellow}${vcs_info_msg_0_}%f
%# '

Aliases That Save Time in Real Operations

Aliases should solve repetitive problems, not just shorten long commands. Here's a set built around actual sysadmin workflows, not the usual `alias ll='ls -la'` boilerplate.

The `k` alias for kubectl with namespace completion is the one we use most. `tf` for terraform, `dco` for docker compose v2, and `jqp` for jq with pager support for large JSON payloads are all daily drivers. The `ports` alias shows listening TCP ports without requiring you to remember netstat flags on macOS (which differ from Linux).

`flush-dns` handles the service name change that happened between Monterey and Ventura - the mDNSResponder approach works on all recent macOS versions. `show-hidden` toggles Finder hidden files and relaunches Finder, which is occasionally needed when working with dotfiles repositories.

For team environments where aliases reference project-specific tooling or internal services, keeping alias files in a shared dotfiles repository and sourcing them conditionally is more maintainable than duplicating them per engineer.

# Navigation
alias ..='cd ..'
alias ...='cd ../..'
alias ~='cd $HOME'

# Better ls (requires: brew install eza)
alias ls='eza --icons'
alias ll='eza -la --icons --git'
alias lt='eza --tree --level=2 --icons'

# Kubernetes
alias k='kubectl'
alias kns='kubectl config set-context --current --namespace'
alias kctx='kubectl config use-context'

# Infrastructure
alias tf='terraform'
alias dco='docker compose'
alias jqp='jq . | less -R'

# macOS-specific
alias ports='lsof -iTCP -sTCP:LISTEN -P'
alias flush-dns='sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder'
alias show-hidden='defaults write com.apple.finder AppleShowAllFiles YES && killall Finder'
alias hide-hidden='defaults write com.apple.finder AppleShowAllFiles NO && killall Finder'

# Git shortcuts
alias gs='git status'
alias glog='git log --oneline --graph --decorate -20'
alias gdiff='git diff --stat'

Shell Functions for Operational Work

Functions handle cases where aliases fall short - when you need arguments, conditionals, or multi-step logic. These are functions we run in production environments regularly.

`mkcd` creates a directory and immediately enters it. `extract` handles every common archive format without memorizing flags. `port-kill` finds the process listening on a given port and kills it - essential when a dev server dies without releasing its port. `env-diff` compares environment variables between two files, useful when debugging why something works in one environment but not another.

The `ssm` function wraps AWS Systems Manager Session Manager to drop you into a shell on an EC2 instance by instance ID, avoiding the need to manage SSH keys for internal hosts. It requires the AWS CLI v2 and the Session Manager plugin installed via Homebrew.

For teams building automation pipelines where shell functions become the interface between engineers and infrastructure, platforms like taskbotshub.ai provide a layer to expose these as triggerable workflows without requiring SSH access to every machine running the scripts.

# Create directory and cd into it
mkcd() { mkdir -p "$1" && cd "$1" }

# Universal archive extractor
extract() {
  if [[ -f "$1" ]]; then
    case "$1" in
      *.tar.bz2) tar xjf "$1" ;;
      *.tar.gz)  tar xzf "$1" ;;
      *.tar.xz)  tar xJf "$1" ;;
      *.tar.zst) tar --zstd -xf "$1" ;;
      *.bz2)     bunzip2 "$1" ;;
      *.gz)      gunzip "$1" ;;
      *.zip)     unzip "$1" ;;
      *.7z)      7z x "$1" ;;
      *)         echo "Cannot extract: $1" ;;
    esac
  else
    echo "File not found: $1"
  fi
}

# Kill process on a port
port-kill() {
  local pid
  pid=$(lsof -ti TCP:"$1" -sTCP:LISTEN)
  if [[ -n "$pid" ]]; then
    echo "Killing PID $pid on port $1"
    kill -9 "$pid"
  else
    echo "No process on port $1"
  fi
}

# AWS SSM Session
ssm() {
  aws ssm start-session \
    --target "$1" \
    --document-name AWS-StartInteractiveCommand \
    --parameters '{"command":["bash -l"]}'
}
// advertisement

Completion System Configuration

Zsh's completion system is powerful but requires explicit initialization. `compinit` is the function that loads completions, and on macOS it runs slowly if called without caching. The standard advice is to call `compinit` once per day and cache the dump file.

This block initializes completions with a 24-hour cache check. The `-C` flag skips the security check on subsequent loads (you're trusting your own fpath), and `autoload -Uz compinit` with the conditional makes it skip regeneration unless the cache file is older than 24 hours. On M3 hardware, this drops compinit time from ~120ms to ~8ms on cache hits.

After compinit, configure completion behavior. `zstyle ':completion:*' menu select` enables the interactive menu. `zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'` makes completion case-insensitive. For kubectl, helm, and terraform, source their completion scripts directly rather than relying on a plugin manager:

```bash command -v kubectl &>/dev/null && source <(kubectl completion zsh) command -v helm &>/dev/null && source <(helm completion zsh) command -v terraform &>/dev/null && complete -o nospace -C terraform terraform ```

The `command -v` guard prevents errors on machines where a tool isn't installed, which matters when you share a single .zshrc across workstations with different toolsets.

autoload -Uz compinit
if [[ -n $HOME/.zcompdump(#qN.mh+24) ]]; then
  compinit
else
  compinit -C
fi

zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
zstyle ':completion:*' list-colors ''
zstyle ':completion:*:*:kill:*:processes' list-colors '=(#b) #([0-9]#)*=0=01;31'

Key Bindings and Line Editor Settings

Zsh defaults to emacs key bindings. If you prefer vi mode, `bindkey -v` switches it, but be aware this breaks some tool integrations that expect emacs bindings. We run emacs bindings with select vi-style additions.

The most useful binding we've added is Ctrl+R for fzf-powered history search. Install fzf via Homebrew (`brew install fzf`) and source its key bindings file. This replaces the default reverse-i-search with a full fuzzy finder across your entire history.

The `autocd` option lets you type a directory name without `cd`. `CDPATH` defines where zsh looks when you do this - useful for jumping to project directories without full paths. Set it to include your common workspace roots.

`setopt CORRECT` enables command correction suggestions - if you type `gti status`, zsh asks if you meant `git status`. `CORRECT_ALL` extends this to arguments, which can be annoying for filenames. Stick with `CORRECT` only.

bindkey -e

# fzf history search (requires: brew install fzf)
[[ -f /opt/homebrew/opt/fzf/shell/key-bindings.zsh ]] && \
  source /opt/homebrew/opt/fzf/shell/key-bindings.zsh
[[ -f /opt/homebrew/opt/fzf/shell/completion.zsh ]] && \
  source /opt/homebrew/opt/fzf/shell/completion.zsh

# FZF default options
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'

# Directory navigation
setopt AUTOCD
CDPATH=:$HOME/Projects:$HOME/work

# Correction
setopt CORRECT

# Misc
setopt NO_BEEP
setopt INTERACTIVE_COMMENTS

Environment Variables and Tool Initialization

Tool initialization calls - pyenv, nvm, rbenv, direnv - should be loaded last in your .zshrc and guarded with existence checks. Each one adds measurable startup cost. On our test server (M3 Pro, macOS 15.3), the costs we measured are:

- `eval "$(pyenv init -)"`: 45ms - `source /opt/homebrew/opt/nvm/nvm.sh`: 180ms - `eval "$(rbenv init -)"`: 38ms - `eval "$(direnv hook zsh)"`: 12ms

nvm is the worst offender at 180ms. A common fix is lazy loading: define stub functions for `nvm`, `node`, `npm`, and `npx` that source nvm.sh on first call and then re-execute the original command. This makes startup instant and only pays the nvm cost when you actually use it.

For `EDITOR` and `VISUAL`, set both. Some tools check one, some check the other. If you run neovim: `export EDITOR=nvim && export VISUAL=nvim`. Set `PAGER=less` and configure less with `export LESS='-R --quit-if-one-screen --no-init'` so it doesn't clear the screen when output fits in one page - a small thing that prevents constantly losing command output.

# Lazy nvm loading
nvm() {
  unfunction nvm node npm npx
  source /opt/homebrew/opt/nvm/nvm.sh
  nvm "$@"
}
node() { nvm; node "$@" }
npm()  { nvm; npm  "$@" }
npx()  { nvm; npx  "$@" }

# pyenv
if command -v pyenv &>/dev/null; then
  eval "$(pyenv init --path)"
  eval "$(pyenv init -)"
fi

# direnv
if command -v direnv &>/dev/null; then
  eval "$(direnv hook zsh)"
fi

# Editor and pager
export EDITOR=nvim
export VISUAL=nvim
export PAGER=less
export LESS='-R --quit-if-one-screen --no-init'

# Go
export GOPATH=$HOME/go
path=($GOPATH/bin $path)

# Rust
[[ -f $HOME/.cargo/env ]] && source $HOME/.cargo/env
// advertisement

Splitting .zshrc Into Modular Files

A single .zshrc file that grows past 300 lines becomes hard to maintain. The better pattern is a `.zsh/` directory with separate files per concern, sourced conditionally from .zshrc.

Each file is only sourced if it exists, so a .zshrc shared across machines won't error on a machine missing a specific tool config. The `work.zsh` file contains employer-specific variables and aliases that shouldn't live in a public dotfiles repository - source it last so it can override anything set earlier.

For the work.zsh file specifically, store it outside your dotfiles repo or in a private submodule. It typically contains `AWS_DEFAULT_PROFILE`, `KUBECONFIG` paths pointing to internal clusters, VPN-specific aliases, and internal registry URLs. Keeping these separate also makes onboarding new team members easier: they clone the public dotfiles and manually create their work.zsh.

# In .zshrc - source modular config files
ZSH_CONFIG=$HOME/.zsh

[[ -f $ZSH_CONFIG/path.zsh ]]       && source $ZSH_CONFIG/path.zsh
[[ -f $ZSH_CONFIG/exports.zsh ]]    && source $ZSH_CONFIG/exports.zsh
[[ -f $ZSH_CONFIG/aliases.zsh ]]    && source $ZSH_CONFIG/aliases.zsh
[[ -f $ZSH_CONFIG/functions.zsh ]]  && source $ZSH_CONFIG/functions.zsh
[[ -f $ZSH_CONFIG/completion.zsh ]] && source $ZSH_CONFIG/completion.zsh
[[ -f $ZSH_CONFIG/prompt.zsh ]]     && source $ZSH_CONFIG/prompt.zsh
[[ -f $ZSH_CONFIG/tools.zsh ]]      && source $ZSH_CONFIG/tools.zsh
[[ -f $ZSH_CONFIG/work.zsh ]]       && source $ZSH_CONFIG/work.zsh

Profiling and Diagnosing Slow Startups

When startup time creeps up and you can't identify the source, zsh's built-in profiler finds it exactly. Add `zmodload zsh/zprof` as the very first line of .zshrc and `zprof` as the very last line. Restart a shell and the profiler output shows every function call with time cost.

The output will look like:

``` num calls time self name ----------------------------------------------------------------------------------- 1) 2 180.23 90.12 62.00% 180.23 90.12 62.00% nvm_auto 2) 1 68.44 68.44 23.51% 68.44 68.44 23.51% compinit 3) 1 18.12 18.12 6.23% 18.12 18.12 6.23% _pyenv ```

This immediately shows nvm_auto (the automatic nvm activation) dominating at 62% of startup time. Remove `zmodload zsh/zprof` and `zprof` once you've fixed the problem - they add ~5ms overhead themselves.

A second diagnostic approach: use `zsh -xvs 2>&1 | ts '%.s' | head -100` to trace execution with timestamps. `ts` is from the `moreutils` package (`brew install moreutils`). This shows you exactly which file and line is slow without modifying .zshrc.

# Temporary profiling - add to START of .zshrc
zmodload zsh/zprof

# ... all your config ...

# Temporary profiling - add to END of .zshrc
zprof