Background completion regeneration (#14000) triggers zcompdump rebuilds when shells start concurrently
Describe the bug
Since #14000 (fix for #13964), plugins that regenerate their completion file in the background write the temporary file into $ZSH_CACHE_DIR/completions/:
() {
local TMPPREFIX="$ZSH_CACHE_DIR/completions/_kubectl"
zf_mv -f -- =( kubectl completion zsh 2> /dev/null ) "$TMPPREFIX"
} &|=( ... ) creates a file named $TMPPREFIX + random suffix (e.g. _kubectlH97hAb), and that directory is in $fpath. compinit counts every _* file in $fpath and compares it against the #files: header of the zcompdump. If a shell starts while another shell's background job has a temp file there, the count is off by one and the whole dump is rebuilt. The next shell then sees the original count again and rebuilds it once more.
Result: new shells are intermittently ~3x slower, depending only on timing. It is easy to hit when opening several tabs/panes in a row, restoring a multiplexer session, or when tools spawn zsh -i -c in the background. Shells that get killed during the rebuild also leave behind partial ~/.zcompdump-*.$HOST.$PID files (from compdump), and I had a stale ~/.zcompdump-*.lock directory left over as well.
This affects all 47 plugins touched by #14000 (docker, kubectl, helm, gh, uv, bun, ...).
Steps to reproduce
Deterministic reproduction (simulates the temp file):
- Enable e.g. the
dockerplugin and start a shell once so the dump exists. - Run:
D=$ZSH_COMPDUMP # e.g. ~/.zcompdump-<host>-5.9 grep -m1 '^#files' $D # #files: 973 touch $ZSH_CACHE_DIR/completions/_dockerTEST time zsh -i -c exit # slow, dump rebuilt grep -m1 '^#files' $D # #files: 974 rm $ZSH_CACHE_DIR/completions/_dockerTEST time zsh -i -c exit # slow again, dump rebuilt back to 973
Real-world reproduction: start one interactive shell, then start a second one ~100-150 ms later, and check whether the zcompdump mtime changed. My measurements (docker, kubectl, helm plugins enabled; 3 runs per delay):
| Second shell started after the first | Startup time | Dump rebuilt |
|---|---|---|
| alone | 0.21-0.23 s | no |
| 0-50 ms | 0.21-0.69 s | occasionally |
| 100-150 ms | 0.57-0.69 s | every time |
| 200-300 ms | 0.22-0.24 s | no |
Expected behavior
Background completion regeneration should not change the set of files compinit sees, so the zcompdump is only rebuilt when completions actually change.
A possible fix that keeps the atomic rename from #14000: put the temp file outside the $fpath directory but on the same filesystem, e.g.
local TMPPREFIX="$ZSH_CACHE_DIR/_kubectl"zf_mv still renames within $ZSH_CACHE_DIR, so the file still appears atomically, and compinit never sees the temporary file. Happy to open a PR with this change across the affected plugins if that direction works for you.
OS / Linux distribution
macOS 26.5 (Darwin 25.5.0, arm64)
Zsh version
5.9
Oh My Zsh version
master (6421f8e); the relevant code is unchanged on current master
Terminal emulator
Ghostty 1.3.1 (with the herdr multiplexer)
Additional context
Plugins: aliases alias-finder ssh-agent fasd git docker-compose kubectl ansible command-not-found fzf gcloud gitfast helm docker pip tmux golang zsh-syntax-highlighting zsh-autosuggestions zsh-completions kube-ps1
Related: #14000, #13964. cc @carlosala @mcornella
Source: ohmyzsh/ohmyzsh