zsh
Zsh
Installation#
Usage#
The git plugin is left out — it defines 197 aliases in one go where only a handful ever get
used, so those are written by hand instead, see the git page. Same for zsh-z: the i
function covers the jumping.
Aliases#
la is the only ls alias kept identical across machines. Git ones live on the git page.
Ubuntu's .bashrc ships la='ls -A' (hidden only, short format), which means something else.
Edit it in place — comment out the distro default and put the new value right below:
Appending at the end of the file works too (the last definition wins), but editing in place is what makes the change visible — otherwise the next reader assumes the distro default is still in effect.
ll and l are deliberately left as each distro ships them. oh-my-zsh gives
ll='ls -lh' and l='ls -lah'; Ubuntu gives ll='ls -alF' and l='ls -CF' — genuinely
inconsistent, but with la being the only one actually typed, aligning them buys nothing.
Cross-machine consistency exists so that switching machines never surprises you, and that
only happens on commands you reach for; aligning unused aliases helps no one and the change
itself is noise.
oh-my-zsh defines la already, so there is nothing to add where it is installed.
Load Order#
path_helper reorders PATH#
macOS ships this in /etc/zprofile:
It moves everything from /etc/paths and /etc/paths.d/* to the front, pushing the user
directories set in ~/.zshenv behind /usr/bin:
So on macOS, relying on ~/.zshenv for PATH priority does not hold. .zshenv only
guarantees that scripts can find things; the actual priority has to be re-established after
path_helper runs.
Non-interactive login shells degrade silently#
If the priority lives only in .zshrc, the same command resolves differently depending on the
shell — because .zshrc is never read non-interactively:
ssh host 'command', LaunchAgents and CI all take the latter path. GNU tar and bsdtar differ on
--wildcards and --transform, so a command that works interactively can fail once it lands in
a script.
The fix is to put the priority block in ~/.zprofile — it runs after path_helper, and both
interactive and non-interactive login shells read it:
To cover zsh -c scripts as well, add the same entries to ~/.zshenv (typeset -U dedupes).
The mise shims belong here for the same reason: mise activate lives in .zshrc and never runs
non-interactively. Shims resolve the version for the current directory themselves, so per-project
switching still works without activation. See the mise page.
Command Shadowing#
When the same executable name exists in several PATH directories, only the first one wins. To list every duplicate:
Count executable files only — directory symlinks (such as gnu-tar's gnuman) also carry the
execute bit and produce false positives.
uv's python and pip must be linked together#
If ~/.local/bin holds only python/python3, then pip3 falls through to Homebrew, and
packages installed by pip3 install are invisible to python3:
uv's python directory already ships pip; just add the links:
Upstreams compete for the same command name#
Cursor's CLI binary is literally called agent and installs into ~/.local/bin; the Grok
installer creates both grok and agent in ~/.grok/bin as symlinks to the same binary. Both
claim agent, and whichever comes first on PATH wins.
Prefer keeping the one that has only that name — Cursor's agent is gone if shadowed, while
Grok's agent is merely an alias for grok and costs nothing to lose.
Keep Secrets Out of the Environment#
exporting a private key from .zshrc makes it readable by every child process — npm
postinstall scripts, CLI crash reporters and agent env dumps all carry it along. Inject it on
demand instead, so the key only exists for the duration of the wrapped command:
Keep the key files at chmod 600 and the directory at chmod 700.