Skip to content

Recipes

Rubin Bhandari edited this page Sep 23, 2026 · 2 revisions

Recipes

Fresh install on a new machine

shelf init --shell zsh --non-interactive
$EDITOR ~/.config/shelf/config.toml
shelf lock

Add to .zshrc:

eval "$(shelf source)"

Open a new terminal, or run eval "$(shelf reload)" in the current one.

Reproducible dotfiles

Commit both files:

cd ~/.config/shelf
git add config.toml plugins.lock

On any machine, git pull && shelf lock installs the exact revisions you pinned. Adding a plugin changes the manifest, so its new pin shows up in your next commit. Changing an existing pin takes shelf lock --update.

Work and personal profiles

[plugins.kubectl-aliases]
github = "ahmetb/kubectl-aliases"
profiles = ["work"]

[plugins.gita]
github = "gita/gita"
profiles = ["work"]

[plugins.zsh-autosuggestions]
github = "zsh-users/zsh-autosuggestions"
shelf --profile work source

Or export SHELF_PROFILE=work in a work-only file so every shell in that environment picks it up.

Clone a private repository over ssh

[plugins.internal-tools]
github = "myorg/internal-tools"
proto = "ssh"

Your ssh key does the auth, no tokens in the config. Forge keys only, git URLs already carry their own transport.

Pin a plugin to a release

[plugins.tokyo-night]
github = "folke/tokyo-night.nvim"
tag = "v1.0.0"

Or pin an exact commit with rev = "0123456789abcdef...". shelf lock respects the pin, shelf lock --update moves it forward when you bump the tag, and the manifest records the resolved commit either way.

Freeze a plugin so updates leave it alone

[plugins.broken-upstream]
github = "someone/broken"
frozen = true

shelf update reports it as Frozen and skips the fetch. Nothing freezes it permanently, shelf lock --reinstall or --force with an update still refreshes it.

Make startup fast

[plugins.powerlevel10k]
github = "romkatv/powerlevel10k"
apply = ["defer"]

[plugins.zsh-autosuggestions]
github = "zsh-users/zsh-autosuggestions"
apply = ["defer"]

[plugins.fast-syntax-highlighting]
github = "zdharma-continuum/fast-syntax-highlighting"
apply = ["defer"]

Then run shelf lock so the new apply lands in the lock. Prompts and suggestions load after the first prompt draws.

For files that stay loaded, add bytecode compilation:

[plugins.my-heavy-plugin]
github = "user/heavy"
apply = ["zcompile"]

Defer the slow ones and zcompile the big ones. They compose, list both in apply if a plugin wants both.

Defer with the zsh-defer plugin

Shelf's built-in apply = ["defer"] needs no plugin. Reach for zsh-defer when you also want to defer your own commands, or when you already keep it around. It provides a zsh-defer command that runs any command after the first prompt.

[templates]
zsh-defer = "{{ hooks?.pre | nl }}{% for file in files %}zsh-defer source \"{{ file }}\"\n{% endfor %}{{ hooks?.post | nl }}"

[plugins.zsh-defer]
github = "romkatv/zsh-defer"

[plugins.zsh-autosuggestions]
github = "zsh-users/zsh-autosuggestions"
apply = ["zsh-defer"]

[plugins.fast-syntax-highlighting]
github = "zdharma-continuum/fast-syntax-highlighting"
apply = ["zsh-defer"]

Order matters. zsh-defer sits above its consumers in the config, plugins load in declaration order, so the function exists before the first zsh-defer source line runs. Hooks still execute immediately, same as the built-in defer.

This template is zsh only, bash would run zsh-defer as a command and fail. In a bash config, or one shared between shells through SHELF_SHELL, keep the built-in apply = ["defer"] instead, it degrades to plain source under bash on its own. Run shelf lock after editing, templates resolve at lock time.

Load one plugin from a large repository

oh-my-zsh ships hundreds of plugins, you want one:

[plugins.sudo]
github = "ohmyzsh/ohmyzsh"
dir = "plugins/sudo"
use = ["*.plugin.zsh"]

dir narrows the plugin root, use picks the entry file. Without use, the defaults probably match anyway.

Keep test files out of the shell

[plugins.myplugin]
github = "owner/myplugin"
use = ["**/*.zsh"]
ignore = ["**/test*", "**/tests/*", "**/bench/*"]

ignore subtracts from whatever selected the files, so helpers named test-helpers.zsh never get sourced.

Build a plugin before loading it

[plugins.fzf]
github = "junegunn/fzf"
build = ["make install"]
dir = "shell"
use = ["*.bash", "*.zsh"]

Each build entry runs through sh -c in the plugin directory when the lock is built, before file selection, so generated files are usable in the same pass. A non-zero exit fails the lock with the command's output on stderr.

Load a single file from a URL

[plugins.z]
remote = "https://raw.githubusercontent.com/rupa/z/master/z.sh"

The first shelf lock downloads it. Later runs send If-None-Match with the ETag recorded in the lock, and a 304 Not Modified response skips the download entirely.

Custom loading behavior

[templates]
path-only = "export PATH=\"{{ dir }}:$PATH\""
announce = "echo \"loading {{ name }}\"\n{% for file in files %}source \"{{ file }}\"\n{% endfor %}"

[plugins.cli-tools]
github = "owner/tools"
apply = ["path-only", "announce"]

apply runs templates in order, so a PATH-only template plus a source template in one plugin works.

Environment variables before plugins

[env]
EDITOR = "nvim"
plugins = "(git npm macos)"
FZF_DEFAULT_OPTS = "--height 40%"

[env] renders first, sorted by name, so every plugin sees the variables.

Optional local plugins across machines

[plugins.work-only]
local = "/opt/work/dotfiles/plugins"
optional = true

optional requires a local source. On a machine without that path the plugin is skipped instead of failing the lock.

CI and scripted use

#!/bin/sh
set -eu

shelf --non-interactive --quiet --config-file "$PWD/ci/config.toml" lock
script=$(shelf --non-interactive --config-file "$PWD/ci/config.toml" source)
printf '%s\n' "$script" > /tmp/generated.sh

--non-interactive turns prompts into failures, --quiet drops diagnostics, and the exit code tells you if anything went wrong. Shell code stays on stdout, everything else on stderr.

Serial installs against a flaky host

shelf lock --concurrency 1 --reinstall

Eight parallel clones is the default because it is usually right. When a rate-limited or unreliable host fails intermittently, drop to 1 or 2.

Uninstall shelf

Remove the eval line from .zshrc or .bashrc first, then delete what shelf owns:

rm -rf ~/.local/share/shelf     # clones, downloads, runtime locks
rm -rf ~/.config/shelf          # config and revision manifests
rm -f "$(command -v shelf)"     # the binary, wherever it sits
rm -f ~/.zfunc/_shelf           # completions, if you installed them there

The config and manifest live in the config directory, copy them somewhere first if you might come back. Package installs go through the package manager instead, pacman -R shelf-sh-bin or your distro's equivalent, which also removes its completion files.