Repository navigation
Recipes
shelf init --shell zsh --non-interactive
$EDITOR ~/.config/shelf/config.toml
shelf lockAdd to .zshrc:
eval "$(shelf source)"Open a new terminal, or run eval "$(shelf reload)" in the current one.
Commit both files:
cd ~/.config/shelf
git add config.toml plugins.lockOn 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.
[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 sourceOr export SHELF_PROFILE=work in a work-only file so every shell in that environment picks it up.
[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.
[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.
[plugins.broken-upstream]
github = "someone/broken"
frozen = trueshelf update reports it as Frozen and skips the fetch. Nothing freezes it permanently, shelf lock --reinstall or --force with an update still refreshes it.
[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.
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.
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.
[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.
[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.
[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.
[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.
[env]
EDITOR = "nvim"
plugins = "(git npm macos)"
FZF_DEFAULT_OPTS = "--height 40%"[env] renders first, sorted by name, so every plugin sees the variables.
[plugins.work-only]
local = "/opt/work/dotfiles/plugins"
optional = trueoptional requires a local source. On a machine without that path the plugin is skipped instead of failing the lock.
#!/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.
shelf lock --concurrency 1 --reinstallEight parallel clones is the default because it is usually right. When a rate-limited or unreliable host fails intermittently, drop to 1 or 2.
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 thereThe 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.