Skip to content

Command reference

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

Command reference

Choosing an update mode

Command Installs missing Fetches sources Re-clones Writes runtime lock Writes manifest Prints script
lock yes no no yes yes, pins honored no
lock --update yes yes no yes yes, pins refreshed no
lock --reinstall yes no yes, pins honored yes yes no
source yes, when stale no no only when relocking only when relocking yes
source --relock yes no no yes yes yes
source --update yes yes no yes yes yes
source --reinstall yes no yes yes yes yes
update yes yes no no no yes
update --lock yes yes no yes yes no

--force joins --update or --reinstall and pulls frozen plugins in.

Every mode except the update ones reads pinned revisions from the manifest and checks them out. That is what makes git pull && shelf lock reproduce a setup. Update modes resolve refs from the config instead, then rewrite the manifest.

Update behavior today

--update really does fetch, the objects and origin/* refs land in the clone, but an already-installed plugin stays on its current commit. Fresh installs get the current tip. This is a known bug in the update path, the fix is to check out the fetched ref once the fetch returns.

Until it lands:

  • New upstream commits reach an existing plugin only through a fresh clone. Delete the manifest and re-clone everything:

    rm ~/.config/shelf/plugins.lock        # or plugins.<profile>.lock
    shelf lock --reinstall

    Plugins with a tag or rev pin in the config go back to that pin, which is what pins are for.

  • Changing tag, rev, or branch in the config takes effect with shelf lock --update or shelf update --lock. Plain shelf lock re-applies the manifest and the manifest wins over the config, so it restores the old revision.

  • A tag that moved upstream needs a reinstall. Fetch never overwrites an existing local tag.

shelf init

shelf init [--shell bash|zsh]

Creates a new config.toml. Interactive mode asks which shell to use and confirms the target path. --shell skips the shell question, --non-interactive skips both, and both together write the file with no prompts at all.

If the config already exists, init prints a warning to stderr and does nothing. It never overwrites.

shelf init --shell bash --non-interactive

shelf lock

shelf lock [--update | --reinstall] [--concurrency N] [--force]

Installs every configured plugin, then writes two files, the runtime lock in the data directory and the revision manifest next to your config. This is the command to run after any config edit.

Modes:

  • plain shelf lock installs what is missing and checks out the pinned revisions from the manifest. Existing installs are left alone.
  • --update fetches sources, resolves refs from the config instead of the manifest, and rewrites the manifest with the result. Frozen plugins are skipped.
  • --reinstall deletes and re-clones every source, then applies the manifest pins again. This is how you apply a new depth or cloneopts, clone options only affect fresh clones.
  • --force with --update or --reinstall also refreshes frozen plugins.

Installs run concurrently, 8 at a time by default. --concurrency 1 serializes them, which is occasionally what a flaky server needs.

shelf source

shelf source [--relock | --update | --reinstall] [--concurrency N] [--force]

Prints the shell script for your current lock to stdout. This is the command that belongs in .bashrc or .zshrc:

eval "$(shelf source)"

With no flags it reads the existing lock, verifies it against the config fingerprint and the installed files, and renders. If the config, profile, shell, or installed files changed, it relocks on the spot. Flags force the issue:

  • --relock regenerates the lock even when it looks valid.
  • --update updates sources, rewrites the lock, and renders.
  • --reinstall re-clones and rewrites the lock, then renders.

On the fast path --verbose prints Unlocked <path> so you can see it skipped the relock.

shelf reload

shelf reload

Prints exec <shell> to stdout. Evaluate it in the current shell after a config change:

eval "$(shelf reload)"

The exec replaces the shell and reruns your startup files, so the eval "$(shelf source)" line picks up new plugins without opening a new terminal.

shelf update

shelf update [--lock] [--concurrency N] [--force]

Fetches every non-frozen plugin, then prints the refreshed script to stdout. --lock skips the script and writes both the runtime lock and the revision manifest instead, which is what cron jobs and dotfiles bootstraps want.

The fetch does not move an installed clone to the new commits yet. See update behavior today above for what actually changes and how to pick up new commits in the meantime.

shelf path

Prints the resolved paths as key=value lines:

config_dir=...
data_dir=...
config_file=...
lock_file=...

lock_file reflects the active profile, so shelf --profile work path shows plugins.work.lock.

shelf status

Verifies the lock, then checks every git-backed plugin's checked-out commit against the locked revision in parallel. Output is one line per plugin:

zsh-autosuggestions: ok
powerlevel10k: revision abc1234, want def5678
some-plugin: unable to read revision

Any non-ok line makes the command exit 2 with error: installed plugins differ from the lockfile. A stale lock fails earlier with error: lockfile is stale or selected plugin files are missing.

shelf doctor

Checks the whole installation and prints a report:

version:  shelf 1.2.3
shell:    /usr/bin/zsh
          zsh 5.9
config:   ok ~/.config/shelf/config.toml
git:      ok /usr/bin/git
lock:     ok ~/.local/share/shelf/plugins.lock

No problems found

The git: line only appears when the config has git-based plugins, and doctor then fails if git is missing from PATH. It also fails when the shell in the config is not installed. Run this first when something feels wrong.

shelf clean

Removes installed sources that the config no longer references. Prints removed: <path> per entry, or nothing to clean. lock, update, and a relocking source prune the same way automatically, so you rarely need clean by hand. It is the right command after a big config edit or a profile rename.

shelf list

Prints every plugin name from the config, one per line. Reads the config, not the lock, so it works before the first shelf lock.

shelf info NAME

Reads the lock and reports one plugin:

- source: "https://github.com/zsh-users/zsh-autosuggestions"
- rev: "a411ef3e09924c1d7d4f0a3e9d0b8b6f3d3e2b5a"
- files: "/home/you/.local/share/shelf/repos/github.com/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh"
- size: "184K"

rev only appears for git sources, size only for plugins with an installed directory. Inline plugins have neither. Fails with error: plugin "x" is not in the lock file when the name is unknown or the lock is stale.

shelf add NAME

Adds a plugin to config.toml while preserving comments and formatting around it.

Source flags, pick exactly one:

--github owner/repo     --gist user/id        --gitlab owner/repo
--bitbucket team/repo   --codeberg owner/repo --git URL
--remote URL            --local PATH          --inline "shell code"

Selection and behavior flags:

--dir SUBDIR            plugin subdirectory inside the source
--file FILE             one exact file to load instead of globbing
--use GLOB              load glob (repeatable)
--ignore GLOB           exclude glob (repeatable)
--apply NAME            template name (repeatable)
--build CMD             install-time command (repeatable)
--hooks key=value       hook (repeatable)
--profiles NAME         profile (repeatable)
--branch NAME | --tag NAME | --rev SHA
--proto https|git|ssh   forge protocol
--depth N               clone depth, 0 means full history
--cloneopts ARG         extra `git clone` argument (repeatable)
--optional              skip when the local path is missing
--frozen                never fetch updates for this plugin

Examples:

shelf add autosuggest --github zsh-users/zsh-autosuggestions --apply defer
shelf add private --github myorg/secret --proto ssh
shelf add theme --local ~/dotfiles/themes/mytheme

shelf edit

Opens config.toml with $SHELF_EDITOR, then $VISUAL, then $EDITOR. The value is split with shell-word rules, so SHELF_EDITOR="nvim --wait" works and quoted paths survive. No editor configured is an error, not a silent no-op.

shelf remove

shelf remove NAME
shelf remove --interactive   # -i

Deletes one plugin table from the config, comments and unrelated TOML intact. --interactive opens a multi-select picker instead and cannot be combined with NAME or with --non-interactive. After removing, run shelf lock to drop the plugin from the lock and prune its install.

shelf completion SHELL

Generates a completion script. SHELL is one of bash, zsh, fish, powershell.

shelf completion zsh > "${fpath[1]}/_shelf"

shelf self-update

shelf self-update [--version TAG] [--yes] [--force]

Downloads the latest release archive, verifies it against the published checksums, and replaces the running binary atomically. Development builds refuse to update unless you pass --force.

--version TAG installs an exact release, which unlike an unpinned update may move backwards on purpose. --yes answers the confirmation prompt; a run with no terminal, or with --non-interactive, cannot prompt and needs it.

Install flow

Self-update suits a standalone install. The official installer places the binary at ~/.local/bin/shelf and can itself update:

curl -fsSL https://github.com/rubiin/shelf/releases/latest/download/install.sh | sh

The script downloads the matching release archive, verifies it against the release's published sha256, and installs the binary. SHELF_INSTALL_PATH chooses another location and SHELF_VERSION pins a release.

Packaged marker

A package manager that owns the install disables self-update rather than letting it replace the packager's binary. The AUR, deb, rpm, and apk packages install /usr/lib/shelf/.disable-self-update, so shelf self-update warns and refuses instead:

self-update is disabled for this install.
Update shelf the same way you installed it, or reinstall it with the standalone installer:
  curl -fsSL https://github.com/rubiin/shelf/releases/latest/download/install.sh | sh

A packager can also ship update instructions at lib/shelf-self-update-instructions.toml, lib/shelf/shelf-self-update-instructions.toml, or lib64/shelf/shelf-self-update-instructions.toml; shelf prints the file's message (or first named command) instead. Marker and instructions paths are resolved under the install prefix, the directory two levels above the binary, so /usr/bin/shelf looks under /usr. SHELF_SELF_UPDATE_AVAILABLE=false disables self-update and =true re-enables it, and --force bypasses the refusal.

shelf --version

Prints shelf version <version>.

Clone this wiki locally