Skip to content
Go back

Carapace: Universal Shell Completions

By SumGuy 9 min read
Carapace: Universal Shell Completions

Your Tab Key Is Lying To You

You type kubectl get and hit Tab. Nothing. You install a plugin. It works in zsh but breaks in fish. You switch to a new machine, copy your dotfiles over, and spend the next forty minutes wondering why docker compose isn’t completing again. You check the docs. The docs say to source a script that the tool generates at runtime. You add it to your shell config. It slows down your shell startup by 200ms. Repeat this for every tool you use.

This is the shell completion dark age, and most of us just accept it as the cost of doing business.

Carapace disagrees.

Carapace-bin is a completion engine that ships pre-built completions for over 1,000 CLI tools and speaks every major shell’s native completion dialect — bash, zsh, fish, elvish, nushell, oil, powershell, tcsh, and xonsh. One binary. One config. Everything just works.

Here’s how it actually works and whether it’s worth swapping out whatever you’re doing now.


What Carapace Actually Is

Carapace is two things that often get confused:

carapace-spec — A YAML-based DSL for writing shell completions in a shell-agnostic way. You write one spec, it generates completions for all supported shells.

carapace-bin — A pre-compiled binary that ships with specs for 1,000+ real-world tools already written. This is the thing you actually want to install.

The way it hooks into your shell is clever: instead of sourcing a hundred different completion scripts, you register carapace as a single completion function. When you hit Tab on git, your shell asks carapace. Carapace looks up its spec for git, figures out what completions make sense given your current input, and hands back the list. Your shell renders it. Done.

No generated scripts sitting in ~/.zfunc. No eval "$(tool completion zsh)" scattered across your .zshrc. No shell-startup penalty for tools you haven’t used in three weeks.


Installation

Carapace-bin 1.6.5 is the current stable as of mid-2026. You’ve got options:

Via your package manager (preferred):

Terminal window
# Arch / Manjaro (it's in the AUR and community repos)
pacman -S carapace-bin
# macOS
brew install carapace-bin
# Debian/Ubuntu — grab the .deb from GitHub releases
wget https://github.com/carapace-sh/carapace-bin/releases/download/v1.6.5/carapace-bin_1.6.5_linux_amd64.deb
sudo dpkg -i carapace-bin_1.6.5_linux_amd64.deb
# Go install (if you want to live on the edge)
go install github.com/carapace-sh/carapace-bin/cmd/carapace@latest

Verify it’s there:

Terminal window
carapace --version
# carapace-bin v1.6.5

Now wire it into your shell. This is the only sourcing line you’ll ever need:

Bash — add to ~/.bashrc:

Terminal window
source <(carapace _carapace bash)

Zsh — add to ~/.zshrc:

Terminal window
source <(carapace _carapace zsh)

Fish — add to ~/.config/fish/config.fish:

Terminal window
carapace _carapace fish | source

Nushell — add to your config.nu:

Terminal window
carapace _carapace nushell | save --force ~/.cache/carapace/init.nu
source ~/.cache/carapace/init.nu

Restart your shell (or source the config), and you’re done. That’s genuinely it.


What You Get Out of the Box

Here’s where carapace-bin earns its keep. The spec library covers tools you actually use:

Let’s see it in action. After wiring carapace into zsh, try:

Terminal window
kubectl get <TAB>
# bindings configmaps daemonsets deployments
# endpoints events horizontalpodautoscalers
# ingresses jobs namespaces nodes
# persistentvolumeclaims persistentvolumes
# pods replicasets secrets services
# statefulsets storageclasses ...
Terminal window
docker run --<TAB>
# --add-host --annotation --attach --blkio-weight
# --cap-add --cap-drop --cgroup-parent --cgroupns
# --cidfile --cpu-count --cpu-percent --cpu-period
# ...
Terminal window
git log --<TAB>
# --abbrev-commit --all --ancestry-path --author
# --author-date-relative --before --branches
# ...

This is context-aware, too. Carapace knows that git checkout <TAB> should show branches and tags, while git checkout -- <TAB> should show modified files. It’s not just splatting flag names at you.


Comparing to What You Were Doing Before

Let’s be honest about the alternatives, because “just install carapace” isn’t obviously better until you’ve lived through the alternatives.

The “eval at startup” approach

Every tool docs page tells you to add something like this to your shell config:

Terminal window
eval "$(kubectl completion zsh)"
eval "$(helm completion zsh)"
eval "$(gh completion -s zsh)"
eval "$(terraform -install-autocomplete)"

The problems compound fast. Each eval forks a subprocess at shell startup. On a loaded machine that’s 10-30ms per call. With 15 tools, your shell takes 300ms+ to open — not catastrophic, but you notice it. More annoyingly: the completions only exist for shells the tool explicitly supports. Half these CLIs don’t have fish support. Almost none support nushell.

With carapace, startup cost is one subprocess, one time. The rest is lookup.

The oh-my-zsh plugin approach

Oh-my-zsh ships completion plugins for popular tools, and they’re decent. But they’re maintained by volunteers, lag behind upstream CLIs by months or years, and they’re zsh-only. If you run fish at work and zsh at home, you’re maintaining two completion setups. If you switch to nushell experimentally, you’re back to square one.

Native completions via shell plugin managers

Fish has its own completion ecosystem and it’s actually pretty good — many tools ship completions/*.fish files. Zsh has zsh-completions. But again: per-shell, per-tool maintenance. When rclone ships a new command, you wait for the maintainer to notice and push an update.

Carapace-bin updates as a single package. The spec library is maintained centrally. You upgrade carapace, you get updated completions for everything.


When Carapace Falls Short

Carapace isn’t magic. There are real rough edges.

Dynamic completions are limited. Some tools generate completions based on live state — things like Kubernetes resource names in your current cluster, or git branch names. Carapace handles some of this (it can shell out to a command to fetch completions dynamically via the spec DSL), but the quality varies by spec. Official kubectl completions pull resource names from the live API; carapace’s kubectl spec does this too, but occasionally a newer resource type is missing until the spec is updated.

Coverage gaps exist. 1,000+ tools sounds like everything, but it isn’t. If you use an obscure internal tool or something niche like age, atuin, or zellij, check the supported commands list before assuming it’s covered. As of mid-2026 the list is growing fast but there are still holes.

Writing your own specs takes effort. The carapace-spec YAML format is genuinely good for what it does, but there’s a learning curve. If you need completions for an internal CLI, budget an hour to learn the format before you start.

Shell edge cases. Fish and nushell support is solid. Bash support exists but bash completion is famously janky regardless of what generates it. If you’re a bash user expecting zsh-quality UX, manage expectations.


Writing a Custom Spec

If you need completions for a tool carapace doesn’t cover yet, you write a YAML spec. Here’s a real example — a simple deploy script that takes an environment and optional flags:

deploy.yaml
name: deploy
description: Deploy to an environment
flags:
- longhand: dry-run
shorthand: n
description: Print what would happen without running
- longhand: verbose
shorthand: v
description: Verbose output
positional:
- name: environment
description: Target environment
values:
- production
- staging
- dev
- local

Place it at ~/.config/carapace/specs/deploy.yaml and it’s live immediately. No restart required. Hit Tab on deploy <Tab> and you get production, staging, dev, local.

For something more dynamic — say, listing available Ansible inventories from a directory:

ansible-inventory-complete.yaml
name: myansible
description: Internal Ansible wrapper
flags:
- longhand: inventory
shorthand: i
description: Inventory file
values:
command:
- bash
- -c
- ls ~/ansible/inventories/*.yml | xargs -n1 basename

That command key tells carapace to shell out and use the output as completion candidates. It’s not instantaneous (you’re forking a process), but for static-ish data like inventory files it’s perfectly fine.


The Setup I Actually Use

For reference, here’s the carapace setup in a zsh config that’s been running daily since early 2026:

.zshrc (carapace section)
# Single line. That's it.
source <(carapace _carapace zsh)
# Optional: tell carapace where your custom specs live
# (default is ~/.config/carapace/specs — you don't need this unless you moved them)
export CARAPACE_SPEC_DIR="$HOME/.config/carapace/specs"
# Optional: disable carapace for specific commands you want to handle differently
# (useful if you have a hand-tuned completion for something carapace gets wrong)
export CARAPACE_EXCLUDES="myspecialtool"

Shell startup time before carapace: ~180ms (measured with time zsh -i -c exit). Shell startup time after: ~190ms.

That’s 10ms for a completion engine covering 1,000 tools. The old eval-everything approach was costing 380ms for a dozen tools. This is not a close comparison.


Should You Bother?

If you use more than five CLIs regularly and you’re tired of inconsistent, missing, or broken tab completions — yes, absolutely bother. The install is five minutes, the config is one line per shell, and the quality is immediately better than whatever patchwork you’ve accumulated.

If you’re primarily a fish user, your built-in completions are already pretty good and the gap is smaller. Carapace is still worth it for the tools fish doesn’t cover natively, but the urgency is lower.

If you run multiple shells across machines (homelab, work laptop, WSL instance), carapace is a clear win because one spec covers all of them. Your ~/.config/carapace/specs/ directory syncs cleanly with chezmoi or whatever dotfile manager you use, and it just works everywhere.

The only case I’d say skip it: if you’re a bash purist running one machine and the handful of tools you use already have decent bash completions. For that exact person the complexity tradeoff doesn’t pay off.

For everyone else — your Tab key has been underperforming. Carapace fixes that.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts