Skip to content
Rubin Bhandari edited this page Sep 30, 2026 · 3 revisions

FAQ

Why is my plugin not loading?

Four suspects, in order of how often they are the cause.

  1. No files matched. Shelf looks for <name>.plugin.zsh, *.zsh, *.sh, and friends in zsh, <name>.plugin.bash, *.bash, *.sh in bash. A repo whose only script is init.zsh under src/ matches nothing. Set use = ["src/*.zsh"].
  2. Wrong subdirectory. Add dir = "...".
  3. Shell mismatch. A *.bash plugin in a zsh config matches nothing useful. Check shell in the config.
  4. Stale lock. Run shelf lock and open a new shell.

shelf info NAME shows exactly which files the lock selected.

What does "lockfile is stale or selected plugin files are missing" mean?

The lock no longer matches the config fingerprint, the profile, the shell, or the installed files. You edited the config, switched profile, or a plugin file disappeared. Running shelf lock fixes it, and plain shelf source fixes it too because it relocks automatically when verification fails.

Why does --force fail with "--force requires --update or --reinstall"?

--force only widens an existing operation, it updates frozen plugins during an update or reinstall. Alone it has nothing to widen, so shelf refuses instead of guessing. Write shelf lock --update --force.

Why do I see "Blocking waiting for file lock on ..."?

Another shelf process holds the config directory lock, usually a concurrent shelf source from a shell that just started. It is not an error. The message comes from shelf's own file lock, and the process proceeds as soon as the lock frees. If it never clears, a crashed process left the lock behind, and the file lock releases when the holder exits.

Why does shelf warn that my profile matches no plugins?

--profile wrk with every plugin either unprofiled or profiled work means no plugin listed wrk. Shelf warns on stderr and loads the unprofiled plugins anyway. Compare against grep profiles config.toml.

Does defer work in scripts or CI?

No. The zsh scheduler drains when zle first goes idle, which requires a prompt. Non-interactive zsh never draws one, so deferred plugins stay unloaded. This is a limit of prompt-time loading in general, not a shelf bug. Use plain source for anything that must run non-interactively.

What happens to defer and zcompile in bash?

Both render as plain source lines. Bash has neither a prompt-time hook shelf can rely on nor a zcompile builtin. The point is that one config stays valid in both shells, nothing fails, nothing is compiled, nothing is deferred.

Should I commit plugins.lock?

Commit the one in the config directory, it holds only plugin names and revisions and makes your setup reproducible. Do not commit the one in the data directory, it holds absolute paths and machine-specific state. shelf path prints the runtime lock's location for the active profile.

I set depth = 0 and nothing changed

Clone options apply to fresh clones only. Re-clone with shelf lock --reinstall (or shelf source --reinstall). The new depth is recorded in the lock, so later reinstalls keep it.

Where are plugins installed and how big are they?

shelf path                 # data_dir
shelf info zsh-autosuggestions

Git sources land in repos/<host>/<owner>/<repo>, downloads in downloads/<host>/<path>. shelf info sums the installed directory for you. shelf clean removes anything the config no longer owns.

Why does eval "$(shelf source)" work if shelf prints to stderr too?

Everything except the script goes to stderr: Loaded, Locked, warnings, errors. $(...) captures stdout only. Pipe it the other way by accident, shelf source 2>&1, and the diagnostics corrupt the script, which is the classic way to break this.

What does exit code 2 mean?

Any failure. Shelf prints error: <message> on stderr and exits 2, for every command. Exit 0 is success. There is no third code to special-case.

What is the difference between frozen and not updating?

frozen = true is a per-plugin promise recorded in the lock. shelf update, shelf lock --update, and shelf source --update skip it and report Frozen. Skipping by hand, by just not running update, means the lock goes stale against upstream. Frozen keeps the lock honest while pinning the version.

Does the config shell or SHELF_SHELL win?

The config's shell key wins. SHELF_SHELL is only consulted when the config omits it, which is how one config can serve both shells:

# no shell key
SHELF_SHELL=bash shelf source
SHELF_SHELL=zsh  shelf source

An unsupported SHELF_SHELL value is an error rather than a silent fallback to zsh.

Should I self-update or use my package manager?

Use shelf self-update for a standalone install, such as the one install.sh places at ~/.local/bin/shelf. If a package manager installed shelf, the AUR, deb, rpm, or apk version, update through that instead: those packages ship a .disable-self-update marker, so self-update warns and refuses rather than replacing the packager's binary. shelf self-update downloads the release archive, verifies its sha256, and swaps the binary atomically. Development builds refuse without --force.

Does shelf re-download remote plugins every time?

No. The first lock records the response's ETag, and later lock, source, and update runs send If-None-Match. A 304 Not Modified answer skips the body entirely.

How do I remove a plugin completely?

shelf remove zsh-autosuggestions
shelf lock

lock prunes the install directory because the config no longer owns it. shelf clean does the same sweep by hand. Comments and unrelated TOML around the removed table survive.

Does build run for plugins my profile excludes?

No. build runs only for plugins active in the selected profile, so a work-profiled plugin's build commands do not execute under the default profile.

Can I set defaults for all plugins?

Yes. Top-level match sets the file patterns for plugins without use, top-level apply sets the templates for plugins without apply. Both fall back to the shell's own defaults. A plugin's own use or apply replaces the default, the two never merge.

Why do two plugins share one clone?

Clones key off the repository URL, not the plugin name. Two plugins with the same source and different dir values read from one clone under repos/. That saves space, and installs into that directory run one at a time.

Can local paths use ~ or environment variables?

Yes. local = "~/dotfiles/plugins" and local = "$HOME/dotfiles/plugins" both expand before shelf touches the path.

What fields can an inline plugin not set?

proto, rev, branch, tag, dir, file, use, apply, cloneopts, depth, and build. Inline plugins have no install step and no files to select. hooks, profiles, and frozen are fine.

In what order do plugins load?

[env] first with names sorted, then plugins in config declaration order, shelf add appends. Inside a plugin the apply templates run in list order.

Did shelf just revert my update?

Plain shelf lock re-applies pinned revisions from the manifest, so it restores recorded pins after a manual checkout change or a pin you expected to move. Use shelf lock --update when you mean to refresh pins and plain shelf lock when you mean to restore them. The command reference covers what update does today.

Why does update say Checked but nothing changed?

The fetch ran and moved origin/*, but the working tree stayed on its current commit. This is the known bug described under update behavior today. Until it is fixed, delete the manifest and shelf lock --reinstall to pick up new commits.

Clone this wiki locally