Imported from Nagell/claude-marketplace (
plugins/base-setup/skills/setup-zsh/SKILL.md). Install upstream withnpx skills add Nagell/claude-marketplace --skill setup-zsh. Copyright stays with the author.
Setup Zsh Environment
Install and configure a complete Zsh environment with zinit plugin manager, Powerlevel10k theme, zsh-autosuggestions, zsh-syntax-highlighting, and MesloLGS NF Nerd Font. No Oh My Zsh — zinit self-bootstraps and manages everything. Handles WSL, native Linux, and macOS environments.
Implementation Steps
When this command is invoked:
1. Detect Environment
Run this check using Bash tool:
if [[ "$(uname)" == "Darwin" ]]; then echo "MACOS"; elif grep -qi microsoft /proc/version 2>/dev/null; then echo "WSL"; else echo "LINUX"; fi
Store the result - it determines how Zsh is installed (Step 3) and how fonts are installed (Step 4).
Possible results:
- WSL - Windows Subsystem for Linux. Zsh via apt, fonts to Windows host.
- LINUX - Native Linux. Zsh via apt, fonts to
~/.local/share/fonts. - MACOS - macOS. Zsh is pre-installed (default shell since Catalina). Fonts to
~/Library/Fonts.
2. Check Prerequisites
Check what is already installed by running these checks in parallel using Bash tool:
which zsh 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which git 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which curl 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which unzip 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which fzf 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which glow 2>/dev/null && echo "INSTALLED" || echo "MISSING"
which btop 2>/dev/null && echo "INSTALLED" || echo "MISSING"
test -x ~/.local/bin/mdv && echo "mdv: INSTALLED" || echo "mdv: MISSING"
test -d "$HOME/.local/share/zinit/zinit.git" && echo "INSTALLED" || echo "MISSING"
Report which components are already installed. Zinit manages all plugins and Powerlevel10k — they are downloaded automatically on the first shell launch after .zshrc is configured.
3. Install Zsh (requires user action on Linux/WSL)
If Zsh is not installed:
If MACOS: Zsh is the default shell since macOS Catalina. It should already be installed. If somehow missing, instruct the user:
Please run this command manually, then confirm when done:
brew install zsh
chsh -s $(which zsh)
If LINUX or WSL:
IMPORTANT: Claude cannot run sudo commands. Output the following message to the user and wait for confirmation before proceeding:
Please run these commands manually in your terminal, then confirm when done:
sudo apt update && sudo apt install zsh -y
chsh -s $(which zsh)
Explain that chsh changes the default shell and takes effect on next login/terminal session.
DO NOT proceed to Step 4 until the user confirms Zsh is installed. Use AskUserQuestion to ask "Have you finished installing Zsh and setting it as default shell?" with options "Yes, done" and "Skip, it was already installed".
4. Install Nerd Font
Ask the user which font to install using AskUserQuestion: "Which Nerd Font would you like to install?" with options:
MesloLGS NF(recommended — specially designed for Powerlevel10k)Fira Code Nerd Font(popular programming font with ligatures)
Store the choice as FONT_CHOICE = "meslo" or "fira". Use it in Step 11 to set the correct VS Code font family name.
Option A: MesloLGS NF (FONT_CHOICE = "meslo")
Download the four MesloLGS NF font files. The installation location depends on the environment detected in Step 1.
First, check if fonts are already installed by looking for them in the appropriate directory.
If WSL:
Fonts must be installed on the Windows host for VS Code to use them. Check if they already exist:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
ls "${FONTDIR}"/MesloLGS*.ttf 2>/dev/null | wc -l
If the count is 4, all fonts are already installed - skip downloading and report them as already installed.
If fewer than 4, download the missing fonts:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
mkdir -p "${FONTDIR}"
declare -A fontmap=( ["Regular"]="Regular" ["Bold"]="Bold" ["Italic"]="Italic" ["Bold Italic"]="Bold%20Italic" )
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]] && [[ $(stat -c%s "$filepath" 2>/dev/null || echo 0) -gt 1000000 ]]; then
echo "Already installed: MesloLGS NF ${font}.ttf"
else
curl -fsSL -o "$filepath" "https://github.com/romkatv/powerlevel10k-media/raw/master/MesloLGS%20NF%20${fontmap[$font]}.ttf"
fi
done
After downloading, verify all 4 files exist and have valid size (>1MB each):
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
MISSING=0
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -c%s "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 1000000 ]]; then
echo "OK: MesloLGS NF ${font}.ttf (${size} bytes)"
else
echo "INVALID (too small): MesloLGS NF ${font}.ttf (${size} bytes)"
MISSING=$((MISSING+1))
fi
else
echo "MISSING: MesloLGS NF ${font}.ttf"
MISSING=$((MISSING+1))
fi
done
echo "Missing fonts: ${MISSING}"
If any fonts are missing or invalid, report the specific failures to the user and do NOT proceed — ask them to retry or download manually.
Then register fonts in Windows registry so Windows discovers them:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
WINFONTDIR="C:\\Users\\${WINUSER}\\AppData\\Local\\Microsoft\\Windows\\Fonts"
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
regname="MesloLGS NF ${font} (TrueType)"
regpath="${WINFONTDIR}\\MesloLGS NF ${font}.ttf"
reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts" /v "$regname" /t REG_SZ /d "$regpath" /f
done
This registers the fonts under the current user's registry hive — no admin/sudo required.
If native Linux:
Check if fonts already exist:
FONTDIR="$HOME/.local/share/fonts"
ls "${FONTDIR}"/MesloLGS*.ttf 2>/dev/null | wc -l
If the count is 4, skip downloading. Otherwise, download missing fonts:
FONTDIR="$HOME/.local/share/fonts"
mkdir -p "${FONTDIR}"
declare -A fontmap=( ["Regular"]="Regular" ["Bold"]="Bold" ["Italic"]="Italic" ["Bold Italic"]="Bold%20Italic" )
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]] && [[ $(stat -c%s "$filepath" 2>/dev/null || echo 0) -gt 1000000 ]]; then
echo "Already installed: MesloLGS NF ${font}.ttf"
else
curl -fsSL -o "$filepath" "https://github.com/romkatv/powerlevel10k-media/raw/master/MesloLGS%20NF%20${fontmap[$font]}.ttf"
fi
done
fc-cache -fv
After downloading, verify all 4 files exist and have valid size (>1MB each):
FONTDIR="$HOME/.local/share/fonts"
MISSING=0
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -c%s "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 1000000 ]]; then
echo "OK: MesloLGS NF ${font}.ttf (${size} bytes)"
else
echo "INVALID (too small): MesloLGS NF ${font}.ttf (${size} bytes)"
MISSING=$((MISSING+1))
fi
else
echo "MISSING: MesloLGS NF ${font}.ttf"
MISSING=$((MISSING+1))
fi
done
echo "Missing fonts: ${MISSING}"
If any fonts are missing or invalid, report the specific failures to the user and do NOT proceed — ask them to retry or download manually.
The fc-cache command refreshes the font cache so the fonts are immediately available.
If macOS:
Check if fonts already exist:
FONTDIR="$HOME/Library/Fonts"
ls "${FONTDIR}"/MesloLGS*.ttf 2>/dev/null | wc -l
If the count is 4, skip downloading. Otherwise, download missing fonts:
FONTDIR="$HOME/Library/Fonts"
mkdir -p "${FONTDIR}"
declare -A fontmap=( ["Regular"]="Regular" ["Bold"]="Bold" ["Italic"]="Italic" ["Bold Italic"]="Bold%20Italic" )
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]] && [[ $(stat -f%z "$filepath" 2>/dev/null || echo 0) -gt 1000000 ]]; then
echo "Already installed: MesloLGS NF ${font}.ttf"
else
curl -fsSL -o "$filepath" "https://github.com/romkatv/powerlevel10k-media/raw/master/MesloLGS%20NF%20${fontmap[$font]}.ttf"
fi
done
After downloading, verify all 4 files exist and have valid size (>1MB each):
FONTDIR="$HOME/Library/Fonts"
MISSING=0
for font in "Regular" "Bold" "Italic" "Bold Italic"; do
filepath="${FONTDIR}/MesloLGS NF ${font}.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -f%z "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 1000000 ]]; then
echo "OK: MesloLGS NF ${font}.ttf (${size} bytes)"
else
echo "INVALID (too small): MesloLGS NF ${font}.ttf (${size} bytes)"
MISSING=$((MISSING+1))
fi
else
echo "MISSING: MesloLGS NF ${font}.ttf"
MISSING=$((MISSING+1))
fi
done
echo "Missing fonts: ${MISSING}"
If any fonts are missing or invalid, report the specific failures to the user and do NOT proceed — ask them to retry or download manually.
macOS picks up fonts from ~/Library/Fonts automatically - no cache refresh needed.
Option B: Fira Code Nerd Font (FONT_CHOICE = "fira")
Download from: https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/FiraCode.zip
The zip contains ~20 TTF files across three variants (standard, Mono, Propo). Use the pattern FiraCodeNerdFont-*.ttf to extract only the ~6 standard variants — this excludes FiraCodeNerdFontMono-* and FiraCodeNerdFontPropo-*.
If WSL:
Check if already installed:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
ls "${FONTDIR}"/FiraCodeNerdFont-Regular.ttf 2>/dev/null && echo "INSTALLED" || echo "MISSING"
If INSTALLED, skip downloading and report already installed.
If MISSING, download and install:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
mkdir -p "${FONTDIR}"
TMPFONT=$(mktemp -d)
curl -fsSL -o "${TMPFONT}/FiraCode.zip" "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/FiraCode.zip"
unzip -j -q "${TMPFONT}/FiraCode.zip" "FiraCodeNerdFont-*.ttf" -d "${FONTDIR}"
rm -rf "${TMPFONT}"
Verify:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
filepath="${FONTDIR}/FiraCodeNerdFont-Regular.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -c%s "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 100000 ]]; then echo "OK: FiraCodeNerdFont-Regular.ttf (${size} bytes)"; else echo "INVALID (too small): ${size} bytes"; fi
else
echo "MISSING: FiraCodeNerdFont-Regular.ttf"
fi
If the font is missing or invalid, report it to the user and do NOT proceed.
Register all installed FiraCode fonts in the Windows registry:
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
WINFONTDIR="C:\\Users\\${WINUSER}\\AppData\\Local\\Microsoft\\Windows\\Fonts"
FONTDIR="/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows/Fonts"
for f in "${FONTDIR}"/FiraCodeNerdFont-*.ttf; do
[[ -f "$f" ]] || continue
fname=$(basename "$f")
reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts" /v "${fname%.ttf} (TrueType)" /t REG_SZ /d "${WINFONTDIR}\\${fname}" /f
done
If native Linux:
Check if already installed:
FONTDIR="$HOME/.local/share/fonts"
ls "${FONTDIR}"/FiraCodeNerdFont-Regular.ttf 2>/dev/null && echo "INSTALLED" || echo "MISSING"
If MISSING, download and install:
FONTDIR="$HOME/.local/share/fonts"
mkdir -p "${FONTDIR}"
TMPFONT=$(mktemp -d)
curl -fsSL -o "${TMPFONT}/FiraCode.zip" "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4/FiraCode.zip"
unzip -j -q "${TMPFONT}/FiraCode.zip" "FiraCodeNerdFont-*.ttf" -d "${FONTDIR}"
rm -rf "${TMPFONT}"
fc-cache -fv
Verify:
FONTDIR="$HOME/.local/share/fonts"
filepath="${FONTDIR}/FiraCodeNerdFont-Regular.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -c%s "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 100000 ]]; then echo "OK: FiraCodeNerdFont-Regular.ttf (${size} bytes)"; else echo "INVALID (too small): ${size} bytes"; fi
else
echo "MISSING: FiraCodeNerdFont-Regular.ttf"
fi
If the font is missing or invalid, report it to the user and do NOT proceed.
If macOS:
Check if already installed:
FONTDIR="$HOME/Library/Fonts"
ls "${FONTDIR}"/FiraCodeNerdFont-Regular.ttf 2>/dev/null && echo "INSTALLED" || echo "MISSING"
If MISSING, download and install:
FONTDIR="$HOME/Library/Fonts"
mkdir -p "${FONTDIR}"
TMPFONT=$(mktemp -d)
curl -fsSL -o "${TMPFONT}/FiraCode.zip" "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/FiraCode.zip"
unzip -j -q "${TMPFONT}/FiraCode.zip" "FiraCodeNerdFont-*.ttf" -d "${FONTDIR}"
rm -rf "${TMPFONT}"
Verify:
FONTDIR="$HOME/Library/Fonts"
filepath="${FONTDIR}/FiraCodeNerdFont-Regular.ttf"
if [[ -f "$filepath" ]]; then
size=$(stat -f%z "$filepath" 2>/dev/null || echo 0)
if [[ $size -gt 100000 ]]; then echo "OK: FiraCodeNerdFont-Regular.ttf (${size} bytes)"; else echo "INVALID (too small): ${size} bytes"; fi
else
echo "MISSING: FiraCodeNerdFont-Regular.ttf"
fi
If the font is missing or invalid, report it to the user and do NOT proceed.
macOS picks up fonts from ~/Library/Fonts automatically - no cache refresh needed.
5. Install fzf
fzf is required for fzf-tab completion and shell key bindings. It must be installed before the first exec zsh, otherwise fzf-tab silently does nothing.
If already installed (which fzf returned INSTALLED), skip this step.
If MACOS or WSL (linuxbrew available):
brew install fzf
If LINUX (no brew):
IMPORTANT: Claude cannot run sudo commands. Output the following and wait for confirmation:
# Please run this command manually, then confirm when done:
sudo apt install fzf -y
Use AskUserQuestion to ask "Have you finished installing fzf?" with options "Yes, done" and "Skip".
After installing, verify:
which fzf && fzf --version
6. Configure ~/.zshrc
Read the existing ~/.zshrc file using the Read tool. If it already contains zdharma-continuum/zinit, zinit is already configured — skip to Step 6.
Otherwise, read the existing ~/.zshrc with the Read tool to identify any PATH exports, NVM setup, custom functions, or keybindings to preserve. Then use the Write tool to create ~/.zshrc with the following structure, incorporating any preserved content after the aliases block.
The core zinit block (add after the P10K instant prompt block, before any other config):
# Zinit - auto-installs itself if missing
ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git"
if [ ! -d "$ZINIT_HOME" ]; then
mkdir -p "$(dirname $ZINIT_HOME)"
git clone https://github.com/zdharma-continuum/zinit.git "$ZINIT_HOME"
fi
source "${ZINIT_HOME}/zinit.zsh"
# Powerlevel10k
zinit ice depth=1; zinit light romkatv/powerlevel10k
# Plugins
zinit light zsh-users/zsh-autosuggestions
zinit light zsh-users/zsh-syntax-highlighting
zinit light zsh-users/zsh-completions
zinit light jirutka/zsh-shift-select
zinit light Aloxaf/fzf-tab
# OMZ snippets
zinit snippet OMZL::git.zsh
zinit snippet OMZP::git
zinit snippet OMZP::sudo
zinit snippet OMZP::command-not-found
# Completions
autoload -Uz compinit && compinit
zinit cdreplay -q
Add history configuration after the zinit block:
# History
HISTSIZE=5000
HISTFILE=~/.zsh_history
SAVEHIST=$HISTSIZE
HISTDUP=erase
setopt appendhistory
setopt sharehistory
setopt hist_ignore_space
setopt hist_ignore_all_dups
setopt hist_save_no_dups
setopt hist_ignore_dups
setopt hist_find_no_dups
# Completion styling
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
zstyle ':completion:*' menu no
zstyle ':fzf-tab:complete:cd:*' fzf-preview 'ls --color $realpath'
# Aliases
alias ls='ls --color'
Also add a shell integrations block before the p10k sourcing line:
# Shell integrations
eval "$(fzf --zsh)"
Do NOT include export ZSH, ZSH_THEME, plugins=(...), or source $ZSH/oh-my-zsh.sh — there is no Oh My Zsh. Zinit handles everything.
7. Configure Keybindings
Read ~/.zshrc using Read tool. Check if a keybindings block already exists by searching for _select_all. If found, skip this step.
If not found, use Edit tool to append the following block before the p10k sourcing line ([[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh) if it exists, or at the end of the file otherwise.
All environments — append this base block:
# Windows-like keybindings
_select_all() {
zle beginning-of-line
zle set-mark-command
zle end-of-line
}
zle -N _select_all
bindkey '^A' _select_all
bindkey '^Z' undo
bindkey '^Y' redo
Then append clipboard bindings depending on the environment detected in Step 1:
If WSL — append:
_cut_to_clipboard() {
zle kill-region
echo -n "$CUTBUFFER" | clip.exe 2>/dev/null
}
zle -N _cut_to_clipboard
_paste_from_clipboard() {
local paste
paste=$(powershell.exe Get-Clipboard 2>/dev/null | tr -d '\r')
LBUFFER+=$paste
}
zle -N _paste_from_clipboard
bindkey '^X' _cut_to_clipboard
bindkey '^V' _paste_from_clipboard
_backspace_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
echo -n "$CUTBUFFER" | clip.exe 2>/dev/null
else
zle backward-delete-char
fi
}
zle -N _backspace_or_delete_region
bindkey '^?' _backspace_or_delete_region
_delete_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
echo -n "$CUTBUFFER" | clip.exe 2>/dev/null
else
zle delete-char
fi
}
zle -N _delete_or_delete_region
bindkey '^[[3~' _delete_or_delete_region
If macOS — append:
_cut_to_clipboard() {
zle kill-region
echo -n "$CUTBUFFER" | pbcopy 2>/dev/null
}
zle -N _cut_to_clipboard
_paste_from_clipboard() {
local paste
paste=$(pbpaste 2>/dev/null)
LBUFFER+=$paste
}
zle -N _paste_from_clipboard
bindkey '^X' _cut_to_clipboard
bindkey '^V' _paste_from_clipboard
_backspace_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
echo -n "$CUTBUFFER" | pbcopy 2>/dev/null
else
zle backward-delete-char
fi
}
zle -N _backspace_or_delete_region
bindkey '^?' _backspace_or_delete_region
_delete_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
echo -n "$CUTBUFFER" | pbcopy 2>/dev/null
else
zle delete-char
fi
}
zle -N _delete_or_delete_region
bindkey '^[[3~' _delete_or_delete_region
If native Linux — skip clipboard bindings but still add backspace/delete region support:
_backspace_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
else
zle backward-delete-char
fi
}
zle -N _backspace_or_delete_region
bindkey '^?' _backspace_or_delete_region
_delete_or_delete_region() {
if (( REGION_ACTIVE )); then
zle kill-region
else
zle delete-char
fi
}
zle -N _delete_or_delete_region
bindkey '^[[3~' _delete_or_delete_region
8. Configure Prefix History Search
Bind Up/Down arrows to prefix history search so they cycle through history entries beginning with whatever has already been typed — the same set zsh-autosuggestions draws its single grey suggestion from (autosuggestions has no cycling feature of its own).
Read ~/.zshrc using Read tool. If it already contains # >>> setup-zsh prefix-history >>>, the block is already configured — skip this step.
Otherwise, use Edit tool to append the following block after the keybindings block from Step 7 — before the p10k sourcing line ([[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh) if it exists, or at the end of the file otherwise. It must load after zinit and after any other arrow-key bindings so it wins:
# >>> setup-zsh prefix-history >>>
# Prefix history search: Up/Down cycle through history entries beginning with
# the already-typed text (matching what zsh-autosuggestions shows in grey).
autoload -Uz up-line-or-beginning-search down-line-or-beginning-search
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
bindkey '^[[A' up-line-or-beginning-search # Up, normal cursor mode
bindkey '^[[B' down-line-or-beginning-search # Down, normal cursor mode
bindkey '^[OA' up-line-or-beginning-search # Up, application cursor mode
bindkey '^[OB' down-line-or-beginning-search # Down, application cursor mode
# <<< setup-zsh prefix-history <<<
Use up-line-or-beginning-search, not up-line-or-search — the latter matches only the first word (docker) instead of the full typed prefix (docker compose), surfacing unrelated history entries.
Bind both ^[[A/B and ^[OA/OB — terminals switch between normal and application cursor-key modes, and binding only one leaves arrows broken in the other mode.
History dedup (hist_ignore_all_dups, hist_find_no_dups) is already set in Step 6's history block — no changes needed there.
Verify:
zsh -c 'source ~/.zshrc 2>/dev/null; bindkey "^[[A"'
Should print "^[[A" up-line-or-beginning-search.
9. Configure WSL Audio (WSL only)
Skip this step entirely if the environment is not WSL.
This enables microphone/audio input in WSL via WSLg's PulseAudio bridge — required for tools like Claude Code voice mode.
Check what's already set up:
which pactl 2>/dev/null && echo "pactl: OK" || echo "pactl: MISSING"
dpkg -l libasound2-plugins 2>/dev/null | grep -q '^ii' && echo "libasound2-plugins: OK" || echo "libasound2-plugins: MISSING"
test -f /mnt/wslg/runtime-dir/pulse/native && echo "WSLg PulseAudio: OK" || echo "WSLg PulseAudio: NOT RUNNING"
test -f "$HOME/.asoundrc" && echo ".asoundrc: EXISTS" || echo ".asoundrc: MISSING"
grep -q 'PULSE_SERVER' "$HOME/.zshrc" 2>/dev/null && echo "PULSE_SERVER in .zshrc: YES" || echo "PULSE_SERVER in .zshrc: NO"
If packages are missing (pactl: MISSING or libasound2-plugins: MISSING):
IMPORTANT: Claude cannot run sudo commands. Output the following and wait for confirmation:
Please run this command manually, then confirm when done:
sudo apt-get install -y pulseaudio-utils libasound2-plugins
Use AskUserQuestion to ask "Have you finished installing the audio packages?" with options "Yes, done" and "Skip audio setup".
If user skips, skip the rest of this step.
If WSLg PulseAudio is not running:
Inform the user: "WSLg PulseAudio is not running — audio may not work until you restart WSL. Continue anyway and audio will work after restart."
Configure ~/.asoundrc:
If ~/.asoundrc does not exist, or does not contain pcm.default pulse, create/overwrite it using Write tool:
pcm.default pulse
ctl.default pulse
Add PULSE_SERVER to .zshrc:
If PULSE_SERVER is not already in ~/.zshrc, use Edit tool to add it after the export PATH= line:
- old_string: the existing
export PATH=...line - new_string: same line + newline +
export PULSE_SERVER=unix:/mnt/wslg/runtime-dir/pulse/native
Report what was configured. Inform the user that audio input (microphone) is now routed through WSLg — tools like Claude Code /voice should work after reopening the terminal.
10. Port Bash Environment to Zsh
Oh My Zsh creates a fresh ~/.zshrc from a template, which means environment setup from ~/.bashrc and ~/.profile is lost. Common breakage: NVM (node/npm/pnpm missing), custom PATH entries, SSH agent auto-start, other exports.
Scan the user's existing bash config files for portable environment setup:
# Extract export, source, PATH, and eval statements from bash configs
for file in "$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile"; do
if [[ -f "$file" ]]; then
echo "=== $file ==="
grep -E '^\s*(export |source |\. |PATH=|eval )' "$file" | grep -v -E '(shopt|bash_completion|PS1=|PROMPT_|HISTCONTROL|HISTSIZE|HISTFILESIZE|__git_ps1|BASH_)'
fi
done
Read the output and identify portable statements that should carry over to zsh. Common ones to include:
- NVM: the
export NVM_DIR/source nvm.sh/nvm bash_completionblock - Custom PATH entries:
~/.local/bin, tool-specific paths, cargo, go, etc. - SSH agent:
eval "$(ssh-agent)"or keychain setup - Custom env vars:
export EDITOR,export GOPATH, etc. - pyenv/rbenv/fnm init:
eval "$(pyenv init -)"etc.
Things to exclude (bash-specific, already handled by Oh My Zsh, or not portable):
shoptcommands- bash-completion sourcing
PS1/PROMPT_COMMAND(p10k handles this)HISTCONTROL/HISTSIZE/HISTFILESIZE(Oh My Zsh sets these)- Anything referencing
BASH_variables
Read ~/.zshrc using Read tool, then use Edit tool to append the portable statements before the p10k sourcing line ([[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh) if it exists, or at the end of the file otherwise. Wrap them in a clearly marked block:
# --- Ported from bash config ---
<portable statements here>
# --- End ported from bash config ---
After appending, verify key commands are resolvable:
zsh -c 'source ~/.zshrc 2>/dev/null; for cmd in node npm pnpm git; do which $cmd 2>/dev/null && echo "$cmd: OK" || echo "$cmd: NOT FOUND"; done'
Report results to the user. If commands like node are missing, suggest they check NVM was ported correctly or run nvm install --lts in a new zsh session.
If no portable statements are found in bash configs, skip this step and inform the user that no environment setup needed porting.
11. VS Code Terminal Font Configuration
Use the FONT_CHOICE from Step 4 to determine the font family name:
FONT_CHOICE = "meslo"→ font family:"MesloLGS NF"FONT_CHOICE = "fira"→ font family:"FiraCode Nerd Font"
Set "terminal.integrated.fontFamily": "<font family>" in all VS Code settings files — both the default and any profile-specific ones. VS Code profiles store their own settings.json that overrides the default.
First, locate the VS Code config directory and find all settings.json files:
if [[ "$(uname)" == "Darwin" ]]; then
VSCODE_DIR="$HOME/Library/Application Support/Code"
else
VSCODE_DIR="$HOME/.config/Code"
fi
# Default settings
echo "${VSCODE_DIR}/User/settings.json"
# Profile-specific settings (override default)
find "${VSCODE_DIR}/User/profiles" -name "settings.json" 2>/dev/null
For each settings.json found, read it using the Read tool, then:
- If
"terminal.integrated.fontFamily"already exists, use Edit tool to update its value to the chosen font family name - If it does not exist, use Edit tool to add
"terminal.integrated.fontFamily": "<font family>"inside the top-level JSON object (after the opening{) - If the file does not exist, create it with Write tool containing:
{ "terminal.integrated.fontFamily": "<font family>" }
Report which files were updated and which already had the correct value.
12. Configure Windows Terminal Shift+Enter (WSL only)
Skip this step entirely if the environment is not WSL.
Windows Terminal submits on plain Enter and does nothing on Shift+Enter by default, which makes multi-line input in CLIs like Claude Code awkward. Add a keybinding so Shift+Enter inserts a newline.
Locate the Windows Terminal settings.json (checks stable, preview, and unpackaged locations):
WINUSER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r')
for p in \
"/mnt/c/Users/${WINUSER}/AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json" \
"/mnt/c/Users/${WINUSER}/AppData/Local/Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json" \
"/mnt/c/Users/${WINUSER}/AppData/Local/Microsoft/Windows Terminal/settings.json"; do
[[ -f "$p" ]] && echo "FOUND: $p"
done
If nothing is found, report that Windows Terminal settings were not located and skip this step.
Read the found settings.json with the Read tool. Check whether a shift+enter binding already exists (search for "shift+enter"). If it does, report it as already configured and skip.
If not, use the Edit tool to add an entry as the first element of the top-level keybindings array (right after the opening [):
{
"command": { "action": "sendInput", "input": "\n" },
"keys": "shift+enter"
},
Preserve the existing indentation and trailing commas. Windows Terminal auto-reloads settings on save — tell the user to open a new tab/window to pick up the change.
If \n still submits instead of inserting a newline on the user's terminal build, \r is the fallback value.
13. Fix Powerlevel10k Right Prompt Wrapping (optional)
On narrow terminals, right prompt segments on line 1 cause powerline cap symbols to wrap and create graphical artifacts when resizing the terminal window. This fix removes all line 1 right segments and clears the cap symbols that render as invisible artifacts even when no segments are shown.
Trade-off: All right-side prompt info (exit code, execution time, node/python/etc. versions, background jobs, etc.) will no longer be visible. Only the left prompt remains.
Use AskUserQuestion to ask: "Do you want to fix p10k right prompt wrapping artifacts on narrow terminals? (Trade-off: all right-side prompt info — exit code, execution time, version managers, etc. — will no longer be visible)" with options "Yes, apply fix" and "No, skip".
If the user skips, proceed to Step 14.
If the user accepts:
Check if ~/.p10k.zsh exists:
test -f ~/.p10k.zsh && echo "EXISTS" || echo "MISSING"
If MISSING: inform the user — "The fix requires ~/.p10k.zsh. It is generated when you run the p10k wizard, which launches automatically on the first exec zsh. Run the wizard first, then re-apply this fix manually or re-run the command." Skip the rest of this step.
If EXISTS, read ~/.p10k.zsh using the Read tool, then apply these changes with the Edit tool:
1. Clear line 1 right prompt segments — replace the entire POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(...) block with one that has no line 1 segments and only newline on line 2. The exact content of the block will vary, but use Edit tool to replace from the opening typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( line through the closing ) with:
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
# =========================[ Line #1 ]=========================
# (all line 1 segments removed to prevent wrapping/artifacts on narrow terminals)
# =========================[ Line #2 ]=========================
newline
)
2. Clear right prompt powerline cap symbols — these render invisible frame artifacts even when no segments are present. Find and replace:
POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0BA'→POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL=''POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0BC'→POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL=''
Note: the exact unicode values may differ depending on the style chosen in the p10k wizard — search for RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL and RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL by name, not by value.
Run to apply immediately:
source ~/.p10k.zsh
14. Install glow
glow renders markdown in the terminal, but glow 3.x word-wraps at a hard-coded 80 columns and never reads the real terminal size — wide tables get shredded to one character per column unless the width is passed explicitly on every call.
Skip this step if which glow returned INSTALLED in Step 2.
Install the binary
If MACOS, or LINUX/WSL with linuxbrew available:
brew install glow
If LINUX or WSL without brew - glow lives in Charm's own apt repo, not in the Ubuntu archive.
IMPORTANT: Claude cannot run sudo commands. Output the following and wait for confirmation:
# Please run these commands manually, then confirm when done:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install glow -y
Use AskUserQuestion to ask "Have you finished installing glow?" with options "Yes, done" and "Skip".
Verify:
glow --version
Build the wide style
This is the upstream dark style with two changes: the 2-column document margin dropped (buys back 4 columns of table), and box-drawing table separators instead of ASCII | and -.
Starting from the full upstream file is not optional — a hand-written partial JSON throws away every colour in the theme, since custom styles replace the built-in theme rather than merge into it.
mkdir -p ~/.config/glow
curl -fsSL https://raw.githubusercontent.com/charmbracelet/glamour/master/styles/dark.json \
| jq '.document.margin = 0
| .table = {center_separator:"┼", column_separator:"│", row_separator:"─"}' \
> ~/.config/glow/glow-wide.json
Requires jq. If it is missing, sudo apt install jq -y or brew install jq.
Verify the file is valid and patched:
jq -e '.document.margin == 0 and (.table.column_separator == "│") and (.heading | length > 0)' \
~/.config/glow/glow-wide.json && echo "style OK"
The .heading check is the one that matters — it confirms the upstream theme survived rather than getting replaced by a two-key stub.
This pins rendering to the dark theme, giving up glow's auto light/dark detection. On a dark terminal auto already resolves to dark, so it costs nothing there. On a light terminal, skip this section and drop the -s flag from the functions below.
Write the glow config
Use the Write tool to create ~/.config/glow/glow.yml:
# style name or JSON path (default "auto")
style: "auto"
# mouse support (TUI-mode only)
mouse: false
# use pager to display markdown
pager: false
# word-wrap at width. Fallback only, for scripts and non-zsh shells. The glow()
# function in ~/.zshrc passes -w $COLUMNS on every interactive call and wins.
width: 120
# show all files, including hidden and ignored.
all: true
Do not set width: 80 here — that is glow's own default and it is the thing that shreds tables.
Configure ~/.zshrc
Read ~/.zshrc using the Read tool. If it already contains # >>> setup-zsh glow >>>, the block is configured - skip this step.
Otherwise use the Edit tool to append the block at the end of the file. These are function definitions, so unlike the keybinding blocks they do not need to land before the p10k sourcing line.
# >>> setup-zsh glow >>>
# glow 3.x word-wraps at a hard-coded 80 columns and never reads the real
# terminal size, so wide markdown tables get shredded to one character per
# line. The width has to be passed on every call. glow-wide.json is the
# built-in dark style with the 2-column document margin dropped and
# box-drawing table separators; both functions fall back to the built-in
# style if that file is missing.
glow() {
local -a style
[[ -r $HOME/.config/glow/glow-wide.json ]] && style=(-s "$HOME/.config/glow/glow-wide.json")
command glow "${style[@]}" -w "${COLUMNS:-100}" "$@"
}
# For tables too wide to wrap readably: render unwrapped and scroll sideways
# with the arrow keys. less -R keeps the colours, -S chops lines instead of
# wrapping them.
glowide() {
local -a style
[[ -r $HOME/.config/glow/glow-wide.json ]] && style=(-s "$HOME/.config/glow/glow-wide.json")
PAGER='less -RSX' command glow "${style[@]}" -p -w 0 "$@"
}
# <<< setup-zsh glow <<<
Notes on the shape of this block, since each part is load-bearing:
- Functions, not aliases.
alias glow='glow -w $COLUMNS'also works in zsh, but a function composes with the-sguard and reads better. command glowstops the function recursing into itself.${COLUMNS:-100}is re-evaluated per call, so it tracks window resizes. zsh maintainsCOLUMNSautomatically. The100fallback covers the case where the function is called from a script with no tty.- The
-rguard on the style file keeps glow working ifglow-wide.jsonis missing or unreadable, rather than failing on a bad-spath. -Xon less keeps the rendered table in scrollback after quitting instead of wiping it.- TUI mode is unaffected. Bare
glowwith no file argument still opens the browser UI; the extra flags are accepted and ignored there.
Verify
printf '| a | b | c |\n|---|---|---|\n| %s | %s | %s |\n' \
"$(head -c 120 /dev/urandom | base64 | tr -d '\n')" "short" "also short" > /tmp/glowtest.md
zsh -i -c 'glow /tmp/glowtest.md' | head -5
The table should fill the terminal width rather than stopping at 78 columns. For an exact check:
zsh -i -c 'glow /tmp/glowtest.md' | sed 's/\x1b\[[0-9;]*m//g' \
| awk '{ if (length($0) > m) m = length($0) } END { print "widest line:", m }'
Compare that number against tput cols. They should match. If it prints 78 or 80, the function is not loaded or command glow is picking up a width: 80 from glow.yml.
15. Install mdv (Neovim Markdown viewer, optional)
mdv FILE.md views Markdown in a throwaway Neovim profile: / search, mouse, live reload that keeps the scroll position while a file is still being written, wide pipe tables reflowed into box tables that fit the window, and copy-on-select — a mouse selection lands on the system clipboard the moment the button is released, with a "copied N lines to clipboard" toast, the way Herdr does it. glow (Step 14) stays useful for one-shot renders; mdv is for watching a plan or report while an agent writes it.
Everything installs under $HOME — no sudo. Needs git, curl, tar and access to GitHub.
Use AskUserQuestion to ask "Install mdv, a Neovim-based Markdown viewer with live reload and copy-on-select?" with options "Yes" and "Skip". If skipped, continue with Step 16.
Skip the whole step if Step 2 reported mdv: INSTALLED; report it as already installed.
Neovim 0.10 or newer
The launcher runs ~/.local/bin/nvim. Check what is there:
test -x ~/.local/bin/nvim && ~/.local/bin/nvim --version | head -1 || echo "MISSING"
-
0.10 or newer → keep it and continue with the next section.
-
MISSING, but
command -v nvimfinds a 0.10+ build elsewhere (Homebrew, apt) → link it and continue:mkdir -p ~/.local/bin && ln -s "$(command -v nvim)" ~/.local/bin/nvim -
Otherwise → install the pinned release tarball into
~/.local/opt. Distribution packages are often older than 0.10 (Ubuntu 24.04 ships 0.9.5), and the config needsvim.base64,vim.ui.clipboard.osc52andnvim_ui_send, all 0.10 APIs. A system Neovim is never touched.
NVIM_VERSION=v0.12.4
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) ASSET=nvim-linux-x86_64 ;;
Linux-aarch64) ASSET=nvim-linux-arm64 ;;
Darwin-arm64) ASSET=nvim-macos-arm64 ;;
Darwin-x86_64) ASSET=nvim-macos-x86_64 ;;
*) echo "unsupported platform: $(uname -s)-$(uname -m)"; exit 1 ;;
esac
mkdir -p ~/.local/opt ~/.local/bin
curl -fsSL -o "/tmp/$ASSET.tar.gz" "https://github.com/neovim/neovim/releases/download/$NVIM_VERSION/$ASSET.tar.gz"
[[ "$(uname -s)" == Darwin ]] && xattr -c "/tmp/$ASSET.tar.gz"
tar -xzf "/tmp/$ASSET.tar.gz" -C ~/.local/opt
ln -sfn "$HOME/.local/opt/$ASSET/bin/nvim" ~/.local/bin/nvim
~/.local/bin/nvim --version | head -1
The xattr -c on macOS drops the quarantine flag so Gatekeeper does not block the unsigned binary.
Copy the launcher and config
The files live in this skill's assets/mdv/ (in the same skill directory as this file). NVIM_APPNAME=mdv keeps the config in ~/.config/mdv, so an existing ~/.config/nvim is never touched.
mkdir -p ~/.config/mdv/lua
cp <this-skill-dir>/assets/mdv/init.lua <this-skill-dir>/assets/mdv/lazy-lock.json ~/.config/mdv/
cp <this-skill-dir>/assets/mdv/lua/mdreflow.lua <this-skill-dir>/assets/mdv/lua/mdview.lua ~/.config/mdv/lua/
install -m 755 <this-skill-dir>/assets/mdv/mdv ~/.local/bin/mdv
Install the plugins
lazy.nvim bootstraps itself on the first start, and Lazy! restore checks out the commits pinned in lazy-lock.json. Headless, about ten seconds:
NVIM_APPNAME=mdv ~/.local/bin/nvim --headless "+Lazy! restore" +qa 2>&1 | tail -3
ls ~/.local/share/mdv/lazy
Expect five directories: catppuccin, lazy.nvim, render-markdown.nvim, rose-pine, tokyonight.nvim.
Make sure ~/.local/bin is on PATH
zsh -i -c 'command -v mdv' 2>/dev/null || echo "NOT ON PATH"
If it prints NOT ON PATH, read ~/.zshrc with the Read tool and, unless a .local/bin PATH export is already there, use the Edit tool to append:
# >>> setup-zsh local-bin >>>
export PATH="$HOME/.local/bin:$PATH"
# <<< setup-zsh local-bin <<<
Verify
NVIM_APPNAME=mdv ~/.local/bin/nvim --headless "+lua print(vim.g.clipboard.name)" +qa 2>&1 | tail -1
Expect osc52. Then tell the user:
mdv is installed. Try it on any Markdown file: mdv README.md
/ search q quit g? all keys
Drag-select with the mouse: the text is on your clipboard the moment you release the button.
Keyboard: v or V to select, y to copy.
Several files: mdv docs/*.md (Tab / Shift-Tab switches). From a pipe: some-command | mdv
Copies travel as OSC 52 through the terminal, so they reach the Windows clipboard from WSL without clip.exe (which mangles non-ASCII text) and work inside Herdr panes. Details, troubleshooting and uninstall: this skill's references/mdv.md.
16. Install btop
btop is a resource monitor (CPU, memory, disks, network, process list) with a much nicer terminal UI than top/htop. Standalone tool, no .zshrc wiring needed.
Skip this step if which btop returned INSTALLED in Step 2.
If MACOS, or LINUX/WSL with linuxbrew available:
brew install btop
If LINUX or WSL without brew:
IMPORTANT: Claude cannot run sudo commands. Output the following and wait for confirmation:
# Please run this command manually, then confirm when done:
sudo apt install btop -y
Use AskUserQuestion to ask "Have you finished installing btop?" with options "Yes, done" and "Skip".
Verify:
btop --version
17. Install herdr, herdr-spin, and agent notifications (optional)
herdr is a terminal workspace manager for running AI coding agents (Claude Code, Codex, etc.) in one window, with a sidebar that tracks each agent's status. Nothing else in this skill depends on it — this step only sets it up (and two optional extras) for people who use or want to try it.
1. Check whether herdr is installed, using the official docs' own check.
which herdr 2>/dev/null && herdr --version || echo "NOT INSTALLED"
2. If NOT INSTALLED, use AskUserQuestion: "herdr isn't installed — it's a terminal
workspace manager for running AI coding agents (Claude Code, Codex, etc.) with a sidebar
that tracks each agent's status. Install it now?" with options "Yes, install herdr" and
"No, skip this step".
-
If the user says no, skip the rest of this step (including herdr-spin and notifications below) and move on to Step 18.
-
If yes, install it with herdr's official installer — this works the same on Linux, macOS, and WSL, and does not need sudo (it installs to the user's own PATH, e.g.
~/.local/bin):curl -fsSL https://herdr.dev/install.sh | shVerify:
herdr --versionIf the install or verification fails, report the exact error to the user and skip the rest of this step — do not retry automatically (see Error Handling below).
3. herdr is now installed (either just now, or already). Check its version against herdr-spin's minimum (0.8.2), and check what's already configured:
herdr --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
test -f "$HOME/.config/herdr-spin/spin.py" && echo "herdr-spin: INSTALLED" || echo "herdr-spin: MISSING"
grep -q '\[ui\.toast\]' "$HOME/.config/herdr/config.toml" 2>/dev/null && echo "notifications: CONFIGURED" || echo "notifications: MISSING"
Compare the version against 0.8.2 (e.g. printf '%s\n0.8.2\n' "$VERSION" | sort -V | head -1 —
if that prints 0.8.2, the installed version is new enough). If it's older, tell the
user herdr-spin needs 0.8.2+ and suggest herdr update (or their package manager, if
they installed via Homebrew/mise/Nix), then don't offer herdr-spin below — notifications
have no version requirement and can still be offered.
Only ask about things that are still MISSING (and, for herdr-spin, only if the version
check passed). If both are already configured, report that and skip to Step 18.
Use AskUserQuestion (multiSelect) with whichever of these still apply: "herdr is installed. Set up any of these?" — "Animated agent spinner (herdr-spin)" (herdr dropped its built-in spinner in 0.8.0 for performance reasons, with no setting to bring it back — this plugin restores one glyph per agent state in the sidebar) and "Agent notifications" (a toast when a background agent finishes or needs input, so you don't have to keep glancing at the sidebar).
If the user picks neither, skip to Step 18.
4. If "Animated agent spinner (herdr-spin)" was picked:
a. Copy this skill's assets/herdr-spin/spin.py and assets/herdr-spin/herdr-plugin.toml
(in the same skill directory as this file) into ~/.config/herdr-spin/:
mkdir -p ~/.config/herdr-spin
cp <this-skill-dir>/assets/herdr-spin/spin.py <this-skill-dir>/assets/herdr-spin/herdr-plugin.toml ~/.config/herdr-spin/
b. Register the plugin with herdr:
herdr plugin link ~/.config/herdr-spin
herdr plugin list
Confirm the output lists local.spin ... enabled. If it doesn't, report the output
to the user and stop this sub-step.
c. Update herdr's sidebar config. Read ~/.config/herdr/config.toml with the Read tool.
This edit is additive — never overwrite an existing config file with a fresh one:
- If the file has no
[ui.sidebar.agents]table, use Edit to append one. - If it already has one, merge
rowsinto it instead of adding a second table.
Target shape:
[ui.sidebar.agents]
rows = [
[
{ token = "$w", fg = "#fabd2f", bold = true, dim = false },
{ token = "$b", fg = "#fb4934", bold = true, dim = false },
{ token = "$d", fg = "#b8bb26", dim = false },
{ token = "$i", fg = "#b8bb26", dim = false },
"workspace",
"tab",
],
["agent"],
]
This drops state_icon (herdr's default first column) in favor of four metadata
tokens, one per agent state — a metadata token carries a single fixed colour, so the
plugin needs one per state rather than one shared column. The hex values are herdr's
gruvbox palette; on another theme, swap them for that theme's yellow/red/green.
d. Apply the config and start the animator:
herdr server reload-config
python3 ~/.config/herdr-spin/spin.py
Herdr only fires the plugin's [[startup]] hook when its server starts, not when a
plugin is linked, so this first run has to be by hand.
e. Verify:
test -f "/tmp/herdr-spin-$UID/spin.lock" && echo "animator running" || echo "animator not running"
If not running, check /tmp/herdr-spin-$UID/spin.log for errors.
Tell the user: restart after editing the script with
python3 ~/.config/herdr-spin/spin.py --restart, and see this skill's
references/herdr-spin.md for tuning the frame rate, troubleshooting a blank icon
column, and uninstalling.
5. If "Agent notifications" was picked:
Read ~/.config/herdr/config.toml with the Read tool — same additive-merge caution as
above. Use Edit to append (or Write to create the file if it doesn't exist):
[ui.toast]
delivery = "herdr"
delay_seconds = 1
[ui.toast.herdr]
position = "bottom-right"
delivery = "herdr" is an in-app toast. Mention the alternatives to the user: "terminal"
also works over SSH (an outer-terminal notification instead of an in-app one), and
"system" uses the OS notification service — on macOS this needs
brew install terminal-notifier for full functionality (click-to-focus); without it,
herdr falls back to osascript, which shows up as "Script Editor" in Notification Center
and can't focus the terminal.
Apply:
herdr server reload-config
18. Apply Configuration
Run using Bash tool to verify the config is valid:
zsh -c 'source ~/.zshrc && echo "Config loaded successfully"' 2>&1 | head -20
If this produces errors, report them to the user. Minor warnings about "no tty" or p10k configuration are expected and can be ignored.
Inform the user:
Setup complete! To apply changes:
- Close and reopen your terminal, OR
- Run: exec zsh
On first launch, Powerlevel10k will start its configuration wizard (p10k configure).
You can re-run it anytime with: p10k configure
Error Handling
If any step fails:
- Report the specific command that failed and the error message
- For network errors (git clone, curl): suggest checking internet connectivity and retrying
- For permission errors: output the exact command the user needs to run manually with sudo
- DO NOT retry failed commands automatically - ask the user how to proceed
Important Notes
- NEVER run sudo commands directly - always instruct the user to run them manually
- NEVER clone git repos into the current directory - always specify the full target path
- Skip components that are already installed - report them as "already installed"
- Detect WSL vs native Linux vs macOS first - this affects Zsh installation and font paths
- Use
--unattendedflag for Oh My Zsh installer to prevent interactive prompts - On macOS, Zsh is pre-installed - skip Zsh installation, fonts go to
~/Library/Fonts - On macOS,
brewis used instead ofaptif any packages are needed - glow never auto-detects terminal width. If tables render squeezed, the
glow()function from Step 14 is not loaded - checkwhich glowreturns a function rather than/usr/bin/glow. - mdv copies over OSC 52. If a selection shows the "copied" toast but the clipboard is unchanged, the terminal or multiplexer dropped the OSC 52 write — see
references/mdv.md.