Skip to main content
Subscribe via RSS Feed

Search site

 

Search the site

Type at least two characters to search blog posts, talks, and livestreams.

How I Used Claude Code to Speed Up My Shell Startup by 95%

• 7 min read
Table of Contents

My terminal was sluggish. Every time I opened a new tab, there was this annoying delay before I could start typing. I decided to dig into it, and with Claude Code's help, I went from a 770ms startup time to just 40ms. That's a 19x improvement.

The Problem

It wasn't that I accumulated too much tooling. Most things I have in my .zshrc, I needed, but each thing I tacked on added to my shell startup time. I honestly hadn't looked into this and just lived with it. Then, John Lindquist posted this the other day so I figured, let Claude Code speed it up for me.

Measuring the Baseline

First thing I needed to know how bad it actually was:

        
          for i in 1 2 3 4 5; do /usr/bin/time zsh -i -c exit 2>&1; done
        
      

Yikes:

        
          0.94 real
0.71 real
0.74 real
0.73 real
0.72 real
        
      

Average: ~770ms per shell startup. Almost a full second just to open a terminal. Not good.

What Was Slowing Things Down

Claude Code helped me identify the main culprits:

| Tool | Impact | Why It Sucks | |------|--------|--------------| | nvm | ~300-500ms | Loads the entire Node.js environment every time | | pyenv init | ~100-200ms | Python version management initialization | | security command | ~50-100ms | Fetching API key from macOS Keychain | | brew shellenv | ~30-50ms | Runs a subshell to get Homebrew paths | | gcloud completion | ~20-30ms | Google Cloud completions |

The Big Unlock: Lazy Loading

Most of the tools don't need to be loaded until I actually use them. So defer the expensive stuff until the first time I run a command.

Self-Destructing Wrappers to the Rescue

This is the clever part. Thanks Claude Code. The wrapper functions remove themselves after first use:

  1. First call: Wrapper runs, does the slow initialization, then deletes itself
  2. After that: Direct execution, zero overhead

You only pay the cost once per session.

nvm Lazy Loading

Instead of this slow startup code:

        
          # Before: runs every shell startup (~400ms)
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
nvm use default --silent
        
      

now, I use wrapper functions:

        
          # After: only runs when you actually need node/npm/npx
nvm() {
  unset -f nvm node npm npx
  [ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
  [ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"
  nvm "$@"
}
node() { nvm use default --silent; unfunction node npm npx; node "$@"; }
npm() { nvm use default --silent; unfunction node npm npx; npm "$@"; }
npx() { nvm use default --silent; unfunction node npm npx; npx "$@"; }
        
      

Update 2025-11-28: I scrapped [nvm](https://github.com/nvm-sh/nvm) in favour of [fnm](https://github.com/Schniz/fnm), so I've since removed the `nvm`, `node`, `npm` and `npx` wrapper functions. If you do use `nvm`, keep them.

The unset -f and unfunction commands remove the wrapper after first use. After that, it's like the tools were loaded normally.

pyenv Gets the Same Treatment

        
          # Before
eval "$(pyenv init -)"

# After
pyenv() {
  unset -f pyenv
  eval "$(command pyenv init -)"
  pyenv "$@"
}
        
      

First pyenv call takes ~150ms to initialize, then it's direct execution forever.

Google Cloud SDK

        
          gcloud() {
  unset -f gcloud gsutil bq
  [ -f "$HOME/.local/google-cloud-sdk/path.zsh.inc" ] && . "$HOME/.local/google-cloud-sdk/path.zsh.inc"
  [ -f "$HOME/.local/google-cloud-sdk/completion.zsh.inc" ] && . "$HOME/.local/google-cloud-sdk/completion.zsh.inc"
  gcloud "$@"
}
gsutil() { gcloud; gsutil "$@"; }
bq() { gcloud; bq "$@"; }
        
      

Homebrew Caching

Homebrew's environment doesn't change often, so just cache it:

        
          # Before: subshell every time
eval "$(/opt/homebrew/bin/brew shellenv)"

# After: cache to file, only regenerate if brew changes
if [[ ! -f ~/.zsh_brew_cache || ~/.zsh_brew_cache -ot /opt/homebrew/bin/brew ]]; then
  /opt/homebrew/bin/brew shellenv > ~/.zsh_brew_cache
fi
source ~/.zsh_brew_cache
        
      

API Key Laziness

I was hitting the macOS Keychain on every shell startup:

        
          # Before: slow keychain lookup every time
        
      

Since I only need this for npm stuff, I moved it into the npm wrapper:

        
          npm() {
  nvm use default --silent
  unfunction node npm npx
  [ -z "$OPENAI_API_KEY" ] && \
  npm "$@"
}
        
      

Update 2025-11-28: as mentioned above, I switched to fnm, so I changed this up since I removed the npm wrapper function. Now for my OpenAI API key, I just call a function whenever I need it instead:

        
          # === Lazy-load OPENAI_API_KEY ===
openai_key() {
}
        
      

More on the macOS Keychain tip in my newsletter.

Other Quick Wins

I also cleaned up some basic stuff:

  • Combined 7 different PATH modifications into one line
  • Removed duplicate GPG_TTY exports
  • Fixed ordering so STARSHIP_CONFIG gets set before starship init

The Results

After all the changes:

        
          for i in 1 2 3 4 5; do /usr/bin/time zsh -i -c exit 2>&1; done
        
      
        
          0.06 real
0.04 real
0.04 real
0.03 real
0.04 real
        
      

Average: ~40ms

| Before | After | Improvement | |--------|-------|-------------| | 770ms | 40ms | 95% faster |

The "Trade-off", Not Really

Yeah, there's a one-time cost when you first use each tool:

  • First node/npm/npx: +400ms
  • First pyenv: +150ms
  • First gcloud: +50ms

But it's once per terminal session and honestly barely noticeable compared to what the commands actually do.

Try It

If your shell is slow, first measure the total startup time:

        
          time zsh -i -c exit
        
      

If it's over 200ms, you've got room to improve. To see exactly what's slow, profile your .zshrc or whatever shell you're using:

        
          # Add to top of .zshrc
zmodload zsh/zprof

# Add to bottom
zprof
        
      

This breaks down which specific commands are eating up your startup time.

My Updated Shell Configuration File

Here's my updated shell with all the performance tweaks.

        
          # === Early exports (no subshells) ===

openai_key() {
}

# === Cache brew shellenv ===
if [[ ! -f ~/.zsh_brew_cache || ~/.zsh_brew_cache -ot /opt/homebrew/bin/brew ]]; then
  /opt/homebrew/bin/brew shellenv > ~/.zsh_brew_cache
fi
source ~/.zsh_brew_cache

# === Zsh options ===
setopt autocd
autoload -U history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^[[A" history-beginning-search-backward-end
bindkey "^[[B" history-beginning-search-forward-end

# === Prompt ===
eval "$(starship init zsh)"

# === fnm ===
eval "$(fnm env --use-on-cd --shell zsh)"

# === Lazy-load pyenv ===
pyenv() {
  unset -f pyenv
  eval "$(command pyenv init -)"
  pyenv "$@"
}

# Lazy load Cargo - defers initialization until first use
cargo() {
  unset -f cargo rustc rustup
  source $HOME/.cargo/env
  cargo "$@"
}
rustc() {
  unset -f cargo rustc rustup
  source $HOME/.cargo/env
  rustc "$@"
}
rustup() {
  unset -f cargo rustc rustup
  source $HOME/.cargo/env
  rustup "$@"
}

# === Plugins ===
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

# === Atuin ===
. "$HOME/.atuin/bin/env"
eval "$(atuin init zsh --disable-up-arrow)"

# === Lazy-load gcloud ===
gcloud() {
  unset -f gcloud gsutil bq
  [ -f "$HOME/.local/google-cloud-sdk/path.zsh.inc" ] && . "$HOME/.local/google-cloud-sdk/path.zsh.inc"
  [ -f "$HOME/.local/google-cloud-sdk/completion.zsh.inc" ] && . "$HOME/.local/google-cloud-sdk/completion.zsh.inc"
  gcloud "$@"
}
gsutil() { gcloud; gsutil "$@"; }
bq() { gcloud; bq "$@"; }

# === Aliases ===
alias flushdns='sudo dscacheutil -flushcache;sudo killall -HUP mDNSResponder'
alias zshconfig='less ~/.zshrc'
alias nr='npm run'
alias ni='npm i'
alias '$'=''
alias brew='env PATH="${PATH//$(pyenv root)\/shims:/}" brew'
alias dotfiles='/opt/homebrew/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
alias g='git'
alias code='code-insiders'
alias c='cursor -r'
alias p='pnpm'
alias pi='pnpm i'
alias dcu='docker compose up -d'
alias dcd='docker compose down'
alias mermaid='mmdc'
alias sniffly='uvx sniffly init'

# === Functions ===
# checkout a pull request in a git worktree
cpr() {
  pr="$1"
  remote="${2:-origin}"
  branch=$(gh pr view "$pr" --json headRefName -q .headRefName)
  git fetch "$remote" "$branch"
  git worktree add "../$branch" "$branch"
  cd "../$branch" || return
  echo "Switched to new worktree for PR #$pr: $branch"
}

rmmerged() {
  git branch --merged | grep -v "*" | grep -v \"master\" | xargs -n 1 git branch -d && git remote prune origin
}

nb() {
  branch="$1";
  git remote -v | grep -q git@github.com:pomerium/ && git checkout -b "nickytonline/$branch" || git checkout -b $branch;
}

db() {
  branch="$1";
  git remote -v | grep -q git@github.com:pomerium && git branch -D "nickytonline/$branch" || git branch -D $branch;
}

glog() {
  git log --oneline --decorate --graph --color | less -R
}
        
      

Wrapping Up

Dev tooling adds up fast and it's easy to not notice the death by a thousand paper cuts. This lazy loading pattern fixes it without any real downsides.

Big ups to John L. and Claude Code for helping me figure out the bottlenecks and solutions.

Note: The performance impact numbers and comparison table were generated with Claude Code's help. Mileage may vary depending on your specific setup. 😅

If you want to stay in touch, all my socials are on nickyt.online.

Until the next one!

Photo by <a href="https://unsplash.com/@marcsm?utmsource=unsplash&utmmedium=referral&utmcontent=creditCopyText">Marc Sendra Martorell</a> on <a href="https://unsplash.com/photos/time-lapsed-of-street-lights--Vqn2WrfxTQ?utmsource=unsplash&utmmedium=referral&utmcontent=creditCopyText">Unsplash</a>

Start