Modern-CLI aliases (eza, bat, rg, fd…), each guarded by a capability check (HAVE_* flags) so a missing tool never breaks the shell.
# core/zsh/20-aliases.zsh
# ──────────────────────────────────────────────────────────────────────────────
# Aliases for the modern CLI stack. Every alias touching an optional tool is
# GUARDED by a HAVE_* flag from 00-tools.zsh, so on a bare box (fresh server, rescue
# shell) you transparently get the classic command. Load AFTER 00-tools.zsh.
# Anything offensive/engagement-flavoured lives in dotfiles-Offense, not here.
# ──────────────────────────────────────────────────────────────────────────────
# ── classic-name shadows: CORE_SHADOW_CLASSICS=0 opts out (#1155) ────────────
# Some aliases below take over a STANDARD command name (ls, cat, cd, vim, diff, rm/cp/mv
# -i, mkdir -p, tree, du, ps, top, watch, df, ping, help). Each one's trailing comment
# starts with `shadow`, which is also its Note in aliases.md. Two of them are not just
# cosmetic: `cd`→`z` changes how a typo FAILS (it jumps by frecency instead of erroring),
# and `rm -i` trains you to expect a prompt that no foreign box will give. Set
# CORE_SHADOW_CLASSICS=0 in the ENVIRONMENT (e.g. ~/.zshenv) to keep the classic
# meanings. The host-local 95-99 modules load too late to set it; there, unalias instead.
# The names that collide with nothing stay: ll la lt llt catp cdi bat, and the tools
# under their own names (eza, bat, z, nvim, dust, …). Removing the shadows outright
# is a MAJOR that waits for a real incident — V8-PROPOSAL.md's non-goals, next to #692.
_core_shadow() { [[ ${CORE_SHADOW_CLASSICS:-1} != 0 ]] }
# ── ls -> eza ─────────────────────────────────────────────────────────────────
if [[ -n ${HAVE_EZA:-} ]]; then
_core_shadow && alias ls='eza --group-directories-first --icons=auto' # shadow
alias ll='eza -lah --group-directories-first --icons=auto --git'
alias la='eza -a --group-directories-first --icons=auto'
alias lt='eza --tree --level=2 --icons=auto'
alias llt='eza --tree --level=3 -l --icons=auto'
_core_shadow && alias tree='eza --tree --icons=auto' # shadow
(($+functions[compdef])) && compdef eza=ls # reuse ls completion for eza
else
alias ll='ls -lah'
alias la='ls -A'
fi
# ── cat -> bat (resolved name from 00-tools.zsh) ────────────────────────────────
if [[ -n ${HAVE_BAT:-} ]]; then
_core_shadow && alias cat="$BAT_BIN --paging=never" # shadow
alias catp="$BAT_BIN" # paged, full bat
# …and `bat` under its CANONICAL name, mirroring the fd line below. Debian/Ubuntu/Kali
# ship the binary as `batcat`, so without this the tool was installed and fully wired
# (cat, catp, MANPAGER, the fzf previews) yet untypeable by the name its own README, man
# page and every upstream recipe use. The two renamed tools were handled asymmetrically —
# fd got this alias, bat did not — which is also what made core-doctor report `✗ bat` two
# lines above a `resolved` section naming batcat. Harmless `alias bat=bat` elsewhere: zsh
# does not re-expand an alias to its own name in command position.
alias bat="$BAT_BIN" # batcat on the Debian family, bat elsewhere
export BAT_THEME="ansi" # follow the terminal palette (tokyonight via ghostty)
export MANPAGER="sh -c 'col -bx | $BAT_BIN -l man -p'"
fi
# ── find -> fd ────────────────────────────────────────────────────────────────
[[ -n ${HAVE_FD:-} ]] && alias fd="$FD_BIN" # fdfind on the Debian family, fd elsewhere
# ── grep stays POSIX for scripts; rg is its own command (smart-case default) ──
[[ -n ${HAVE_RG:-} ]] && alias rg='rg --smart-case'
# ── cd -> zoxide (z), interactive jump (zi), `-` to previous dir ─────────────
if [[ -n ${HAVE_ZOXIDE:-} ]]; then
_core_shadow && alias cd='z' # shadow · zoxide: frecency-ranked directory jump
alias cdi='zi' # interactive jump (pick from matches)
fi
alias -- -='cd -' # previous directory
# ── disk / process / monitor ──────────────────────────────────────────────────
[[ -n ${HAVE_DUST:-} ]] && _core_shadow && alias du='dust' # shadow
[[ -n ${HAVE_PROCS:-} ]] && _core_shadow && alias ps='procs' # shadow
[[ -n ${HAVE_BTOP:-} ]] && _core_shadow && alias top='btop' # shadow
[[ -n ${HAVE_BTOP:-} ]] && _core_shadow && alias htop='btop' # shadow
[[ -n ${HAVE_VIDDY:-} ]] && _core_shadow && alias watch='viddy' # shadow
# df → duf (modern, mountpoint-aware); classic `df -h` stays the bare-box fallback.
if _core_shadow; then
if [[ -n ${HAVE_DUF:-} ]]; then
alias df='duf' # shadow
else
alias df='df -h' # shadow
fi
fi
# ── file manager ──────────────────────────────────────────────────────────────
[[ -n ${HAVE_YAZI:-} ]] && {
alias fm='yazi'
alias y='yazi'
}
# ── terminal web browser (w3m preferred; BROWSER_BIN resolved in 00-tools.zsh) ──
# When a browser is present, `web <url>` is defined; with none installed this whole
# block is skipped — no alias, no export (the "no browser" row of the PR's table).
# $BROWSER is claimed only on a headless box (SSH / server / WSL-no-X) so it never
# hijacks GUI-opening tools on a desktop; macOS ($OSTYPE=darwin*) always has a GUI,
# so it's skipped too.
if [[ -n ${HAVE_BROWSER:-} ]]; then
alias web="$BROWSER_BIN" # the terminal web browser (see the note under the table)
if [[ -z ${DISPLAY:-} && -z ${WAYLAND_DISPLAY:-} && $OSTYPE != darwin* ]]; then
export BROWSER="$BROWSER_BIN"
fi
fi
# ── 2026 modern stack additions (all guarded; classics untouched) ────────────
# xh: Rust HTTPie — for poking APIs / web targets. curl stays for scripts.
[[ -n ${HAVE_XH:-} ]] && {
alias http='xh'
alias https='xh --https'
}
# glow: render markdown in the terminal (engagement notes, READMEs)
[[ -n ${HAVE_GLOW:-} ]] && alias md='glow --pager'
# doggo: modern dig (DNS recon). dig stays as-is; this is a distinct verb.
[[ -n ${HAVE_DOGGO:-} ]] && alias dns='doggo'
# gron / sd are their own commands (no alias — never shadow sed in scripts).
# jq / yq / jnv / jc / lnav / hyperfine / watchexec / shellcheck / shfmt are likewise their own
# commands: they shadow nothing classic, so they get HAVE_* detection in 00-tools.zsh but
# no alias.
# (jnv is the interactive JSON explorer — you run `jnv file.json` or pipe into it.)
# (lnav is the log reader — `lnav /var/log/...` or a directory; it merges and follows.)
# (watchexec re-runs a command on file changes — `watchexec -e py -- pytest`. NOT aliased
# to `watch`: 20-aliases.zsh already points `watch` at viddy, and conflating "re-run on a
# timer" with "re-run on a change" would silently give you the wrong one.)
# ── editor + misc QoL ─────────────────────────────────────────────────────────
_core_shadow && alias vim='nvim' # shadow
# diff: colourise ONLY when this box's diff actually supports `--color` (GNU does;
# BSD/macOS diff — the dotfiles-MacBook target — and busybox diff on Alpine do NOT,
# where an unconditional alias would make every `diff` invocation error). `--color`
# support is a STABLE property of the box's diff binary, so probing it forks the real
# `diff` on every shell for an answer that never changes. Cache the verdict keyed on the
# binary's mtime (the same invalidation _cache_eval uses): re-probe only when diff is
# newer than the cache — e.g. after a GNU/BSD toolchain change. When the cache dir isn't
# writable the live probe still decides correctly, so correctness never depends on the
# cache. (df → duf/df -h above.)
() {
emulate -L zsh
local bin="${commands[diff]}" cache="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/diff-color"
_core_shadow || return # CORE_SHADOW_CLASSICS=0 → no alias, no probe
[[ -z "$bin" ]] && return # no diff at all → no alias
if [[ -e "$cache" && ! "$bin" -nt "$cache" ]]; then
# fresh cache: zero forks
[[ -s "$cache" ]] && alias diff='diff --color=auto' # shadow
return
fi
# (re)probe once, then persist the verdict (non-empty = supported) for next start.
# `>|` forces the write past 10-options.zsh's NO_CLOBBER (loaded before 20-aliases.zsh).
if diff --color=auto /dev/null /dev/null >/dev/null 2>&1; then
alias diff='diff --color=auto' # shadow
mkdir -p "${cache:h}" 2>/dev/null && print -rn -- 1 >| "$cache" 2>/dev/null
else
mkdir -p "${cache:h}" 2>/dev/null && print -rn -- '' >| "$cache" 2>/dev/null
fi
}
# ── git ───────────────────────────────────────────────────────────────────────
# The git alias set is the single source of truth in 25-git.zsh (OMZ-style, loaded
# right after this file). Two exceptions live here because they launch a separate TOOL
# rather than a git workflow: the `lg` lazygit launcher and the HAVE_DIFFT-gated `gdft`
# below. `lg` is deliberately UNGUARDED — there is no HAVE_LAZYGIT, and on a box without
# lazygit the alias fails exactly as typing `lazygit` would, so a gate would buy nothing.
# git-absorb gets NO alias at all — it installs as the `git absorb` subcommand, so git
# already dispatches it and there is nothing to shadow; see git/gitconfig's `fix` alias.
# 00-tools.zsh sets HAVE_GIT_ABSORB for symmetry with the other detected tools, and nothing
# reads it today — core-doctor probes the tool itself rather than consulting the flag. The
# two are kept in agreement anyway (#425): both look past $PATH, because the Debian familyShowing the first 160 of 229 lines — read the full file ↗