Self-hosted from my home lab. It's offline right now, so you're viewing a snapshot. Check back soon for the live site.
All posts
Programming Tips

My zsh Terminal Setup for Faster Development

Ten small tools and one shell hook that save me time every day, including switching GitHub accounts by folder.

I spend most of my working day in a terminal, so every small annoyance adds up. This is the setup I run on my Mac: 53 lines of .zshrc, ten tools from Homebrew, and a shell that still starts in about a tenth of a second.

TL;DR

  • Three zsh plugins give you suggestions as you type, colours for typos, and history search with the arrow keys.
  • fzf, zoxide and starship make finding files, jumping between folders and reading the prompt fast.
  • eza and bat replace ls and cat with versions that are easier to read.
  • A million lines of shared history, with timestamps, so nothing you typed last month is lost.
  • A small hook switches my GitHub account automatically depending on which folder I'm in.

The problem

Out of the box, zsh on macOS does very little:

  • You retype long commands because nothing suggests them.
  • A typo in a command only shows up after you press Enter.
  • The Up arrow walks through every command, not the ones that start with what you typed.
  • History is saved when a tab closes, so two open tabs don't see each other's commands.
  • cd into a project means typing or tab-completing the full path every time.

And one problem that cost me real time: I have two GitHub accounts, one for my own work and one for a side project. The GitHub CLI only has one active account at a time, so I kept opening pull requests from the wrong one.

Why it happened

zsh is a toolbox, not a finished product. Its defaults are minimal on purpose and expect you to add what you need. History is a good example: the default size is small, and it's written when the shell exits, not after each command.

The GitHub problem is similar. gh remembers several accounts, but it has one global "active" account. It has no idea that a folder belongs to a different one, so you have to remember to switch. I didn't always remember.

How I fixed it

Instead of a big framework like Oh My Zsh, I install a handful of focused tools with Homebrew and wire them up myself. Each one does one job, the whole config fits on one screen, and startup stays fast because nothing loads that I don't use.

For GitHub, I let the shell do the remembering. zsh can run a function every time you change directory, so a short hook checks where I am and hands gh the right account's token.

Steps

  1. Install everything with Homebrew.

    brew install zsh-completions zsh-autosuggestions zsh-syntax-highlighting \
      zsh-history-substring-search fzf zoxide starship eza bat gh
    
  2. Load completions and suggestions. Add this to ~/.zshrc:

    BREW_PREFIX=$(brew --prefix)   # ask Homebrew once, not once per plugin
    
    FPATH=$BREW_PREFIX/share/zsh-completions:$FPATH
    autoload -Uz compinit && compinit
    
    source $BREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh
    

    Open a new tab and start typing a command you've used before. A grey suggestion appears; press the Right arrow to accept it.

  3. Add fuzzy search, smart cd and the prompt, then the two order-sensitive plugins at the end.

    source <(fzf --zsh)
    eval "$(zoxide init zsh)"
    eval "$(starship init zsh)"
    
    # These two go last, in this order
    source $BREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
    source $BREW_PREFIX/share/zsh-history-substring-search/zsh-history-substring-search.zsh
    bindkey '^[[A' history-substring-search-up
    bindkey '^[[B' history-substring-search-down
    
    • Ctrl+R opens a fuzzy search over your history, Ctrl+T over files. (fzf --zsh needs fzf 0.48 or newer.)
    • After visiting a folder once, z portfolio jumps straight back to it from anywhere.
    • Starship shows the Git branch, status and language versions in the prompt, with no config needed.
    • Type git and press Up: you only see your git commands.

    The order of those two matters. When zsh-history-substring-search loads, it checks whether syntax highlighting is already there and hooks into it, so its README asks for syntax highlighting first. zsh-syntax-highlighting's own README asks to come after anything that adds key bindings: on older versions it only covers shortcuts that exist when it loads. On zsh 5.9 with plugin version 0.8 it hooks every redraw instead, so that part matters less, but the order above suits both.

  4. Swap in friendlier ls and cat.

    alias ls="eza --icons --group-directories-first"
    alias ll="eza -la --icons --git --group-directories-first"
    alias tree="eza --tree --icons --level=2 --ignore-glob='node_modules|.git'"
    alias cat="bat --paging=never"
    

    ll shows each file's Git status next to it, and cat now highlights syntax.

  5. Keep all your history, shared across tabs.

    HISTFILE=~/.zsh_history
    HISTSIZE=1000000
    SAVEHIST=1000000
    setopt EXTENDED_HISTORY    # save when each command ran and how long it took
    setopt SHARE_HISTORY       # every tab sees every other tab's commands
    setopt HIST_IGNORE_SPACE   # start a command with a space to keep it out of history
    setopt HIST_VERIFY         # show what !! expands to before running it
    setopt HIST_REDUCE_BLANKS
    setopt HIST_IGNORE_DUPS
    
    alias h="history -i 1"        # full history with timestamps
    alias hg="history -i 1 | grep" # e.g. hg docker
    

    HIST_IGNORE_SPACE is the one to remember: if you ever have to type a password or token on the command line, put a space in front and it's never written to disk.

  6. Switch GitHub accounts by folder. Log in to both accounts once with gh auth login. Then add:

    _gh_account_by_dir() {
      if [[ "$PWD/" == "$HOME/work/side-project/"* ]]; then
        if [[ -z "$_GH_SIDE" ]]; then
          local token
          token="$(gh auth token --hostname github.com --user second-account 2>/dev/null)"
          if [[ -n "$token" ]]; then
            export GH_TOKEN="$token" _GH_SIDE=1
          else
            print -u2 "gh: second-account isn't logged in here. Run: gh auth login"
          fi
        fi
      elif [[ -n "$_GH_SIDE" ]]; then
        unset GH_TOKEN _GH_SIDE
      fi
    }
    autoload -Uz add-zsh-hook
    add-zsh-hook chpwd _gh_account_by_dir
    _gh_account_by_dir
    

    Change side-project and second-account to your own folder and account. The token isn't stored in .zshrc: it's read from gh's own secure storage when you enter the folder. The last line runs the check once at startup, so a new tab opened inside the folder is right too.

    Two details make it safe. If the second account isn't logged in, the hook says so instead of quietly carrying on with your main account, and it tries again on the next cd. And the trailing / in "$PWD/" means a folder that only starts with the same name, like side-project-old, doesn't count.

    Check it worked:

    cd ~/work/side-project && gh api user --jq .login   # second-account
    cd ~ && gh api user --jq .login                      # your main account
    

How it works

zsh fires a chpwd event every time the current directory changes. The hook only does work when you cross into or out of the side-project folder; moving around inside it costs nothing.

sequenceDiagram
  participant You
  participant zsh
  participant Hook as _gh_account_by_dir
  participant gh as GitHub CLI
  You->>zsh: cd ~/work/side-project/api
  zsh->>Hook: chpwd event
  Hook->>Hook: inside side-project, not switched yet
  Hook->>gh: gh auth token --user second-account
  gh-->>Hook: token from secure storage
  Hook->>zsh: export GH_TOKEN
  You->>gh: gh pr create
  gh-->>You: PR opened as second-account
  You->>zsh: cd ~/work/portfolio
  zsh->>Hook: chpwd event
  Hook->>zsh: unset GH_TOKEN
  You->>gh: gh pr create
  gh-->>You: PR opened as your main account

gh always prefers GH_TOKEN over its active account when the variable is set, which is what makes this work without ever running gh auth switch.

How to avoid it next time

  • Comment every line. Mine are commented in place, so six months later I still know why each option is there.
  • Watch startup time. Run time zsh -i -c exit after adding anything. Mine takes about 0.1 seconds; if yours creeps past half a second, something you added is slow.
  • Keep secrets out of dotfiles. Read tokens from a secure store, like the gh auth token call above, instead of pasting them into .zshrc.
  • Let the shell remember context for you. If you keep forgetting to switch something per project, a chpwd hook is often a ten-line fix.

Tools used

  • Homebrew: installs everything below
  • zsh-autosuggestions: grey suggestions from your history as you type
  • zsh-syntax-highlighting: colours commands so typos stand out before you press Enter (load it near the end)
  • zsh-history-substring-search: Up and Down search history for what you've typed so far (load it right after syntax highlighting)
  • zsh-completions: extra Tab completions for common tools
  • fzf: fuzzy search for history (Ctrl+R) and files (Ctrl+T)
  • zoxide: jump to folders you've visited with z name
  • Starship: a fast prompt showing Git and language info
  • eza: ls with icons, colours and Git status
  • bat: cat with syntax highlighting
  • GitHub CLI: gh, which the account-switching hook drives
Was this useful?
#terminal#zsh#productivity#github

Comments

Comments and votes open again when the site is back online.