Skip to content

feat(zsh): 10x faster startup, fzf bindings, non-interactive PATH, startup budget - #47

Merged
garygentry merged 5 commits into
mainfrom
feat/zsh-startup-correctness
Sep 27, 2026
Merged

garygentry merged 5 commits into
mainfrom
feat/zsh-startup-correctness

Conversation

@garygentry

@garygentry garygentry commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

zsh correctness and speed for the zinit setup. No new tools. With no content overlay, behaviour is the same apart from speed and the fixes below.

  • About 10× faster interactive startup (measured on Ubuntu 22.04: about 700 ms to about 70 ms warm). zprof showed compinit + compdump at about 300 ms, because the dump was audited and rewritten on every start.
    • compinit now uses a cached dump (-C) under $XDG_CACHE_HOME/zsh/zcompdump-$ZSH_VERSION, fully rebuilt when older than 24 h.
    • zinit turbo defers autosuggestions and syntax highlighting until just after the first prompt.
    • zsh-completions is blockf, so it only extends fpath.
  • Syntax highlighting now loads last, as upstream requires. The old template loaded it first.
  • fzf key bindings are finally wired. The fzf module installs with --no-update-rc, so nothing sourced them before.
    • Uses fzf --zsh on fzf 0.48 and later. Older builds fall back to scripts from the same install as the binary, because mixing versions breaks the widgets.
    • Skipped under zsh -i -c, where there is no line editor and fzf's option restore prints can't change option: zle.
  • ~/.zshenv managed PATH block, so non-interactive zsh (ssh host cmd, zsh -c, agent tool shells) also sees ~/.local/bin. It uses a new upsert_managed_block helper: marker-delimited, replaced in place, never duplicated, user lines untouched.
  • History: 100k entries, EXTENDED_HISTORY, HIST_EXPIRE_DUPS_FIRST. setopt CORRECT removed.
  • verify.sh reports the median interactive startup time. It fails only when modules.zsh.startup_budget_ms is set.
  • Fix: zsh -i -c exit returned 1 when ~/.zshrc.local was absent, because the rc file ended with a false [[ ]] &&.

Part of the estate's shell/toolchain track (private ADRs 0042/0043). Phase 2, an opt-in mise module, follows in a separate PR and builds on upsert_managed_block.

Test plan

  • make test-integration-ubuntu: 51 passed, 0 failed. New assertions cover the .zshenv block present and not duplicated, ~/.local/bin first in non-interactive zsh, zsh -i with empty stderr and rc 0, and the cached dump location.
  • Rendered both framework variants (zinit, ohmyzsh) and checked syntax with zsh -n.
  • Tested in a throwaway HOME:
    • cold start about 800 ms (dump rebuild), warm 57–85 ms
    • ^R/^T/Alt-C bound to fzf widgets with fzf 0.70
    • old apt fzf with no scripts gets no bindings and no errors
    • turbo plugins confirmed loaded after the prompt through a pty
  • verify.sh budget: unset reports 58 ms; 150 passes; 10 fails with rc 1.
  • shellcheck clean on the touched scripts.
  • CI: Arch integration run and Go unit tests. No Go code changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT

Review round 2 (adversarial, 2026-09-26): all addressed

  • Bug: an aborted compinit left an empty dump, which was trusted for 24 h (no completions). Now the dump is only touched after a successful, non-empty dump, and an empty dump counts as missing. Reproduced with an insecure fpath dir, and the fix was verified.
  • Bug: the startup budget failed verify. The runner uses verify to decide whether to skip a module, so this caused a reinstall on every run. The budget is now a warning, and timing takes 3 runs, reported before the summary.
  • Risk: Ubuntu's global compinit still ran on every start (/etc/zsh/zshrc). ~/.zshenv now sets skip_global_compinit=1.
  • Risk: macOS zsh -lc PATH order. The PATH block is mirrored into ~/.zprofile.
  • Nits: idempotency is now asserted after a forced zsh re-run; the ~/.zcompdump assertion is self-contained; HIST_EXPIRE_DUPS_FIRST was dropped (it did nothing here).

Integration: 54 passed, 0 failed.

hal canary (pre-merge pin, the engine at feat/mise-module head, which contains this PR)

before (bf9f604) after (bf163d5)
interactive start (zsh -i </dev/null) about 600 ms about 60 ms
^R zsh builtin fzf-history-widget
zsh -c 'node; pi' not found found (~/.local/bin first)
install — 26/26 OK; a re-run skips all 26
~/.zcompdump in HOME rewritten every start untouched

garygentry and others added 3 commits September 25, 2026 23:00
…artup budget

- compinit from a cached dump (-C) under XDG_CACHE_HOME, full rebuild when >24h old;
  was a full audit + dump rewrite (~300ms) every start
- zinit turbo for autosuggestions + syntax-highlighting; highlighting now loads LAST
  (the old template loaded it first); zsh-completions is fpath-only (blockf)
- fzf key bindings wired: fzf --zsh (>=0.48) or the same install's scripts; skipped
  under zsh -i -c (no ZLE; fzf's option restore errors there)
- ~/.zshenv managed PATH block so non-interactive zsh (ssh host cmd, agent shells)
  sees ~/.local/bin; new upsert_managed_block helper (in-place, idempotent)
- history 100k + EXTENDED_HISTORY + HIST_EXPIRE_DUPS_FIRST; drop setopt CORRECT
- verify.sh reports median interactive startup; fails only if
  modules.zsh.startup_budget_ms is set

Measured on Ubuntu 22.04: ~700ms -> ~70ms warm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…an interactive start

The rc's last statement ([[ -f ~/.zshrc.local ]] && source) left $?=1 when the file is
absent, so every zsh -i -c caller inherited a failure. Integration test now asserts
empty stderr + rc 0 for zsh -i, and the cached completion dump location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
…dation

- upsert_managed_block: refuse (rc 1, file untouched) on unpaired markers instead of
  deleting user lines after an unterminated begin; collapse duplicate blocks to one and
  settle; match markers ignoring CR so a CRLF file isn't given a second block
- touch the completion dump after the daily full compinit: compinit only rewrites it
  when the completion set changed, so once 24h old every shell stayed on the slow path
- verify: time 'zsh -i </dev/null' (includes setup that -c skips); warn on and ignore a
  non-numeric startup_budget_ms instead of silently passing
- fzf bindings also require a terminal on stdin (no zle errors for 'cmd | zsh -i')

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
garygentry and others added 2 commits September 26, 2026 20:43
…y, skip global compinit

- only refresh the compinit dump mtime after a successful, non-empty dump, and treat an
  empty dump as missing: an aborted compinit (insecure dirs, prompt declined/EOF) plus
  the unconditional touch left an empty dump that -C trusted for 24h (no completions)
- startup budget is a WARNING: the runner runs verify.sh to decide skips, and a failure
  there reinstalls the module on every run; timing is 3 runs, reported before the summary
- ~/.zshenv sets skip_global_compinit=1 so Debian/Ubuntu /etc/zsh/zshrc no longer runs a
  full compinit (and writes ~/.zcompdump) on every start
- macOS: mirror the PATH block into ~/.zprofile (path_helper reorders login-shell PATH)
- drop HIST_EXPIRE_DUPS_FIRST (no effect with HIST_IGNORE_ALL_DUPS and HISTSIZE=SAVEHIST)
- integration: idempotency asserted after a FORCED zsh re-run; skip_global_compinit and
  no ~/.zcompdump asserted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
Shells run before the managed .zshenv block existed can leave a ~/.zcompdump; remove it,
start a fresh interactive shell, then assert the distro global compinit didn't recreate it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S29GMkRB43pGsAUBUXwLdT
@garygentry
garygentry merged commit 9776031 into main Sep 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant