---
title: "Comprehensive Zsh Completions Setup & Troubleshooting on macOS"
description: "A deep dive into fixing broken zsh tab completions with Oh My Zsh, and how I turned the solution into a Claude Code skill. Learn why fpath order matters, how compinit timing breaks completions, and the pattern of encoding debugging knowledge for AI assistants."
date: 2026-02-04T00:00:00.000Z
author: "Kobi Kadosh"
url: https://kobi.kadosh.me/blog/2026-02-04-comprehensive-zsh-completions-troubleshooting
category: "Software Engineering"
tags: ["Developer Experience", "CLI", "Zsh", "Shell", "macOS", "Homebrew", "Troubleshooting", "Claude Code", "AI Tools"]
---

# Comprehensive Zsh Completions Setup & Troubleshooting on macOS

## The Moment Everything Clicked

I'd been fighting with my shell completions for weeks. `brew <TAB>` showed nothing. `rustup <TAB>` was silent. `cargo <TAB>`—the tool I use dozens of times daily—gave me nothing but a blinking cursor.

I tried the usual fixes: reinstalling Oh My Zsh, adding the zsh-completions plugin, running `compinit` manually. Nothing worked. Every Stack Overflow answer led to the same vague advice: "just add it to your fpath."

Then I discovered the root cause, and it was so simple I almost laughed.

**The problem wasn't what I was adding. It was when I was adding it.**

---

## The Core Problem: Timing Matters

Here's what I had in my `.zshrc`:

```bash
# My broken configuration
source $ZSH/oh-my-zsh.sh
fpath=("$(brew --prefix)/share/zsh/site-functions" $fpath)  # Wrong!
```

And here's why it didn't work:

Oh My Zsh calls `compinit` automatically when `oh-my-zsh.sh` is sourced. `compinit` is the function that scans your `fpath` directories and caches all completion functions. Any `fpath` modifications **after** this line are invisible to the completion system because `compinit` has already scanned and cached the paths.

It's like adding books to a library catalog after the index has been printed. The books are there, but no one can find them.

The fix is embarrassingly simple:

```bash
# The correct configuration
fpath=("$(brew --prefix)/share/zsh/site-functions" $fpath)  # First!
source $ZSH/oh-my-zsh.sh  # Then compinit sees it
```

**Fpath first, then source Oh My Zsh.**

---

## The Complete Fix: A Step-by-Step Guide

### Step 1: Fix Your \~/.zshrc Configuration

Open your `.zshrc` and ensure your `fpath` modifications come **before** the Oh My Zsh source line:

```bash
# Path to Oh My Zsh
export ZSH="$HOME/.oh-my-zsh"

# Theme
ZSH_THEME="robbyrussell"

# Plugins
plugins=(git brew docker 1password zsh-autosuggestions fast-syntax-highlighting)

# === COMPLETIONS (BEFORE oh-my-zsh.sh) ===

# zsh-completions plugin (200+ additional completions)
fpath+=${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions/src

# Homebrew completions (Apple Silicon + Intel compatible)
if type brew &>/dev/null; then
  fpath=("$(brew --prefix)/share/zsh/site-functions" $fpath)
fi

# Custom user completions (optional)
# fpath=(~/.zsh/completions $fpath)

# === LOAD OH MY ZSH ===
source $ZSH/oh-my-zsh.sh

# === POST-LOAD INITIALIZATION ===
eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
eval "$(atuin init zsh)"

export PATH="$HOME/.local/bin:$PATH"
```

### Step 2: Remove Manual compinit Calls

If you have a line like this anywhere in your `.zshrc`, **delete it**:

```bash
# DELETE THIS LINE if present
autoload -U compinit && compinit
```

Oh My Zsh handles `compinit` automatically. Having multiple calls causes confusion and can slow down shell startup.

### Step 3: Rebuild the Completion Cache

After making changes to your `.zshrc`, you need to rebuild the completion cache:

```bash
rm -f ~/.zcompdump* && exec zsh
```

This removes all cached completion data and starts a fresh shell session.

---

## The Hidden Treasure: Tools That Generate Their Own Completions

Here's something I didn't know until I dug into this: many modern CLI tools can generate their own zsh completions, but they don't install them automatically. You have to ask.

### Tools with Built-in Completion Generation

| Tool     | Command                                                                                | Category                  |
| -------- | -------------------------------------------------------------------------------------- | ------------------------- |
| rustup   | `rustup completions zsh rustup > $(brew --prefix)/share/zsh/site-functions/_rustup`    | Rust toolchain            |
| cargo    | `rustup completions zsh cargo > $(brew --prefix)/share/zsh/site-functions/_cargo`      | Rust packages             |
| starship | `starship completions zsh > $(brew --prefix)/share/zsh/site-functions/_starship`       | Shell prompt              |
| uv       | `uv generate-shell-completion zsh > $(brew --prefix)/share/zsh/site-functions/_uv`     | Python packages           |
| atuin    | `atuin gen-completions --shell zsh > $(brew --prefix)/share/zsh/site-functions/_atuin` | Shell history             |
| op       | `op completion zsh > $(brew --prefix)/share/zsh/site-functions/_op`                    | 1Password CLI             |
| fclones  | `fclones complete zsh > $(brew --prefix)/share/zsh/site-functions/_fclones`            | Duplicate finder          |
| rclone   | `rclone completion zsh - > $(brew --prefix)/share/zsh/site-functions/_rclone`          | Cloud sync (note the `-`) |
| gh       | `gh completion -s zsh > $(brew --prefix)/share/zsh/site-functions/_gh`                 | GitHub CLI                |

### Discovery Patterns

Not sure if a tool supports completion generation? Try these patterns:

```bash
# Most common patterns
<tool> completion zsh        # Most common
<tool> completions zsh       # Rust tools
<tool> complete zsh          # Alternative
<tool> gen-completions --shell zsh    # Some tools
<tool> generate-shell-completion zsh  # Modern tools

# Discovery command
<tool> --help | grep -i completion
```

---

## Verification: Proving It Works

After making changes, you need to verify completions are actually loaded.

### Check if Completions Exist

```bash
whence -v _brew     # Should show path
whence -v _rustup   # Should show path
whence -v _cargo    # Should show path
```

If these return "not found," the completion isn't loaded. If they show a path, you're in business.

### View Current fpath

```bash
echo $fpath | tr " " "\n"
```

This should include:

- `/opt/homebrew/share/zsh/site-functions` (or `/usr/local/...` on Intel)
- `~/.oh-my-zsh/custom/plugins/zsh-completions/src`

### Test Tab Completion

```bash
brew <TAB>
rustup <TAB>
cargo <TAB>
```

You should see a list of available commands and options.

---

## Common Troubleshooting Scenarios

### Issue 1: Completions Generated but Not Found

**Symptom**: `whence -v _rustup` shows "not found"

**Cause**: Completion cache not rebuilt after generating new completions

**Fix**:

```bash

rm -f ~/.zcompdump* && exec zsh
```

### Issue 2: Permission Denied Writing Completions

**Symptom**: Cannot write to `/opt/homebrew/share/zsh/site-functions/`

**Fix**: Use a user directory instead:

```bash
mkdir -p ~/.zsh/completions
rustup completions zsh rustup > ~/.zsh/completions/_rustup

# Add to ~/.zshrc BEFORE oh-my-zsh.sh:
fpath=(~/.zsh/completions $fpath)
```

### Issue 3: Wrong Completion Takes Precedence

**Symptom**: System completion loads instead of your generated one

**Cause**: Your path is appended instead of prepended

**Fix**: Use prepend (`=`) instead of append (`+=`):

```bash
# Prepend - your completions take priority
fpath=("$(brew --prefix)/share/zsh/site-functions" $fpath)

# Append - system completions take priority
fpath+=("$(brew --prefix)/share/zsh/site-functions")
```

### Issue 4: Extended Attributes Blocking

**Symptom**: Files exist but aren't loaded

**Diagnosis**:

```bash

ls -l@ $(brew --prefix)/share/zsh/site-functions/_rustup
# Shows: com.apple.provenance
```

**Fix**: Clear the quarantine attribute:

```bash

xattr -c $(brew --prefix)/share/zsh/site-functions/_rustup
rm -f ~/.zcompdump* && exec zsh
```

---

## Tools That Don't Support Completion Generation

Not every tool includes this feature. These popular tools require external completion sources:

**Cloud/DevOps**: docker, kubectl, helm, terraform, aws, gcloud, az

**Languages**: python, node, ruby (use system completions)

**Python tools**: ruff, black, mypy, pytest

**Data tools**: jq, yq

For these, rely on:

- Oh My Zsh plugins (e.g., `plugins=(docker kubectl terraform)`)
- The [zsh-completions](https://github.com/zsh-users/zsh-completions) plugin (200+ completions)
- System-provided completions from Homebrew

---

## Systematic Completion Discovery

Want to find all tools in your system that support completion generation? Here's a discovery script:

```bash
#!/usr/bin/env bash
# Discover all tools with completion support

PATTERNS=("completion zsh" "completions zsh" "complete zsh" "gen-completions --shell zsh")

for tool in /opt/homebrew/bin/*; do
    tool_name=$(basename "$tool")
    for pattern in "${PATTERNS[@]}"; do
        if timeout 1s "$tool" $pattern --help &>/dev/null 2>&1; then
            echo "$tool_name: $pattern"
            break
        fi
    done
done
```

Run this, and you'll get a list of every tool that responds to completion commands.

---

## The Mental Model: Think of compinit as an Index

The key insight that made everything click for me:

**`compinit` is like building an index.** It scans all directories in `fpath`, finds all files starting with `_`, and caches them. Once the index is built, it doesn't rescan.

This explains why:

- Adding to `fpath` after `compinit` runs doesn't work
- You need to rebuild the cache after adding new completions
- Prepending paths matters for priority

If you think of `~/.zcompdump` as a compiled index and `fpath` as the source directories, the behavior becomes predictable.

---

## Key Insights & Best Practices

After hours of debugging, here's what I learned:

1. **Timing is Everything**: `fpath` modifications MUST come before Oh My Zsh loads

2. **No Manual compinit**: Let Oh My Zsh handle it—that's what it's designed to do

3. **Portable Homebrew Path**: Use `$(brew --prefix)` for Apple Silicon/Intel compatibility

4. **Prepend for Priority**: Use `fpath=(new $fpath)` not `fpath+=(new)`

5. **Always Rebuild Cache**: After any fpath changes: `rm -f ~/.zcompdump* && exec zsh`

6. **Generate to Homebrew Location**: Completions in `$(brew --prefix)/share/zsh/site-functions/` are auto-discovered

7. **Verify with whence**: Use `whence -v _toolname` to confirm loading

---

## The Complete Setup Script

Here's a one-shot script to set up completions for all common tools:

```bash
#!/usr/bin/env bash
# setup-completions.sh - Generate completions for modern CLI tools

DEST="$(brew --prefix)/share/zsh/site-functions"

echo "Generating completions to: $DEST"

# Rust tools (via rustup)
if command -v rustup &>/dev/null; then
    rustup completions zsh rustup > "$DEST/_rustup" 2>/dev/null && echo "✓ rustup"
    rustup completions zsh cargo > "$DEST/_cargo" 2>/dev/null && echo "✓ cargo"
fi

# Starship prompt
if command -v starship &>/dev/null; then
    starship completions zsh > "$DEST/_starship" 2>/dev/null && echo "✓ starship"
fi

# UV (Python package manager)
if command -v uv &>/dev/null; then
    uv generate-shell-completion zsh > "$DEST/_uv" 2>/dev/null && echo "✓ uv"
fi

# Atuin (shell history)
if command -v atuin &>/dev/null; then
    atuin gen-completions --shell zsh > "$DEST/_atuin" 2>/dev/null && echo "✓ atuin"
fi

# 1Password CLI
if command -v op &>/dev/null; then
    op completion zsh > "$DEST/_op" 2>/dev/null && echo "✓ op"
fi

# GitHub CLI
if command -v gh &>/dev/null; then
    gh completion -s zsh > "$DEST/_gh" 2>/dev/null && echo "✓ gh"
fi

# Rclone
if command -v rclone &>/dev/null; then
    rclone completion zsh - > "$DEST/_rclone" 2>/dev/null && echo "✓ rclone"
fi

echo ""
echo "Done! Run: rm -f ~/.zcompdump* && exec zsh"
```

---

## Why This Matters

Tab completion isn't just a convenience—it's a form of documentation. When `cargo <TAB>` shows me `build`, `run`, `test`, `check`, I'm not just saving keystrokes. I'm being reminded of what's possible.

A shell without completions is like a codebase without types. You can work in it, but you're flying blind.

The frustrating part about zsh completion problems is that they're almost always configuration issues, not missing features. The completions exist. The system works. But the timing, the ordering, the caching—these invisible details make or break the experience.

Now that I understand the mental model, I can debug completion issues in minutes instead of hours. And hopefully, after reading this, you can too.

---

## Codifying Knowledge: A Claude Code Skill

After spending hours debugging this, I realized something: this is exactly the kind of knowledge that gets lost. You solve it once, forget the details, and three months later you're back on Stack Overflow trying to remember whether it was `fpath+=` or `fpath=`.

So I turned this troubleshooting knowledge into a **Claude Code skill**.

### What's a Claude Code Skill?

Claude Code supports custom skills—reusable prompts that encode domain expertise. When you encounter a problem that matches a skill's domain, Claude can invoke it to get specialized guidance.

I created a skill called `zsh-completions` that contains:

- The diagnostic workflow (check fpath order, verify compinit timing)
- The complete list of tools with completion generation support
- All four troubleshooting scenarios and their fixes
- The verification steps to prove it's working

### Using the Skill

When I (or anyone with the skill installed) asks Claude Code about zsh completion issues, it now has immediate access to this entire troubleshooting guide:

```text
> My brew completions stopped working after I updated my zshrc

Claude Code invokes the zsh-completions skill and immediately:
1. Checks fpath order relative to oh-my-zsh.sh
2. Identifies if compinit is being called manually
3. Generates missing completions
4. Verifies with whence -v
5. Rebuilds the cache
```

### The Pattern: Debug Once, Encode Forever

This is a pattern I'm increasingly adopting:

1. **Encounter a gnarly problem** that takes hours to debug
2. **Document the solution** thoroughly (like this blog post)
3. **Encode it as a skill** so AI assistants can apply the knowledge

The skill doesn't replace understanding—it augments recall. When I haven't touched my zshrc in months and something breaks, I don't have to remember everything. The skill contains the systematic approach.

### Creating Your Own Skills

If you use Claude Code, consider encoding your own hard-won debugging knowledge:

```markdown
# .claude/skills/your-skill.md

## When to use this skill
- User reports [specific symptom]
- User asks about [domain topic]

## Diagnostic steps
1. Check [first thing]
2. Verify [second thing]
...

## Common fixes
### Issue: [symptom]
**Cause**: [root cause]
**Fix**: [solution]
```

The goal isn't to replace expertise—it's to make expertise reproducible. The next time someone (including future me) encounters broken zsh completions, the solution is one skill invocation away.

---

## Quick Reference Card

```bash
# Check if a completion is loaded
whence -v _toolname

# View fpath directories
echo $fpath | tr " " "\n"

# Rebuild completion cache
rm -f ~/.zcompdump* && exec zsh

# Generate completions to Homebrew location
TOOL completions zsh > "$(brew --prefix)/share/zsh/site-functions/_TOOL"

# Clear extended attributes
xattr -c "$(brew --prefix)/share/zsh/site-functions/_TOOL"

# Key rule
# fpath modifications BEFORE source $ZSH/oh-my-zsh.sh
```

That's it. Order matters, cache rebuilds matter, and verification matters. Get those three right, and you'll have a shell that responds to your every `<TAB>`.
