Zsh compinit Security: Auditing Completion Paths Before Startup
Why compinit checks ownership and permissions in fpath, how compaudit exposes unsafe completion trees, and how to repair them without bypass flags.
Zsh’s function-based completion system loads executable shell functions from directories in fpath. A writable completion directory is therefore a startup-code path: another account could replace a function and wait for an interactive shell to initialize or invoke it.
compinit checks that path before enabling completion. The familiar “insecure directories” warning is not cosmetic startup noise. It reports that ownership or write permissions make at least part of the function tree unsafe under the shell’s documented model.
Initialize completion in the supported order
A minimal setup is:
autoload -Uz compinit
compinit
autoload makes the initialization function available from fpath; -U prevents alias expansion while it is loaded, and -z selects normal zsh function behavior. compinit then scans completion functions, recognizes declarations such as #compdef, builds mappings, performs its security audit, and writes a dump file to speed later startup.
Add plugin-manager or package directories to fpath before this call. Adding them afterward and running a second compinit creates duplicated work and can leave the security decision disconnected from the final path.
Ask compaudit for the exact paths
Run the same audit directly:
autoload -Uz compaudit
compaudit
With no arguments, compaudit examines the completion directories derived from fpath and the completion installation. The check looks for files or directories that are group- or world-writable, or not owned by root or the current user. It prints paths that violate the policy.
Inspect every component, including parents:
for path in ${(f)"$(compaudit)"}; do
ls -ld "$path"
done
Do not pipe the output into a blind recursive chmod or chown. A package-manager tree, network home, shared workstation path, and personal plugin checkout have different legitimate owners. First determine who installed the path and who is supposed to update it.
Repair ownership and write access at the source
For a personal completion directory, the normal result is ownership by the current user with no group or other write bit:
chown -R "$USER" "$HOME/.zsh/completions"
chmod -R go-w "$HOME/.zsh/completions"
Run privileged commands only for paths you have verified. Do not chown -R a system package directory to your user; that would let an unprivileged account replace code loaded by shells. Root-owned package paths should normally remain root-owned and be updated through their package manager.
If a framework cloned files as root into the home directory, reinstall it as the user or correct only that framework tree. If a shared group genuinely needs write access, it is not a safe completion-code source for accounts outside an equivalent trust boundary. Copy reviewed functions into a protected per-user or root-owned directory instead.
After repair, run compaudit again and require no output.
Understand the bypass flags before rejecting them
compinit -u tells zsh to use insecure files and directories without asking. compinit -i silently ignores insecure entries. compinit -C skips the security check entirely when an existing dump file is available.
These flags solve different problems, and none repairs permissions:
-uaccepts the risk and loads the code;-imay make completions disappear because unsafe functions are excluded;-Ctrusts that the existing dump and function path do not need a fresh audit.
Copying compinit -u from a startup-speed guide converts a visible control into silent code execution. Use a bypass only when the trust boundary has been reviewed and documented, and prefer fixing the directory.
Rebuild the dump after changing paths
By default, compinit maintains a dump such as .zcompdump in the zsh startup directory. The dump caches completion definitions, not the entire security state forever. After removing a plugin, changing zsh versions, or repairing fpath, delete only the user’s relevant dump and initialize again:
rm -f -- "$ZDOTDIR/.zcompdump"*(N)
autoload -Uz compinit
compinit
If ZDOTDIR is unset, the normal location is the home directory; adjust the command instead of assuming an empty expansion names the right file. The (N) glob qualifier prevents an unmatched pattern from becoming a literal argument.
Do not delete completion source directories as a cache-cleaning step. A dump rebuild can fix stale mappings, while missing source files require reinstalling the owning package or plugin.
Keep startup fast without removing the audit
Security checks are usually not the main cause of a multi-second shell startup. Measure with zprof, count fpath entries, remove duplicate plugin initialization, and keep generated completion trees small. Package updates should install atomically with safe modes so every new shell does not rediscover the same warning.
For managed systems, place approved completions in a root-owned directory and let users add personal functions only from protected home paths. Run compaudit in configuration tests after package or plugin changes.
Audit the complete path after every updater
A completion file can be mode-safe while one parent directory is writable by another account. Capture the final fpath after all framework, plugin-manager, and package initialization, resolve duplicate entries, and inspect the ownership chain from each completion directory to a trusted root. The updater for that tree should be the same identity that the ownership model expects.
Package managers create several common splits: a root-owned system prefix, a user-owned package prefix, and a plugin checkout in the home directory. All can be safe, but mixing a root-owned file inside a user-writable parent or running a user plugin installer with sudo makes later audits confusing. Repair the installation method, then modes, instead of adding exceptions to .zshrc.
Automate a noninteractive check after updates:
autoload -Uz compaudit
insecure=(${(f)"$(compaudit)"})
(( ${#insecure} == 0 )) || {
print -ru2 -- 'Unsafe completion paths:'
print -rlu2 -- $insecure
return 1
}
Run it under the same account and startup environment as the interactive shell. A CI check executed as root can approve paths that are unsafe for a user, while a minimal shell may omit the plugin directory that causes the real warning. After a successful audit, initialize once and confirm that the generated dump itself is owned and writable only by the intended account.
This turns the warning into a maintainable invariant: every updater may add completion code, but none may silently add a new writer to the shell’s startup trust boundary.
The warning is valuable because zsh recognizes that completion functions are code. A clean fix leaves compaudit silent, preserves the intended updater, rebuilds the dump, and keeps compinit’s default check enabled.
Related:
- Bash mapfile and readarray: Loading Lines Without a Fragile Read Loop
- How Tab Completion Actually Works in Bash and Zsh
Sources: