Skip to content

Configuration

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

Configuration reference

The config file is config.toml in the config directory. Top-level keys:

shell = "zsh"          # "zsh" (default) or "bash"
match = ["*.zsh"]      # file patterns for plugins without `use`
apply = ["source"]     # templates for plugins without `apply`

[env]                  # rendered before every plugin
ZSH_THEME = "robbyrussell"
plugins = "(git npm)"  # values are assignment right-hand sides

[templates]            # custom apply templates, see Templates
announce = "..."

[plugins.NAME]         # one table per plugin

[env] keys must be valid shell variable names. Values are pasted after =, so arrays are written as strings that already look like shell syntax.

Top-level match and apply set defaults for every plugin. A plugin's own use or apply replaces the default, the two never merge. Patterns in match follow the built-in rule, the first pattern that selects any file wins. Patterns in a plugin's use all contribute.

Sources

Each plugin sets exactly one of these. Two sources, or none, is an error that names the plugin.

Key Accepts Installs to
github owner/repository .../shelf/repos/github.com/owner/repository
gist user/id .../shelf/repos/gist.github.com/...
gitlab owner/repository .../shelf/repos/gitlab.com/...
bitbucket team/repository .../shelf/repos/bitbucket.org/...
codeberg owner/repository .../shelf/repos/codeberg.org/...
git Git URL or local repo path .../shelf/repos/<host>/<owner>/<repo>
remote Absolute URL of one file .../shelf/downloads/<host>/<path>
local File or directory on disk nothing, used in place
inline Shell source in the TOML string nothing, stored in the lock

The forge keys clone over https by default. proto = "ssh" or "git" switches the transport, and shelf add --proto ssh writes the same field.

local paths expand ~, ~/..., and environment variables before shelf touches them, so local = "~/dotfiles/plugins" and local = "$HOME/dotfiles/plugins" both work.

Plugin fields

Field Type Effect
use list of globs Files to load, matched recursively against the plugin directory. Without it, the shell defaults below apply.
ignore list of globs Excluded from whatever use or the defaults selected.
file path Load this one file and nothing else. Fails the lock if the file is missing.
dir path Treat this subdirectory as the plugin root.
apply list of template names Defaults to ["source"].
branch, tag, rev string Git ref to check out. Precedence is rev, then branch, then tag. The ref is checked out detached.
proto https, git, ssh Forge transport only. On a plain git or remote source it is rejected.
depth int Clone depth. Default 1. 0 clones full history.
cloneopts list of strings Extra arguments passed to git clone.
build list of strings Each entry runs through sh -c in the plugin directory when the lock is built, before file selection, so pipes and redirection work. Needs a directory source, so inline and remote reject it.
hooks table pre and post shell code wrapped around the plugin's files.
profiles list of strings Load only under those profiles. No profiles key means always.
optional bool Only valid with local. Missing path is skipped instead of failing.
frozen bool Never fetched by update, lock --update, or source --update.

Default file globs when use is absent:

zsh:   <name>.plugin.zsh  <name>.zsh  <name>.sh  <name>.zsh-theme
       *.plugin.zsh  *.zsh  *.sh  *.zsh-theme

bash:  <name>.plugin.bash  <name>.plugin.sh  <name>.bash  <name>.sh
       *.plugin.bash  *.plugin.sh  *.bash  *.sh

use and ignore take doublestar globs relative to the plugin directory. ** crosses directories. A typo in a pattern fails the lock even when no file would have matched, so a broken glob does not silently load nothing.

depth and cloneopts only affect fresh clones. Apply them to an installed plugin with shelf lock --reinstall. Both are recorded in the lock, so later reinstalls reproduce the same clone.

build runs on fresh install, update, reinstall, and a stale relock. Its output goes to stderr and --quiet hides it. A non-zero exit fails the lock. Because it runs before file selection, use can pick up files that build just generated.

How shelf picks the files to load

Selection follows the first rule that applies.

  1. file is set. Exactly that file loads, and the lock fails when it is missing.
  2. The local source points at a single file. That file loads and its parent directory becomes the plugin directory.
  3. use is set. Every pattern contributes, matches from all patterns merge.
  4. Top-level match is set. The first pattern that selects any file wins, the rest are skipped.
  5. None of the above. The shell defaults listed above apply with the same first-match rule.

Patterns are doublestar globs matched against paths relative to the plugin directory, and {{ name }} in a pattern expands to the plugin name. Shelf validates every pattern before matching, so a typo fails the lock even when the tree holds no candidates. The .git directory is never walked. ignore subtracts from whatever selected the files, results sort by base name, and duplicates from overlapping patterns collapse to one path.

Profiles

[plugins.kubeconfig]
github = "example/kube-plugin"
profiles = ["work"]
shelf --profile work source      # or SHELF_PROFILE=work

Plugins without profiles load under every profile. A profile that matches nothing produces a warning on stderr and still loads the unprofiled plugins, which turns most typos into something you notice immediately.

Each profile gets its own lock file, plugins.work.lock, so switching profiles relocks once and then hits the fast path.

A config with every field

Every field shows up at least once below. A real config uses a handful of these.

# ~/.config/shelf/config.toml
shell = "zsh"

# Defaults for plugins that set neither field. First matching pattern wins.
match = ["{{ name }}.plugin.zsh", "*.plugin.zsh", "*.zsh-theme", "*.zsh"]
apply = ["source"]

[env]
EDITOR = "nvim"
plugins = "(git npm)"          # looks like TOML, is shell syntax after the quotes

[templates]                    # custom templates, covered under Templates
announce = "echo \"loading {{ name }}\"\n{% for file in files %}source \"{{ file }}\"\n{% endfor %}"

[plugins.powerlevel10k]
github = "romkatv/powerlevel10k"   # forge shorthand, owner/repo
frozen = true                       # never fetched by update

[plugins.tokyo-night]
github = "folke/tokyo-night.nvim"
tag = "v1.0.0"                      # ref pin, see precedence above
apply = ["defer"]                   # per-plugin templates
profiles = ["dotfiles"]             # loads only with --profile dotfiles

[plugins.private-tools]
github = "myorg/tools"
proto = "ssh"                       # forge transport, https by default
depth = 0                           # full history instead of the shallow default
cloneopts = ["--single-branch"]     # extra arguments for git clone
rev = "0123456789abcdef0123456789abcdef01234567"  # exact commit, wins over branch and tag

[plugins.oh-my-zsh-sudo]
github = "ohmyzsh/ohmyzsh"
dir = "plugins/sudo"                # subdirectory is the plugin root
use = ["*.plugin.zsh"]              # explicit file selection
ignore = ["**/test*"]               # excluded from whatever matched
hooks = { pre = "echo before", post = "echo after" }  # around the files when loading

[plugins.fzf]
github = "junegunn/fzf"
build = ["make install"]            # sh -c in the source root, before file selection
file = "shell/key-bindings.zsh"     # one exact file

[plugins.dotfiles-themes]
local = "~/dotfiles/themes"         # ~ and $VAR expand
optional = true                     # skipped when the path is missing

[plugins.gist-snippet]
gist = "579d02802b1cc17baed07753d09f5009"

[plugins.from-gitlab]
gitlab = "owner/repo"               # bitbucket and codeberg work the same way

[plugins.raw-file]
remote = "https://example.com/plugin.zsh"

[plugins.tiny]
inline = 'tiny() { echo "defined in the config"; }'

Validation runs before anything installs. Exactly one source per plugin, optional only with local, proto only on forge sources, cloneopts and depth only on git sources, no source-specific field on inline, and build only on directory sources. The troubleshooting section lists every message this produces.

Clone this wiki locally