Fix/zsh audit remediation - #10
Open
andrewmcodes wants to merge 43 commits into
Open
Conversation
…run indices
`set -euo pipefail` in an autoloaded zsh function applies to the *caller*, so
running `bench-startup` once left the interactive shell with errexit/nounset
set — it would then exit on the next non-zero command. Replace with
`emulate -L zsh` (which implies local_options) plus a scoped `setopt pipefail`.
The summary also averaged the wrong awk field: lines are `run 1: 0.120s`, so
`$2` is the run index (`1:`), not the time. Every min/avg/max ever printed by
this tool was a run number. Use `$3`.
Also `for i in $(seq 1 $runs)` -> `{1..$runs}` to drop a fork.
Two independent faults made the median a garbage value on real input: - `PROCINFO["sorted_in"]` is a gawk extension and gawk is not installed here (system awk is BSD 20200816, which silently ignores it). - Even under gawk it only affects `for (k in arr)` iteration order, which this code never uses. The array is indexed by NR, so the "median" was simply the middle element in *input* order. `fetch_action_stats` emits reverse-chronological order, so the actual pipeline reported a wrong median every time. Sort in place with an insertion sort -- POSIX awk has no asort(), and this keeps it to a single process. Also guard n == 0 so empty input no longer divides by zero. The three existing fixtures were all pre-sorted, which is precisely why this survived. Adds unsorted odd-, even-, and 7-element cases; all three were confirmed to FAIL against the old implementation first (median=1.0 vs 3.0, 2.0 vs 2.5, 1.0 vs 5.0) so they are proven to have teeth.
… branch names `grep --invert-match $(git branch --show-current)` was unquoted and treated the branch name as a regex. Both failure modes reproduced in a scratch repo: - Detached HEAD: the pattern is empty, so grep aborts with "no PATTERN specified" and the function dies before fzf. - Metacharacters: on a branch named `foo.bar`, `.` matched any character and THREE unrelated branches (`fooXbar`, `feature/foo.bar`, `featureXfooYbar`) were silently hidden from the delete list. `grep -vxF` alone would not fix this -- `git branch` pads output with a 2-char `* `/` ` prefix and `cut -c 3-` runs *after* the grep, so `-x` would never match and the current branch would stop being filtered at all. Use `git for-each-ref` instead: bare names, no prefix to strip, and no `(HEAD detached at ...)` pseudo-entry. Matches the `gb9` alias's idiom.
- `....` ran `cd ../../` -- up TWO levels, identical to `...`. It has meant
this for its entire existence; now `cd ../../..` as the name implies.
- `alias %= \$=` did not "define % as an alias for $=". Zsh parses it as two
arguments and defines two EMPTY aliases, `%` and `$` (verified: `'$'=''`
and `%=''`). Deleted. The paste-a-README intent it was reaching for is a
function, added separately.
- `rT` was double-quoted, so `$2` expanded at definition time and the alias
held `awk '{print }'` -- echoing whole `rails -T` lines instead of the task
name. Escaped as `\$2`.
`zsh -i -c exit` was not silent -- it printed `(eval):1: can't change option: zle` twice. Root-caused with `zsh -o sourcetrace` to `source <(fzf --zsh)`: fzf's completion and key-bindings scripts each snapshot the shell's options into `__fzf_*_options="setopt"` and restore them later via `eval`. Under `-c` that snapshot includes `zle`, which cannot be re-enabled. Two scripts, two warnings. Purely cosmetic in a real terminal, but `zsh -i -c` is exactly what bench-startup and zsh-bench run, so it was noise in every measurement. A `[[ -o zle ]]` guard does not work -- verified that `zle` reads as *set* under `zsh -i -c` even though it can't be restored. Redirection is the only fix that doesn't skip fzf entirely; confirmed all three fzf widgets still bind.
`bindkey -A viins main` with zsh's default KEYTIMEOUT=40 means every <Esc> to leave insert mode waits 400ms before the mode switch registers. Zero startup cost. Not exported: ZLE reads it as a shell parameter. Also documents why `bindkey -v` is load-bearing here — belak/zsh-utils binds emacs, viins and vicmd explicitly but calls neither `bindkey -e` nor `-v`, so this line is the only thing selecting the keymap.
`export FNOX_AGE_KEY=$(cat ~/.config/fnox/age.txt | grep "AGE-SECRET-KEY")` ran in .zshenv, which is sourced by *every* zsh including scripts. It put a raw age private key into the environment of every shell and every child process, and cost two forks plus a subshell (~2.5ms per shell, measured) to do it. It also turned out to be completely redundant. fnox's age provider already defaults its identity to `<config dir>/age.txt`, which resolves to $XDG_CONFIG_HOME/fnox/age.txt -- exactly where the key already lives. Proved it by pointing FNOX_CONFIG_DIR at an empty directory, which fails with "Age identity file not found: <that dir>/age.txt". Verified with FNOX_AGE_KEY unset: `fnox get` succeeds in all four repos that carry a fnox.toml (podia, DropSwipe, andrewm.codes, andrewmcodes-v8), none of which override `key_file`, and `fnox activate zsh` output is byte-identical. So no replacement config is needed -- not the global `[providers.age] key_file` block, and not FNOX_AGE_KEY_FILE/age_key_file (which are the same option as each other and which fnox's own help marks "deprecated: use provider config instead"). A comment records the reasoning so this doesn't get re-added.
Dead config removed (each verified live, not inferred):
- .zstyles: fzf-preview for `cd` called `exa`, which is NOT installed (only
`eza`) -- the cd tab-preview was silently broken. The `systemctl-*` preview is
Linux-only. `list-colors ${(s.:.)LS_COLORS}` was worse than a no-op: LS_COLORS
is never set anywhere here, so it actively set the style to EMPTY (confirmed:
0 elements). `special-dirs false` never took effect either --
compstyle_zshzoo_setup runs after .zstyles and sets `special-dirs ..`
(confirmed live). Both are now documented rather than silently absent.
- .zshenv: a Dracula fzf palette was assigned and then a grey one appended,
giving FIVE --color= flags where the grey palette already covers every key the
four Dracula flags set. Collapsed to one. `--inline-info` -> `--info=inline`
(the file already used the modern spelling two lines down).
ZSH_HIGHLIGHT_HIGHLIGHTERS is read by zsh-syntax-highlighting; this config runs
fast-syntax-highlighting, which ignores it. Also unset the _fzf_* scratch
arrays, which were leaking into every shell.
- 06-commands.zsh: the commented-out `rails()` wrapper.
- 04-opts.zsh: the `is-at-least 5.8` gate around CD_SILENT (this box is 5.9.2);
`autoload -U colors` (nothing reads $fg[]/$reset_color -- SPROMPT uses %F{}).
Setopts consolidated into 04-opts.zsh as the single owner, dropping verified
no-ops: `setopt notify` and `appendhistory` are already zsh defaults,
`unsetopt mail_warning` is already the default, INC_APPEND_HISTORY is implied by
SHARE_HISTORY (zsh's own docs say to turn it off when sharing), and
HIST_IGNORE_DUPS is subsumed by HIST_IGNORE_ALL_DUPS. `unsetopt nomatch` moved
out of 01-hist.zsh, where a global globbing change was hiding among history
settings. Adds HIST_FCNTL_LOCK: SHARE_HISTORY has many shells appending
constantly, and without it zsh uses lock files rather than fcntl locking.
Robustness:
- .zshrc: `(N)` on both the functions/ and rc.d/ globs -- without nullglob an
empty directory aborts the whole rc file with "no matches found". Guarded the
autoload on a non-zero count too, since bare `autoload -Uz` prints the
autoload list. `(.)` so only regular files are sourced.
- .zshrc: quoted + fell back HOMEBREW_PREFIX, which is only exported inside
.zshenv's `darwin*` branch, so the bare form resolved to /opt/antidote/... off
macOS. Added $+commands guards to the mise/fnox evals to match every
rc.d/<tool>.zsh, and documented why those two must stay eager.
- .zshrc: `path=($path)` after the plugin loader. `typeset -gU path` does not
dedupe a scalar `PATH=` assignment, so zsh-bench accumulated a second entry in
nested shells. Measured from a clean parent: before 1/2/2 entries at nesting
depth 1/2/3, after 1/1/1.
- 01-hist.zsh: `$(( SAVEHIST * 12 / 10 ))` instead of `echo ... | bc` (2 forks).
- 06-commands.zsh: `mkcd` took "$@" but cd'd to `$_`, so with multiple args it
entered only the last one -- now single-arg and `--`-safe. pg_* resolved the
version with `awk -F/ '{print $9}'`, which only lands on the version because
$HOME is 2 levels deep; now `:h:h:t`. Extracted `_pg_running_version` for the
duplicated psql query and take the first word, since `show server_version` can
answer `17.2 (Homebrew)` which matches no mise install dir. `curl -fsSL` so
install_casks stops piping a progress meter into jq and fails loudly on HTTP
errors. `print -l` over `echo -e` in print_path. `[[ ]]` over `[ ]`.
- functions/funcs: dropped `local_options` (implied by `emulate -L zsh`) and
declared `c` as the scalar it actually is. functions/os: `[[ ]]` over `[ ]`.
Verified with a behavioural diff against the base commit via a git worktree:
`setopt`, `alias`, `bindkey -M viins`, precmd/chpwd/preexec hooks and `$path`
all differ ONLY in the intended ways. zunit 9/9. Startup 101.6ms -> 97.2ms.
…ions `calculate_actions_stats`, `fetch_action_stats`, `grecent` and `is-macos` had no leading comment, so `funcs` printed them with a blank description even though README.md:157-160 documents one for each. (calculate_actions_stats picked its up in the median fix.) Confirmed through a real TTY that these were exactly the blank rows, and that all 15 entries now carry a description. Note the irony: `grecent` is the file AGENTS.md cites as the exemplar of the convention it was violating.
`rc.d/sharship.zsh` -> `starship.zsh`, `rc.d/zoixide.zsh` -> `zoxide.zsh`. Load order is preserved -- alphabetically `fzf` < `starship` < `zoxide` < `zz-atuin` exactly as before, so atuin still binds last. Verified after the rename: `^R` -> atuin-search-viins, Up -> atuin-up-search-viins, and the precmd/chpwd/preexec hook lists are identical to the base commit. The misspellings had propagated into README.md, AGENTS.md and .github/copilot-instructions.md; all three updated.
Runs a command that prints zsh code, sources it, and caches the output so later
shells skip the fork. About 19ms of this config's ~30ms of startup subprocess
cost is cacheable (atuin 5ms, fnox 5ms, starship 4ms, fzf 3ms, zoxide 2ms);
`mise activate` is deliberately excluded because its output embeds a snapshot of
the generating shell's PATH.
Design notes:
- Keyed on the FULL command line, so `tool init zsh` and `tool init bash` can't
collide.
- Rebuilds via a temp file and only installs it if the command exited 0 AND
produced output, so a broken or half-installed tool can't poison the cache.
- Invalidated both by the tool binary being newer than the cache (so a
`brew upgrade` is picked up automatically) and by a TTL backstop, default 7
days via $ZSH_CACHED_EVAL_TTL.
- Uses `>|` because this config sets NO_CLOBBER.
- Returns 1 silently for a missing command, so callers keep the existing rc.d
idiom. $commands only holds bare names, so a path-ish argument is checked with
-x instead.
- Locals are `_ce_`-prefixed: the function sources third-party code in its own
scope, and an unprefixed `key`/`out`/`tmp` could shadow something that code
expects.
17 zunit tests covering the happy path, cache-hit, both invalidation paths,
keying, --clear/--list, and three poison cases (command fails, command prints
nothing, command missing) -- each asserting no cache file and no temp file is
left behind. One bug found and fixed by these tests: `--clear <pattern>` matched
nothing, because zsh does not glob-expand the result of a parameter expansion
without ${~...}.
First tool converted, as the safest one to verify. Re-confirmed independently that `starship init zsh` output is byte-identical across three different PWD/PATH combinations before caching it. Verified cold then warm: the cache file appears, prompt_starship_precmd is registered, $PROMPT is populated, and the prompt renders. Startup 97.2ms -> 91.1ms.
Keeps the 2>/dev/null from the A6 fix: the "can't change option: zle" warnings come from fzf's own option save/restore code, so they fire when the cached copy is sourced too. Verified `zsh -i -c exit` is still silent, all three fzf widgets bind, ^T is fzf-file-widget, and ^R is still atuin's.
Last of the five cacheable inits. Also documents why the neighbouring `mise activate` stays eager and uncached: its output embeds a snapshot of the generating shell's PATH plus a set of `unset`s, so caching it would freeze $PATH. Re-verified that mise's output changes with PATH while all five cached tools' output does not. Verified after conversion: all 5 cache files present, _fnox_hook registered, `fnox get` still resolves in podia, precmd/chpwd/preexec identical to the base commit, and mise shims still front $path (ruby and node resolve into ~/.local/share/mise/installs).
ez-compinit's only invalidation is a 20-hour mtime check plus a `touch` (confirmed by reading ez-compinit.plugin.zsh) -- it never notices that $fpath changed. So after adding a plugin or a completion file, the new completions could take up to 20 hours to appear, which is what the manual "clear the cached dump" note in .zstyles was working around. rc.d/03-completion.zsh stamps the joined $fpath beside the dump and, when it differs, removes the dump and its .zwc so ez-compinit takes its full `compinit -i` path at the next precmd. ZSH_COMPDUMP is now set explicitly so the stamp and ez-compinit cannot disagree on the path. The stamp read is `$(<file)`, which zsh optimises to no fork. Nothing else in this config touches $fpath after rc.d starts, so the stamp is computed against the final value. Verified the whole cycle: fresh shell writes dump + stamp; an unchanged $fpath is a cache hit (dump mtime untouched); a stale stamp drops both dump and .zwc, rewrites the stamp, and the next interactive shell rebuilds -- after which the cache-hit path resumes.
`g`, `b`, `be`, `cz` and `y` had no completion at all, and `r` was worse than
nothing: it completed as zsh's *builtin* `r` via `_fc`, which is meaningless now
that `r` is aliased to rails. Verified before/after via _comps.
Uses the `compdef` FUNCTION's `name=service` form, called from
rc.d/03-completion.zsh. Two findings behind that choice:
- The `#compdef name=service` FILE-tag form the plan suggested does not exist.
compinit's file tag only accepts `<names>`, `-p`/`-P <patterns>`, `-k` and
`-K`, and registers the file's own function for those names -- so a file
containing just `#compdef g=git` would not delegate to git's completion.
Confirmed by reading /usr/share/zsh/5.9/functions/compinit.
- Calling `compdef` this early is safe: ez-compinit installs a `compdef` shim
that queues calls and replays them when the real compinit runs. Verified by
running the actual precmd chain in order -- all six map to the real functions
(_git, _bundle, _chezmoi, _yarn, _rails) with no forced compinit.
Each mapping is guarded on the target command existing.
Also adds `completions/` to $fpath with `(-/FN)` so a missing directory vanishes
silently, as an extension point for hand-written `_name` files -- adding one
changes $fpath, which the new stamp already turns into an immediate rebuild.
NOT doing the plan's other H6 item: checking in a generated `_mise`/`_starship`.
Homebrew already installs real `_mise`, `_starship`, `_git` and `_chezmoi` files
into share/zsh/site-functions, which is already on $fpath -- a checked-in copy
would shadow those and go stale. The stated rationale ("the only way to reclaim
any of mise's 9ms") also doesn't hold: completion functions are autoloaded lazily
on first use and cost nothing at startup, so they're unrelated to the
`mise activate` cost.
Lists every shell option this config changes from a pristine zsh, and which of
our files is responsible -- the point being to surface duplicate and conflicting
setopts, and to reveal which changed options are NOT ours.
Bare `setopt` already prints only the options that differ from zsh's defaults,
in both directions (`noclobber` as readily as `autocd`), so this is a set
subtraction against `zsh -df -o interactive -c setopt`. One fork, no GNU tools;
the reference implementation's `gdiff --changed-group-format` isn't available
here. Verified the reverse subtraction is never interesting: it can only hold
`norcs`/`noglobalrcs`, artifacts of the `-df`. `interactive` and `zle` are on
both sides and cancel, as intended.
Two bugs found by actually running it, both in the name normalisation:
- `${name#no}` misattributed `nonomatch`. `nomatch` and `notify` are genuinely
named with a `no` prefix, so blind stripping made 04-opts.zsh's
`unsetopt nomatch` normalise to `match` while setopt's `nonomatch` normalised
to `nomatch` -- and our own `unsetopt nomatch` was reported as coming from a
plugin. Now the prefix is stripped only when the word is not itself a key of
`$options`, which is exactly the discriminator zsh uses. (`${+options[nobeep]}`
is NOT that test -- it returns 1 for any valid spelling; the key ARRAY is.)
- `${(z)line}` tokenises a trailing comment into words, so prose after a setopt
was harvested as option names. Now the word loop breaks at the first `#`, and
every candidate must normalise to a real option name, which drops prose for
free. `.zunit.yml` is also no longer scanned -- `.z*` was matching it.
Also: attribution lists every file that touches an option rather than
first-wins, since a duplicate is precisely what this is meant to show; the
`--plugins` footer no longer claims its subset is the whole diff; `--raw` stays
silent when nothing differs instead of printing prose into a pipe; an unknown
flag exits 2 rather than silently behaving like bare mode.
Empirically confirmed against the real config: the five unattributed options are
`alwaystoend`, `completeinword`, `pathdirs` (compstyle_zshzoo_setup),
`noflowcontrol` (belak/zsh-utils editor.plugin.zsh:15) and `promptsubst`
(`starship init zsh`) -- none of them ours, which is why the label says "tool
init" too. All 37 others resolve to rc.d/04-opts.zsh or rc.d/01-hist.zsh.
Tested (8 cases, suite now 34/34): the whole function is driven inside a
pristine `zsh -df -o interactive` with a fixture $ZDOTDIR, so both sides of the
subtraction are deterministic. Covers the nomatch/no_beep folding, comment
rejection, duplicate listing, all three modes, and -- the trap this function is
most likely to regress into -- that it captures the caller's options BEFORE its
own `emulate`/`setopt extended_glob`, verified by asserting a pristine shell
reports an empty diff rather than gaining `extendedglob`.
Profiling was documented as `zmodload zsh/zprof; source ~/.zshrc; zprof`, which re-sources an already-initialized shell: the compdump, every cached-eval output and the antidote static file are already warm, so the numbers describe a re-source, not a startup. Instead load zsh/zprof at the top of .zshrc when ZPROFRC=1 and dump the report at the very end, and add `alias zprofrc="ZPROFRC=1 zsh"` so a fresh shell is one word away. Verified: `ZPROFRC=1 zsh -i -c exit` prints the report (_mise_hook 15.25ms at the top, which matches what we know), and a normal `zsh -i -c exit` still prints nothing at all. The trailing `true` is future-proofing, NOT a bug fix: .zshrc already ends with `unset _rc`, which returns 0, and `zsh -ic 'print $?'` → 0 both before and after this change. It only insures against a future reordering that leaves a failing test as the last statement, which starship would render as an error status on a shell that started fine. It is placed after the zprof call so it cannot mask the report. AGENTS.md's "Develop / test / profile" recipe updated to match.
A deny-list means every new machine-local dotfile that lands in $ZDOTDIR is
committable until someone remembers to add a rule. Ignore `.*` wholesale and
re-admit only the seven dot-paths that are genuinely part of the config
(.github/, .gitignore, .zprofile, .zshenv, .zshrc, .zstyles, .zunit.yml).
`!.github/` is load-bearing: `.*` matches the directory, and git cannot
re-include a file whose parent directory is excluded, so without that line
the two tracked files under .github/ would have silently fallen out of
tracking. Verified `git ls-files | wc -l` is 49 before and after, and
`git status --porcelain` is clean.
Dropped the stale `.zsh_plugins.zsh` and `.antidote` entries. Kept
.zsh_history ignored — NOT stale: /etc/zshrc:16 sets
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history, i.e. inside this repo, so it is
defensive against 01-hist.zsh failing to load; `.*` now covers it and the
reason is recorded in a comment. Also confirmed via `git check-ignore -v`
that .claude/, .ai/, .revolver/, antidote_plugins.zsh (generated but
load-bearing), tests/_output/ and both .DS_Store files stay ignored, while
completions/ (new, tracked) does not.
rc.d/05-aliases.zsh loads after the antidote plugins, so a plain
`alias diff="diff --color"` would drop any diff alias a plugin had set.
Use the `${aliases[diff]:-diff}` form belak/zsh-utils already uses for `ls`
and `grep`.
Future-proofing, not a live bug fix — verified that belak/zsh-utils
path:utility composes `ls` and `grep` (utility.plugin.zsh:43,46) but never
touches `diff`, so today this clobbered nothing. Live values after the
change are unchanged: diff='diff --color', grep='grep --color=auto'.
Recorded the rule in AGENTS.md: compose when adding flags to the same binary
(diff, grep, future tmux/gpg), clobber when replacing the binary (ls→eza,
cat→bat) — composing there would yield the broken
`ls --color=auto eza --icons`. Also noted the known non-idempotency: a
`source ~/.zshrc` re-run yields `diff --color --color` (grep already did this),
which is harmless and doesn't apply to the recommended `exec zsh` reload.
Three small conveniences, each placed per AGENTS.md's "where to add things"
table:
- `iwd` (rc.d/02_dirs.zsh, with the other navigation shortcuts) — records
$IWD=$PWD at startup and `alias iwd='cd $IWD'` gets you back after
wandering. Single-quoted so $IWD resolves at use time, and deliberately not
exported so a nested shell records its own start dir rather than inheriting
its parent's.
- `touchf` (rc.d/06-commands.zsh) — `mkdir -p` the parents, then touch.
Verified `touchf a/b/c/file.txt deep.txt` creates both, and a no-arg call
prints usage and returns 1.
- a no-op `$` (rc.d/06-commands.zsh) — so a `$ some-command` line pasted from
a README just runs. This is what the junk `$=`/`%=` aliases deleted in the
earlier alias cleanup were groping toward. It has to be spelled
`function $` (`$() { ... }` parses as a command substitution). Verified
`$ echo hi` → `hi`, and that ordinary `$VAR` / `$(cmd)` expansion is
unaffected — parameter expansion happens before command lookup, so only the
command position is involved.
`funcs` picks up `touchf`; it does NOT list `$`, because its name regex
requires `[A-Za-z_]` — left alone rather than loosened for one entry, and
noted in the README table instead.
Every shell start executes code from nine unpinned third-party repos, and `antidote update` rewrites them in place. zdharma-continuum is a community fork of an abandoned org — the clearest account-takeover profile of the nine — so it gets pinned first. Pinned to cf318e06a9b7c9f2219d78f41b46fa6e06011fd9, read from the actual clone's HEAD (~/.cache/repos/zdharma-continuum/fast-syntax-highlighting), so this freezes the code that has actually been running rather than adopting anything new. Note upstream master is already ahead at 3d574ccf as of 2026-07-29; bumping is now a deliberate, reviewable edit instead of a silent side effect of `antidote update`. The SHA is a literal, full 40 characters because it has to be: `antidote bundle` reads the .conf through a plain `<` redirect, so no shell expansion happens and a $VAR is rejected with "pin requires a full 40-character commit SHA". Skipped `using:` — it exists to cherry-pick many plugins from one clone, and there is exactly one OMZ plugin here. Verified: editing the .conf regenerated antidote_plugins.zsh (byte-identical — a pin doesn't change loader output), antidote recorded antidote.pin in the clone and detached HEAD at that SHA, and after draining the zsh-defer queue `fast-theme` and `_zsh_highlight` are both defined, so the deferred load still works. zsh -i -c exit stays silent; zunit 34/34.
Both were sourced eagerly (8.3 KB and 5.2 KB) but neither is needed before the first prompt: YSU only has to exist by the first `preexec`, and `extract` is a one-off interactive command. zsh-defer is already loaded for fast-syntax-highlighting, so this adds no dependency. hyperfine -w 3 -r 20 'zsh -i -c exit': 75.7 ms ± 0.9 → 66.6 ms ± 1.8. Read that honestly — `zsh -i -c exit` never fires precmd, so the deferred sources never run there at all; the real-world effect is that ~9 ms of work moves off the path to the first prompt rather than disappearing. Verified the deferral doesn't break either plugin. antidote emits `fpath+=` eagerly and defers only the `source`, so nothing completion-related moved: the extract dir is still on $fpath and the compdump still maps 'extract' → '_extract'. After draining precmd + the zsh-defer queue by hand (zsh -ic can't fire precmd on its own), `extract` is a defined function and YSU has registered all four hooks — _check_aliases, _check_global_aliases, _check_git_aliases on preexec and _flush_ysu_buffer on precmd. Regenerated antidote_plugins.zsh (both lines now `zsh-defer source`, with zsh-defer's bootstrap hoisted above them). zsh -i -c exit still silent, zunit 34/34, Ctrl-R/Up/Ctrl-T still atuin-search-viins / atuin-up-search-viins / fzf-file-widget.
`zstyle ':antidote:*' zcompile 'yes'` — the pattern covers both `:antidote:bundle:<repo>` (each plugin file gets a sibling .zwc, 30 of them) and `:antidote:static`, which makes antidote emit a self-zrecompiling preamble into antidote_plugins.zsh so the loader itself is sourced from antidote_plugins.zsh.zwc. Re-measured rather than trusting the plan's old "10 ms → 8.5 ms" figure, which it flagged as unreliable. A one-shot before/after is contaminated, because zsh uses a .zwc whenever one sits next to the script — flipping the zstyle alone changes nothing until the .zwc files are deleted. So: delete every .zwc, 40 runs, regenerate, 40 runs, twice (hyperfine -w 5 -r 40 'zsh -i -c exit'): round 1 off 67.7 ms ± 1.5 on 65.4 ms ± 1.8 round 2 off 68.4 ms ± 1.1 on 67.1 ms ± 1.8 ~1.8 ms, consistently signed across both rounds but the same order as σ — so it is a real-looking but marginal win, not something to quote as a headline number. Kept because it is free and is the standard antidote setting. Also had to widen the .gitignore entry to `antidote_plugins.zsh*`: the static .zwc lands in $ZDOTDIR next to the loader, and the old exact-name entry didn't cover it, so it showed up untracked. `git ls-files | wc -l` still 49, `git status --porcelain` clean. ~/.cache/zsh/zcompdump.zwc is unrelated and already handled — ez-compinit compiles the dump itself. Verified: zsh -i -c exit silent, zunit 34/34, and after draining the zsh-defer queue extract/fast-syntax-highlighting/YSU all still load from their .zwc.
…ecture ~22 commits of remediation moved the code out from under both agent-facing guides. Corrected, with what each claim was verified against: - `.zshenv` no longer exports FNOX_AGE_KEY (cd6860d). Both files still listed it as an example env var, which is actively security-relevant guidance to get wrong. Nothing replaced it — fnox's age provider already defaults to $XDG_CONFIG_HOME/fnox/age.txt. - The "always lazy-load tool inits" rule was never the real policy. Documented the actual three-tier one: `cached-eval` for the five cacheable inits (starship, fzf, zoxide, atuin, fnox), a wrapper function when the output isn't cacheable, and `mise activate` as a deliberate eager+uncached exception because its output embeds a $PATH snapshot. - The `main() { ... }; main "$@"` convention was stated as universal; it is 5 of 9 files, and `bench-startup` is also the lone `#!/usr/bin/env zsh`. Counted, and stated as a convention with named exceptions. - The good-example glob was `$ZFUNCDIR/*(.:t)` — missing the `(N)` the code now has. Fixed to `(.N:t)` and said why it is mandatory. - Boot sequence: added ZPROFRC profiling, `completions/` on $fpath, and the `path=($path)` re-dedupe after the plugin loader. - rc.d order: `03-completion.zsh` joins the numeric sequence. - Antidote: added the `pin:` annotation (literal, full 40-char SHA — the `.conf` is read through a plain `<` redirect, so `$VAR` never expands) and `zstyle ':antidote:*' zcompile 'yes'`. - Tests: 34 tests over four suites, not two. Recorded the zunit 0.8.2 gotchas that cost real time this session (`@setup` not `setup()`, non-zero commands failing with an empty message, glob qualifiers needing an unquoted word, no `not_exists` assertion, top-level helpers invisible inside test bodies, and a stray tests/.DS_Store breaking the whole runner). - Added `completions/`, `rc.d/03-completion.zsh`, `optdiff` and `cached-eval` to the where-to-add-things tables and the diagnostics list. Verified after: `zsh -i -c exit` silent, `zunit run` 34/34.
The README described a repo that no longer exists in several places: - It said the fnox age key is "sourced into FNOX_AGE_KEY". That export was removed in cd6860d and nothing replaced it — fnox's age provider already defaults to $XDG_CONFIG_HOME/fnox/age.txt. Leaving the old sentence in place is security-relevant guidance to get wrong, so it's corrected explicitly. - `....` was documented as "up two levels", same as `...`; b35fb39 fixed the alias to `cd ../../..`, so it's three. - The repo-structure tree was missing `completions/`, `rc.d/03-completion.zsh`, `tests/`, `mise.toml` and `.zunit.yml`, and the functions list was missing `cached-eval` and `optdiff`. - Function table: added `cached-eval` and `optdiff`. Noted *why* `$` is documented by hand — `funcs`'s rc.d scanner only matches names starting with `[A-Za-z_]`, so it structurally cannot list it. - Alias tables: added `cz`, `gb9`, `gcom`, `gi`, `redis.s`, `zprofrc` and the composed `diff`. - Tests section: 34 tests over four suites, not two. - Plugins: noted that f-sy-h is pinned, that YSU and ohmyzsh extract are now deferred, and that antidote byte-compiles the plugin files and the loader. - Added a "Startup Performance" section covering cached-eval, the deliberate eager+uncached `mise activate`, and the diagnostics (bench-startup, zsh-bench, zprofrc, optdiff). Rewritten paragraphs are unwrapped to one physical line per the repo's Markdown style; untouched paragraphs are left as they were. Verified after: `zsh -i -c exit` silent, `zunit run` 34/34.
- Version was 2.1.0; `brew list --versions antidote` reports 2.2.1. Noted that `antidote -v` is unreliable here because `antidote` is a shell function from the sourced lib, not a binary. - `pin:`: documented the two constraints found the hard way — the SHA must be a literal (the .conf is read through a plain `<` redirect, so there is no shell expansion and a `$VAR` fails) and exactly 40 characters (a short SHA is rejected). Also recorded why f-sy-h in particular is pinned. - Added the `zcompile` zstyle this config now sets, including why the pattern is `:antidote:*` (it has to cover `:antidote:static` as well as `:antidote:bundle:<repo>`) and how that differs from ez-compinit's zcompdump. - Refreshed the `.zshrc` snippet: it was missing the HOMEBREW_PREFIX fallback, the `$+functions[antidote]` guard, the `-r` readability guards and the `path=($path)` re-dedupe that the real file has now. - Refreshed the plugins-file example: YSU and ohmyzsh extract are `kind:defer` now, and f-sy-h carries its pin. - Noted globally that the plugins file gets NO shell expansion. - `antidote list` example listed atuinsh/atuin, which is purged (atuin is a brew binary). Replaced it, and added a note that `list` shows what is cloned rather than what is loaded — with romkatv/zsh-defer called out as the one clone with no bundle line that must stay. Verified after: `zsh -i -c exit` silent, `zunit run` 34/34.
AGENTS.md names `Improve Performance.instructions.md` as the authority on startup performance, but that file still told you to hand-roll a cache-aware `compinit` — which this config forbids, because ez-compinit owns compinit. Rewrote it around what the code actually does: `cached-eval` first, a lazy wrapper second, eager+uncached only as a documented exception (`mise`), plus the `$+commands` presence check and the mandatory `(N)` glob qualifier. `.zprofile`'s header claimed it was "the entry point ... the first file that is sourced ... loaded only once". All three are false: `.zshenv` is first and is read by every shell including scripts, and a second login shell sources `.zprofile` again. Replaced it with what the file is actually for. Verified after: `zsh -i -c exit` and `zsh -l -c exit` both silent, `zunit run` 34/34.
The `#? Merge with any options inherited from the environment` block appended our own option words to whatever FZF_DEFAULT_OPTS already held. .zshenv runs for every zsh, so a nested shell — or the `exec zsh` reload this config recommends — re-read a value that already contained those words and appended a second copy. Measured: 315 bytes and one more --color= flag per generation, growing without bound (1/2/3/4 --color= flags at generations 1-4). The previous code overwrote the variable before appending, so it was constant regardless of nesting; making the merge real reintroduced the growth. `typeset -aU` keeps the first occurrence of each word, so our own words dedupe away while a genuinely different inherited flag (e.g. an --height= the user exported) still survives ahead of ours.
b55461b made the merge idempotent with `typeset -aU _fzf_all`, but -U uniquifies *every* word in the array, including words that came in from the environment — and repeating a flag is completely normal in fzf. An inherited `--bind a:x --bind b:y` lost its second `--bind`, leaving a value word stranded in the command line, which fzf rejects outright. Reproduction, before this commit: $ FZF_DEFAULT_OPTS='--bind ctrl-a:select-all --bind ctrl-d:deselect-all' \ zsh -c 'source ./.zshenv; print -r -- $FZF_DEFAULT_OPTS; : | fzf --filter=x' --bind ctrl-a:select-all ctrl-d:deselect-all --history=... (second --bind gone) $FZF_DEFAULT_OPTS: unknown option: ctrl-d:deselect-all (fzf exits 2) i.e. every fzf call in that shell — including fzf-tab and the Ctrl-T/Ctrl-R widgets — fails, not just the one flag. Append only the words of *our* block that aren't present yet instead, using an exact-match index lookup. That keeps the property b55461b was after (sourcing .zshenv N times yields the same string) without touching the inherited words at all. Verified: idempotent over three generations both with and without an inherited value, the repeated --bind survives, and fzf accepts the result.
b0d2762 gave both functions a `local v=$(_pg_running_version) || return 1` guard, but that idiom cannot work in zsh: `||` sees the exit status of the `local` builtin, which is 0 no matter what the command substitution did. So with no server running, both functions carried on with an EMPTY version and shelled out to a path with a hole in it. pg_switch went further and ran `mise use -g postgres@<arg>` — which installs the version — off the back of a reading it never got. Reproduction, before this commit (psql stubbed to fail, as when no server is up): --- pg_stop with a failing psql --- psql: could not connect to server pg_stop:5: no such file or directory: \ /Users/andrew.mason/.local/share/mise/installs/postgres//bin/pg_ctl pg_stop returned: 127 <- the `|| return 1` never fired --- pg_switch with a failing psql --- Switching from to 17.2 <- "from" nothing ... two more missing-pg_ctl errors, then `mise use -g postgres@17.2` Split the declaration from the assignment, which is what pg_start (in the same file, same commit) already does correctly. Verified after: pg_stop and pg_switch both return 1 immediately and run nothing else when psql fails, and still get past the guard when psql succeeds.
`a cache older than the tool binary is rebuilt` did `touch -t 202001010000` on the cache and then asserted a rebuild. But a 2020 mtime is also outside the default 7-day TTL, so cached-eval's first invalidation check already fired and the `-nt` comparison it was named after was never evaluated: $ zsh -f -c 'touch -t 202001010000 /tmp/old; a=(/tmp/old(N.ms-604800)); print $#a' 0 # i.e. _ce_fresh is empty, so the && never reaches -nt It was therefore a duplicate of the TTL test above it. Proven by mutation — deleting `&& [[ $_ce_cache -nt $_ce_bin ]]` from functions/cached-eval left the old suite at 17/17 green. Rewrite it to isolate the check the name promises: copy the fixture somewhere this test owns, build the cache, poison the cache *without* aging it (so the TTL check still says fresh), then push the binary's mtime past it. Only -nt can notice, and the marker proves which path ran. Verified: 17/17 with the check present, and 16/17 with `&& [[ ... -nt ... ]]` removed — the test now fails for exactly the reason it exists.
The usage block said `--list` shows what's cached "oldest first", but the glob qualifier is `(N.om)`, and `om` is newest-first (`Om` would be the reverse): $ touch -t 202001010000 $D/aaa-oldest; touch -t 202201010000 $D/bbb-middle $ touch $D/ccc-newest; cached-eval --list ccc-newest bbb-middle aaa-oldest Fix the comment rather than the order: newest-first is a reasonable listing and nothing depends on either, so there's no behaviour worth changing here — only a description that would mislead the next reader.
Running `bench-startup` left three names behind in the interactive shell: `runs`, `measure_basic` and `measure_hyperfine`. Pre-existing and harmless, but it means the benchmark tool mutates the shell it is benchmarking. An autoloaded function's file body IS the function body, so `local runs` scopes it directly. `local` cannot scope a function, though — a nested definition becomes a global function as soon as the outer one runs — so the two helpers are renamed to the private `_bench_startup_*` form (so the cleanup can never clobber a user-defined name) and dropped with `unfunction` once they have run. The repeated /tmp path also moves into a local. `emulate -L zsh` + `setopt pipefail` are unchanged (they fix a real option leak) and so is the leading comment `funcs` uses as its description. Verified: `bench-startup 2` still prints per-run seconds and the min/avg/max summary, `whence -w runs measure_basic measure_hyperfine _bench_startup_basic _bench_startup_hyperfine` reports "none" for all five in the calling shell, `funcs bench` still lists it, `zsh -i -c exit` is 0 bytes, and zunit is 34/34.
The old note pointed the next reader at `command_timeout` and
`core.untrackedCache`. Both were half-right and it omitted the two things that
actually decide the number, so replace it with what was measured.
Measured in ~podia (9.5 GB, 15,895 tracked files), hyperfine, >=12 runs each,
first run discarded:
starship prompt (left) ~90 ms (88.5+-1.7 / 89.3+-1.8 / 93.7+-8.3
across three separate batches)
starship prompt --right 4.7+-0.8 ms
same, [git_status] disabled 22.2+-0.6 ms -> git_status is ~67 ms
same, [git_status] ignore_submodules 91.5+-2.8 ms -> no effect, no submodules
same, nodejs+ruby disabled 89.0+-2.3 ms -> no wall-clock effect
small repo (this one), left 15.2+-0.4 ms
Modules run concurrently, so trimming the version modules buys nothing on the
wall clock (user CPU drops 33 -> 21 ms, latency does not). Only git_status
matters, and it is a subprocess: proven by putting a logging `git` stub on
PATH, which recorded `git config -lz --show-origin --name-only` followed by
`git -C <root> ... -c core.fsmonitor=<repo value> ... status --porcelain=2
--branch`. The same stub in a repo with core.fsmonitor unset recorded no
`status` call at all, i.e. starship falls back to its built-in gitoxide
implementation there. So git-side config does reach starship, and which code
path runs is decided by core.fsmonitor.
`git status --porcelain=2 --branch` in ~podia, toggled per-invocation with -c
so the repo config was never touched:
untrackedCache on, fsmonitor on 74.5+-19.5 ms
untrackedCache off, fsmonitor on 155.2+-13.0 ms
untrackedCache on, fsmonitor off 74.1+-5.2 ms
untrackedCache off, fsmonitor off 156.5+-13.9 ms
untrackedCache is worth ~81 ms; fsmonitor is worth ~0 ms of wall clock to the
git binary (it does cut system CPU 139 -> 41 ms). fsmonitor's real value is
indirect and much larger: with it set to false, starship uses gitoxide and
git_status becomes 165-167 ms (prompt 199.2+-7.9 ms), so it is worth ~110 ms
per prompt.
The residual is not shell config at all. GIT_TRACE2_PERF shows 0 lstats and a
4 ms untracked scan (the cache is working) but 43-63 ms in the index-vs-HEAD
diff, and `git cat-file -e HEAD^{tree}` alone costs 51.4+-0.8 ms there against
4.5+-0.4 ms for `git rev-parse HEAD` (which reads no objects). The repo has
1,995 pack files, 153,471 loose objects and no multi-pack-index; the same
command in a 0-pack repo is 4.8+-0.2 ms, and a synthetic scratch repo scales
4 ms -> 18 ms from 1 to 2,001 packs. Repacking that repo is the biggest
remaining latency win and it lives in the repo, not here.
No change to ~/.config/starship.toml: command_timeout is already 200, and every
module-level option tested was inside the noise.
…REFIX
Bundling straight into antidote_plugins.zsh truncated the live loader before writing, so an interrupted `antidote bundle` left a partial file that was newer than the .conf — the -nt freshness test never fired again and later shells started with no plugins. Bundle to a .tmp and mv on success.
PKG_CONFIG_PATH still interpolated a bare ${HOMEBREW_PREFIX}, which is only exported in .zshenv's darwin* branch.
pg_switch with no argument stopped the running server, then ran a nonexistent .../installs/postgres//bin/pg_ctl start and `mise use -g postgres@`. _pg_running_version also returned 0 when psql exited 0 with no output, so callers' `|| return 1` guards never fired.
…files
.zshrc calls `cached-eval fnox activate zsh` at startup, so the `main() { … }; main "$@"` wrapper left a function named `main` defined in every interactive shell. Renamed to _cached_eval_main, status captured, then unfunction'd.
--clear/--list globbed every regular file in the cache dir: --list reported a crashed shell's <key>.<pid>.tmp as a cache entry and --clear deleted a live shell's in-flight temp out from under its mv.
The fixed /tmp/zsh_startup_basic.txt sat in a world-writable dir (another user can pre-create or symlink it and tee writes through), and two concurrent runs clobbered each other. Removed on exit.
…vention brewUp's row was missing the `brew link` step and brewUpg was undocumented; antidote is 2.3.0, not 2.2.1; the main() note now reads 4 of 9 and records why cached-eval is exempt.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Purpose
The context for this pull request or the Conventional Commits type that best matches the changes.
Changes Summary
Summarize what you've changed and why. Make sure to mention if this is a breaking change.
Screenshots
Add screenshots of the feature in action or a video/GIF walkthrough of the UX. Remove if not applicable (e.g. refactoring).
Areas of interest
Edge cases, workarounds, hacks, security concerns, and other areas of the code you'd like the reviewer to pay extra attention to or give feedback on.