Oh My Zsh on macOS: A Reference for a Clean, Maintainable Shell
If you spend a large part of your day in a terminal, your shell stops being just a shell and starts becoming part of your development environment. On my Mac, that environment is built around Zsh, Oh My Zsh, the Spaceship prompt, and a small set of plugins that improve the things I do constantly: Git, GitHub, containers, Kubernetes, Terraform, Python, Go, and Ansible. The result is not flashy for the sake of being flashy. It is a shell that surfaces useful context quickly, stays out of the way when I am focused, and is still simple enough to maintain without turning ~/.zshrc into a junk drawer.
Why Start with Oh My Zsh?¶
Zsh is already the default interactive shell on modern macOS, so the question is not whether you need to install a shell first. The question is how much behavior you want to build from scratch.
You can absolutely run plain Zsh with a hand-rolled configuration. In fact, understanding core Zsh features is worth your time. But starting from bare Zsh means you also need to decide how to structure completions, aliases, prompt logic, history behavior, plugin loading, key bindings, and update workflows. None of that is impossible, but it is a lot of incidental work for something that should be helping you get real work done.
Oh My Zsh gives you a sane baseline:
- A well-known plugin system
- A large set of maintained shell plugins
- Built-in completion and helper behavior
- Sensible defaults for many common workflows
- A configuration structure most terminal users will recognize immediately
That last point matters more than people admit. A recognizable shell setup is easier to debug, easier to share, and easier to revisit six months later when you need to remember why something works the way it does.
The Three Layers of the Setup¶
The easiest way to reason about an Oh My Zsh configuration is to separate it into three layers:
- Shell behavior: history settings,
PATH, completions, key bindings, and shell options. - Framework behavior: Oh My Zsh itself, plus the plugins it loads.
- Prompt behavior: the theme or prompt engine, which in my case is Spaceship.
When those concerns get mixed together, ~/.zshrc becomes hard to read. When they are kept separate, the file tends to stay understandable.
How the Pieces Fit Together
flowchart TD
terminal["macOS Terminal<br/>Ghostty, iTerm2, Terminal"] --> zsh["Zsh"]
zsh --> rc["~/.zshrc"]
rc --> shell["Shell Behavior<br/>PATH, fpath, history, key bindings"]
rc --> omz["Oh My Zsh"]
rc --> shiprc["~/.spaceshiprc.zsh"]
omz --> plugins["Plugins<br/>git, gh, docker, terraform,<br/>golang, python, ansible, kubectl"]
omz --> theme["Theme Loader"]
theme --> spaceship["Spaceship Prompt"]
shiprc --> spaceship
plugins --> aliases["Aliases and Helpers"]
plugins --> completions["CLI-aware Completions"]
spaceship --> prompt["Prompt Context<br/>Git, runtimes, Kubernetes,<br/>Terraform, exit status"]
aliases --> experience["Daily Shell Experience"]
completions --> experience
prompt --> experience What Oh My Zsh Actually Does¶
A lot of descriptions of Oh My Zsh stop at "it makes your terminal nicer." That is true, but it is not specific enough to be useful.
In practice, Oh My Zsh gives you:
- Plugin loading: it sources plugins from a defined plugin list
- Completion integration: it prepares shell completion behavior and plugin-specific helpers
- Aliases and wrapper functions: many plugins add shortcuts around common tools
- Theme loading: it initializes the configured prompt theme
- Update mechanics: it can self-update and notify you about changes
That means Oh My Zsh is not just a prompt package. It is a framework sitting on top of Zsh. If a completion breaks, a plugin alias behaves unexpectedly, or startup gets slow, you are often debugging framework behavior as much as shell behavior.
Tip
If you ever need to debug a shell issue, temporarily remove all plugins except git, start a fresh shell, and add plugins back one at a time. Most "Zsh is broken" moments are really "one plugin or completion script is broken" moments.
Installing Oh My Zsh on macOS¶
The standard install method is the project-provided bootstrap script, and the official Oh My Zsh install documentation is worth checking first so you can confirm the current guidance before running it:
That clones the framework into ~/.oh-my-zsh, creates a starter ~/.zshrc if you do not already have one, and switches the default shell to Zsh if needed.
On modern macOS, the shell is already Zsh, so the main value is the framework installation and the initial configuration scaffold.
After installation, the core settings typically look like this:
That is enough to get started, but it is not where most long-term setups stay.
The Plugins I Actually Find Useful¶
Oh My Zsh ships with a huge plugin catalog. That does not mean you should enable a huge number of plugins.
In my setup, the useful set is focused and practical:
| Plugin | Best for | Adds primarily |
|---|---|---|
git | Repository workflow | Aliases and helpers |
gh | GitHub CLI workflow | CLI-aware completions |
docker | Containers and images | Aliases and completions |
terraform | Infrastructure as code | Aliases and prompt helpers |
golang | Go development | Shell helpers |
python | Python projects | Shell ergonomics |
ansible | Automation | Aliases and completions |
kubectl | Cluster operations | Aliases, helpers, and completions |
Here is why each one earns its place.
git¶
This is the baseline plugin almost everyone enables, and for good reason. It adds a large set of aliases and helpers around the command I use most in the terminal.
Useful examples include:
gstforgit statusglforgit pullgpforgit pushgcmsgforgit commit -m
Even if you do not use every alias, the plugin reduces a lot of repetitive typing.
gh¶
If you use GitHub heavily, the gh plugin is worth enabling. More precisely, it wires Oh My Zsh into the installed GitHub CLI and registers cached completions for it. That makes the GitHub CLI feel like a first-class terminal tool rather than an external binary you need to memorize manually.
That matters more than it sounds. Good completions make infrequently used subcommands discoverable.
docker¶
The Docker plugin adds aliases and, when Docker is installed, completion support that is genuinely helpful when you are regularly building images, inspecting containers, and cleaning up local state.
It is also a good example of how shell plugins improve ergonomics without changing the underlying tool. You still need to know Docker. You just stop fighting your keyboard quite so much.
terraform¶
Terraform has enough subcommands, flags, and workspace-related operations that completion support is a real quality-of-life improvement. The plugin does not replace understanding the CLI, but it does reduce mistakes and speed up routine operations.
golang¶
I keep this enabled because Go tooling is part of my normal workflow. The plugin adds useful helpers and works well alongside a version manager. If you are using something like govm, the plugin is not the version manager. It is the shell ergonomics layer on top of the Go toolchain.
python¶
This one is especially helpful if you bounce between virtual environments or work across multiple Python repositories. The plugin is lightweight, but the convenience adds up.
ansible¶
Ansible has enough module names, flags, and inventory-related commands that shell-level aliases and completion support are worth having. If you automate infrastructure regularly, it is an easy win.
kubectl¶
This plugin tends to pay for itself quickly. Kubernetes commands are verbose, context-sensitive, and easy to mistype. The plugin registers completions against the installed kubectl binary and adds a long list of aliases and shell helpers, all of which are useful here.
Note
More plugins is not the same thing as a better shell. Every plugin adds startup work, completion behavior, aliases, and potential interaction effects. Enable plugins you actually use, not plugins that merely sound impressive.
Why I Use Spaceship for the Prompt¶
Prompt design is one of those things people either obsess over or dismiss entirely. I think the right middle ground is this: the prompt should answer useful questions at a glance and then get out of the way.
Spaceship is good at that because it is modular. Rather than hard-coding one giant prompt string, it assembles context from small prompt sections, such as:
- Current directory
- Git branch and status
- Exit status
- Active language runtime
- Kubernetes context
- Docker context
- Terraform workspace
- Virtual environment
That makes it a particularly good fit for a development machine where the context changes constantly from repository to repository.
For example, when I am in a Git repository, I care about branch and working tree state. When I am in a Python project, I care about the virtual environment. When I am in infrastructure code, Terraform and Kubernetes context become relevant. Spaceship is good at making those transitions feel natural without requiring a completely different prompt per toolchain.
Installing Spaceship on macOS¶
There are two clean ways to use Spaceship on macOS, and it helps to keep them distinct:
- Install it with Homebrew and source it directly.
- Install it as an Oh My Zsh theme and select it with
ZSH_THEME="spaceship".
Both work well, but they are not the same setup.
If you use Homebrew on Apple Silicon, the simplest direct-source install path is:
Then source the prompt in your ~/.zshrc:
If you are on an Intel Mac, Homebrew is usually rooted under /usr/local instead of /opt/homebrew, which is why using $(brew --prefix) is safer than hard-coding the path.
If you want Spaceship to be loaded by Oh My Zsh itself, install the theme into your Oh My Zsh custom themes directory and then select it as the active theme:
git clone https://github.com/spaceship-prompt/spaceship-prompt.git \
"$ZSH_CUSTOM/themes/spaceship-prompt" --depth=1
ln -s "$ZSH_CUSTOM/themes/spaceship-prompt/spaceship.zsh-theme" \
"$ZSH_CUSTOM/themes/spaceship.zsh-theme"
Then set:
This is the mode I use with Oh My Zsh. It keeps prompt loading inside the framework's normal theme path instead of mixing manual source logic into the same startup path.
Note
brew install spaceship by itself does not make ZSH_THEME="spaceship" work in Oh My Zsh. If you want Oh My Zsh theme mode, make sure the spaceship.zsh-theme file is present in $ZSH_CUSTOM/themes or linked there.
Spaceship Prompt Design: Show Context, Not Noise¶
A good prompt gives you context you would otherwise need to ask for manually. A bad prompt is a wall of status segments that turns every command line into a dashboard.
Spaceship works best when you keep the visible context intentional.
For many macOS development workflows, the most useful prompt sections are:
- Directory
- Git branch and dirty state
- Exit status when the previous command failed
- Active Python virtual environment
- Go version when relevant
- Kubernetes context for cluster work
- Terraform workspace when working in infrastructure repositories
The common thread is simple: show context that changes your next command.
If a prompt segment never affects what you do, it is probably just decoration.
Here is a concrete .spaceshiprc.zsh example that matches that philosophy:
~/.spaceshiprc.zsh
SPACESHIP_PROMPT_ORDER=(
dir
git
golang
python
venv
kubectl_context
terraform
line_sep
jobs
exit_code
char
)
SPACESHIP_DIR_TRUNC=4
SPACESHIP_GIT_SHOW=true
SPACESHIP_EXIT_CODE_SHOW=true
SPACESHIP_EXEC_TIME_SHOW=false
SPACESHIP_NODE_SHOW=false
SPACESHIP_PACKAGE_SHOW=false
SPACESHIP_DOCKER_SHOW=false
SPACESHIP_TIME_SHOW=false
This keeps the prompt focused on repository state, active runtime context, and infrastructure context while leaving out sections that tend to add visual noise on every single command.
Tip
If you are customizing Spaceship for the first time, start by disabling sections you do not need rather than trying to style every section at once. Fewer prompt segments usually produce a better prompt faster.
A Maintainable ~/.zshrc Structure¶
The most common shell configuration failure mode is not one broken command. It is gradual drift. Over time, installation scripts append lines, prompt packages add sourcing instructions, plugin docs recommend extra snippets, and before long the file has duplicate initialization paths, conflicting completion setup, and no clear loading order.
What helps is a stable layout with a small number of predictable sections:
- Early guard for interactive shells
- Environment variables
- History and shell options
PATHand completion paths- Aliases and helper scripts
- Oh My Zsh and plugin loading
- Prompt configuration
- Custom functions
- Tool-specific extras
Here is a compact example that reflects the shape of my current setup on macOS:
~/.zshrc
# Only run for interactive shells
[[ $- != *i* ]] && return
export LANG="en_US.UTF-8"
export ANSIBLE_NOCOWS=1
export ANSIBLE_FORCE_COLOR=true
export PYTHONDONTWRITEBYTECODE=1
export PYTHONUNBUFFERED=1
export HISTSIZE=100000
export HISTFILESIZE="$HISTSIZE"
setopt EXTENDED_HISTORY
setopt SHARE_HISTORY
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_FIND_NO_DUPS
setopt HIST_SAVE_NO_DUPS
setopt AUTO_CD
setopt INTERACTIVE_COMMENTS
: "${GOPATH:=$HOME/go}"
export GOPATH
if [[ -x /usr/libexec/path_helper ]]; then
eval "$(/usr/libexec/path_helper -s)"
fi
typeset -U path fpath
path=(
"$HOME/.govm/shim"
"$GOPATH/bin"
/opt/homebrew/bin
/opt/homebrew/sbin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
/Library/Apple/usr/bin
"${path[@]}"
)
fpath=(
/opt/homebrew/share/zsh/site-functions
/usr/local/share/zsh/site-functions
/usr/share/zsh/site-functions
"/usr/share/zsh/${ZSH_VERSION}/functions"
"${fpath[@]}"
)
[[ -d "$HOME/.docker/completions" ]] && fpath=("$HOME/.docker/completions" "${fpath[@]}")
[[ -f "$HOME/.zsh_aliases" ]] && source "$HOME/.zsh_aliases"
[[ -f "$HOME/.spaceshiprc.zsh" ]] && source "$HOME/.spaceshiprc.zsh"
export ZSH="$HOME/.oh-my-zsh"
export COMPLETION_WAITING_DOTS="true"
export ENABLE_CORRECTION="false"
plugins=(git gh docker terraform golang python ansible kubectl)
ZSH_THEME="spaceship"
source "$ZSH/oh-my-zsh.sh"
The specific tools can change, but the structure is what makes the file maintainable.
Why PATH and fpath Deserve Extra Attention¶
Most shell breakages that look mysterious are not mysterious at all. They are usually caused by one of three things:
PATHno longer includes system command directoriesfpathno longer includes Zsh function directoriescompinitruns multiple times with conflicting assumptions
PATH controls how your shell locates commands like git, mkdir, python, and kubectl. fpath controls how Zsh locates autoloaded functions, including completion internals.
If PATH is wrong, normal commands disappear. If fpath is wrong, completions and shell functions fail in strange ways.
That is why I prefer being explicit about both.
What fpath is for
In Zsh, completion functions such as _git, _kubectl, and core helpers used by compinit are loaded from directories listed in fpath. If those directories are missing, completion setup may fail even when the corresponding binaries are installed correctly.
Completion Setup: Avoid Duplicates¶
One easy mistake in a long-lived ~/.zshrc is running compinit more than once.
This usually happens because:
- Oh My Zsh initializes completion support
- Docker Desktop or another tool appends its own
compinitlines - A blog post or old dotfile snippet adds a second manual
compinitblock
The result is usually wasted startup time, occasionally broken completion caches, and sometimes odd "function definition file not found" errors.
If you use Oh My Zsh, let it own completion setup unless you have a specific reason not to. Add completion directories to fpath, but avoid reinitializing completion logic unnecessarily.
Avoid duplicate completion initialization
If Oh My Zsh is already managing completions, do not add extra autoload -Uz compinit and compinit blocks unless you understand exactly why they are needed. Repeated completion initialization is one of the quickest ways to make a shell feel flaky.
Practical macOS Considerations¶
macOS introduces a few shell-specific details worth calling out.
Homebrew path differences¶
On Apple Silicon:
On Intel:
If you copy terminal setup guides blindly, this is one of the easiest places to end up with broken command resolution.
path_helper¶
macOS includes /usr/libexec/path_helper, which reads system path configuration and produces a normalized shell PATH. Running it near the top of your shell config is a good way to avoid fighting the platform.
Terminal-specific integrations¶
Many tools now append shell integration hooks automatically: terminal emulators, Docker Desktop, version managers, prompt engines, IDEs, AI assistants, and cloud CLIs all want a few lines in your shell startup files.
That is not inherently bad. The bad part is when those additions accumulate without review.
A good habit is to periodically audit ~/.zshrc and ask:
- Does this tool still exist on this machine?
- Is this block duplicating behavior another tool already handles?
- Can this live in a separate sourced file?
- Is this running on every shell startup when it only needs to run sometimes?
What I Would Not Add¶
There is a temptation to turn an Oh My Zsh setup into a museum of clever shell features. I try to resist that.
I would avoid:
- Dozens of plugins "just in case"
- Aggressive prompt customization that buries the command line
- Repeated
evalblocks copied from install instructions without review - Multiple overlapping version managers for the same language
- Huge alias files full of one-off shortcuts you never remember
The best shell configuration is not the most elaborate one. It is the one that keeps helping you a year later.
Troubleshooting Checklist¶
When an Oh My Zsh setup goes sideways on macOS, this is the checklist I work through:
- Confirm
PATHincludes/usr/bin,/bin, and your Homebrew prefix. - Confirm
fpathincludes system Zsh function directories. - Check whether
compinitis being called more than once. - Temporarily reduce
plugins=(git)and test again. - Start a clean shell with
zsh -fto distinguish framework issues from shell issues. - Re-source
~/.zshrcafter edits instead of stacking terminal tabs full of different states.
That process is not glamorous, but it is fast, and it usually identifies the problem quickly.
Quick Reference¶
Oh My Zsh Syntax Reference¶
If you only remember one thing about Oh My Zsh configuration, remember this: most of the setup is just a handful of variables plus one source line.
| Setting | Purpose | Example |
|---|---|---|
ZSH | Points to the Oh My Zsh installation directory | export ZSH="$HOME/.oh-my-zsh" |
plugins=(...) | Lists plugins to load | plugins=(git gh docker kubectl) |
ZSH_THEME | Selects the active theme | ZSH_THEME="spaceship" |
source "$ZSH/oh-my-zsh.sh" | Loads the framework | source "$ZSH/oh-my-zsh.sh" |
ZSH_CUSTOM | Optional custom directory for user overrides | export ZSH_CUSTOM="$HOME/.oh-my-zsh/custom" |
The minimum working structure looks like this:
Here are the pieces that are useful to know by memory:
plugins=(git gh docker)is Zsh array syntax, not a quoted string. Separate plugin names with spaces, not commas.ZSH_THEME="spaceship"tells Oh My Zsh which theme file to load.source "$ZSH/oh-my-zsh.sh"must come after your main Oh My Zsh variables are set.$ZSH_CUSTOMis where custom plugins, themes, and overrides usually live.
Common Oh My Zsh Paths
Comma mistake to avoid
This is correct:
This is not:
Oh My Zsh expects a normal Zsh array of plugin names.
Useful omz Commands¶
Oh My Zsh also ships with an omz command for day-to-day framework management. These are the subcommands I actually find worth remembering:
| Command | What it does |
|---|---|
omz version | Shows the installed Oh My Zsh version and current commit |
omz update | Updates Oh My Zsh itself |
omz reload | Reloads the current Zsh session |
omz plugin list | Lists available plugins |
omz plugin list --enabled | Lists only enabled plugins |
omz plugin info git | Shows information about a specific plugin |
omz plugin enable kubectl | Enables a plugin in your .zshrc |
omz plugin disable kubectl | Disables a plugin in your .zshrc |
omz theme list | Lists available themes |
omz theme set spaceship | Sets the theme in your .zshrc |
omz theme use spaceship | Loads a theme for the current session |
# Check the installed framework version
omz version
# Update Oh My Zsh
omz update
# See which plugins are enabled right now
omz plugin list --enabled
# Switch themes
omz theme list
omz theme set spaceship
# Reload the current session
omz reload
Note
omz reload is the Oh My Zsh built-in reload command. If you also define your own shell helper such as reload-zsh or alias reload='source ~/.zshrc', that is your custom layer, not an Oh My Zsh built-in.
Oh My Zsh solves a very real problem well: most people want a better interactive shell, not a shell configuration hobby project. Pair it with a prompt like Spaceship and a disciplined plugin list, and you get a terminal that feels both expressive and practical.
That is really the goal on my Mac. I want the shell to tell me what matters, help me move faster, and stay easy to reason about when something inevitably needs to be adjusted.
If your own ~/.zshrc is growing messy, the fix usually is not starting over completely. It is simplifying the load order, trimming duplicate setup, and being more intentional about what your shell is allowed to do on startup.