---
title: Zsh Environment
url: https://doc.liz6.com/en/tools/shell
locale: en
area: tools
tags:
- tools
date: null
modified: 2026-07-16
description: The only thing worth writing about in zsh is completion — native compinit as the base, carapace as the fallback, and fzf-tab providing command-specific previews.
---

# Zsh Environment

My zsh configuration is only a couple of hundred lines long. The prompt is Starship, there are many aliases, but none of these are particularly noteworthy. The only area I put real effort into, and the only one worth documenting, is **completion** — I’ve configured Tab to trigger a menu with previews, and I had to wrestle a bit with carapace to make it work.

The completion priority is: native `compinit` as the base, with carapace only as a fallback. This order is crucial. By default, carapace initializes by using `compdef` to take over over 700 commands. While this sounds convenient, it actually overrides zsh’s native, more granular completions and breaks fzf-tab’s dependency on subcommand context (e.g., `git checkout` needs to preview based on branches). Therefore, I filter out its `compdef` calls and only let carapace kick in when native completion fails:

```zsh
source <(carapace _carapace zsh | grep -v '^compdef ')
zstyle ':completion:*' completer _complete _carapace _ignored
```

After completion, fzf-tab passes the candidates to fzf and provides different previews depending on the command: files are rendered with `bat`, directories are listed with `eza`, `kill` shows processes, `systemctl` displays unit status directly, `git checkout` lists recent commits, and `git add` shows diffs. This is the area that provides the biggest daily productivity boost — often, you don’t need to finish typing the command and press Enter to see the result; a quick Tab preview is enough to judge.

```zsh
zstyle ':fzf-tab:complete:systemctl:*' fzf-preview 'systemctl status --no-pager $word | head -30'
zstyle ':fzf-tab:complete:git-(checkout|switch):*' fzf-preview 'git log --oneline -n 5 $word'
```

This preview setup looks polished, but it has its pitfalls, which I’ve documented in the comments: passing an unrecognized flag to fzf causes it to exit immediately or leave the completion list inexplicably empty; setting `fzf-min-height` too large leaves empty space at the bottom when there are few candidates. So those `--no-sort` and `fzf-pad 0` settings aren’t copied from somewhere else — they’re the result of tuning.

Beyond completion, there are a few small tweaks that make the shell feel “responsive.” I’ll mention each briefly:

**zsh-autosuggestions** provides fish-style grayed-out history suggestions — as you type, it appends the most recently matching full command in gray text, which you can accept with `→` or `Ctrl+f`. It fetches suggestions asynchronously (`ZSH_AUTOSUGGEST_USE_ASYNC`), so even a long history doesn’t cause keypress lag.

**zsh-syntax-highlighting** validates as you type: valid commands are highlighted in green, invalid ones in red, and unclosed quotes change color immediately. You can spot typos before pressing Enter. It **must be sourced last in `.zshrc`** — it needs to wrap the outermost hook for all ZLE widgets; loading it earlier would cause subsequent plugins to override it.

**starship** serves as the prompt: a single TOML configuration, cross-shell compatible, displaying git branch/status, language versions, k8s context, etc., based on directory context. It only appears when relevant, avoiding constant screen clutter.

**ssh completion** source changed: by default, it scans `known_hosts` (a mess of IPs, possibly hashed and unreadable). I’ve switched it to complete only from `Host` aliases in `~/.ssh/config`. Now, `ssh <TAB>` directly lists machines I’ve named, making it clear who is who.

Details about `zoxide`’s `z` for jumping directories, replacing the entire `ls` family with `eza`, and using `delta` for git pager are covered in the [CLI Toolbox](cli-toolbox.md).
