Plugins

June 16, 2026 · View on GitHub

A Hollow plugin is a directory of Lua files. The runtime clones git-based plugins, autoloads any hollow_plugin/*.lua files, and calls setup(opts) on the module if it provides one.

For the API see hollow.plugins. For working examples see examples/plugins/hollow-spirit (general) and examples/plugins/smart-splits (nvim integration).

Declaring plugins

Plugins are declared in your personal config:

local hollow = require("hollow")

hollow.plugins.setup({
  plugins = {
    -- short form: resolved to https://github.com/{user}/{repo}.git
    "user/repo",

    -- with opts passed to M.setup(opts)
    { "user/repo", opts = { key = "value" } },

    -- explicit git URL (any host: GitLab, Codeberg, SourceHut, ...)
    "https://gitlab.com/user/repo",

    -- local path (starts with ~ or /)
    "~/hollow-spirit",
    { "/absolute/path/to/plugin", opts = { ... } },
  },
})

A failed clone never aborts startup — it logs a warning and Hollow continues with the rest of the config.

Plugin layout

my-plugin/
  lua/                    -- prepended to package.path
    my-plugin/
      init.lua            -- should expose M.setup(opts) if configurable
  hollow_plugin/          -- all .lua files are autoloaded
    my-plugin.lua         --   or: hollow_plugin/my-plugin/init.lua
  • lua/ is for on-demand require(). The runtime prepends each plugin's lua/ to package.path before loading autoload files.
  • hollow_plugin/ is sourced unconditionally in alphabetical order at startup. Use it for keymaps, event listeners, and command registration.
  • A plugin may be autoload-only; setup() is optional.

Authoring a plugin

Minimal plugin (hollow-hello/):

-- hollow-hello/hollow_plugin/hello.lua
local hollow = require("hollow")

hollow.keymap.set("<leader>hi", function()
  hollow.ui.notify.info("hello from a plugin", { ttl = 1500 })
end, { desc = "say hi" })
-- hollow-hello/lua/hollow-hello/init.lua
local M = {}

function M.setup(opts)
  print("hollow-hello loaded with", vim and vim.inspect(opts) or "")
end

return M

Drop the directory somewhere stable and register it:

hollow.plugins.setup({ plugins = { "~/code/hollow-hello" } })

On the next startup, <leader>hi shows the toast, and M.setup({}) runs.

A more complete example is examples/plugins/hollow-spirit; it demonstrates a module with setup(opts), an autoloaded event listener, and a UI notifier.

Updating plugins

hollow.plugins.sync()

sync() walks all declared plugins. For each one with a .git directory, it runs git pull --ff-only --recurse-submodules and logs the result. Restart Hollow to pick up new code.

You can bind this to a key for convenience:

hollow.keymap.set("<leader>us", function()
  hollow.plugins.sync()
  hollow.ui.notify.info("Plugins synced — restart Hollow", { ttl = 2000 })
end, { desc = "sync plugins" })

Where plugins live

  • Git plugins are cloned into hollow.fs.data_dir() .. "/plugins".
  • data_dir() resolves to:
    • Windows: %APPDATA%\hollow
    • Linux/macOS/WSL: $XDG_DATA_HOME/hollow or ~/.local/share/hollow

Local plugins stay where they are; the loader reads from the path you give it.

Errors and recovery

SituationBehaviour
Git clone failsLogged, plugin skipped, others continue
hollow_plugin/*.lua errorsLogged, loader continues with the next file
require() of the module failsSilently skipped (plugin may be autoload-only)
setup() throwsLogged with traceback, loader continues
Local path missingLogged, plugin skipped

The loader never panics. Errors are visible in hollow.log next to the executable.

See also