A modern, high-performance Neovim plugin manager that leverages Neovim's built-in native package management (:help packages, vim.pack) while providing a rich, interactive, floating-window UI.
Unlike traditional native pack managers (like minpac or paq-nvim), pack.nvim focuses on developer experience with a beautiful dashboard, non-blocking asynchronous git operations, and real-time log streaming.
- Neovim 0.12+ β pack.nvim delegates all cloning, checkout, updating, pinning, and lockfile management to Neovim's built-in
vim.packAPI, which only exists in 0.12 and later. On an older Neovim,setup()warns and does nothing. giton yourPATH.
- Native Backend: All plugins install under
vim.pack's directory (<stdpath("data")>/site/pack/core/opt). Every plugin (lazy or not) ispackadd-ed explicitly through pack rather than relying on Neovim'sstart/auto-load, so lazy loading and ordered eager loading are fully under pack's control. - Native Git, Async Probes: Clone / checkout / update / pinning are handled by native
vim.pack. pack.nvim layers on non-blocking, concurrency-limited read-only git probes (viavim.system) purely to power the dashboard's "outdated" indicator and commit preview. - Optional Background Git (
use_git): Withuse_git = true, installs and updates run as backgroundedgitjobs (viavim.system), so the UI stays responsive during large transfers; nativevim.packthen runs cheaply afterwards β with objects already local β purely to register plugins and keep the lockfile in sync. Bulk updates (U/S/:Pack sync/:Pack update) are batched at a maximum of 5 plugins per call. - Interactive UI Dashboard: A centralized floating window showing real-time plugin statuses, log streaming, and pending-commit previews for outdated plugins.
- Reproducible installs: Version pinning (
branch/tag/commit/ semverversionranges) is resolved to a nativevim.packspec; native owns the lockfile, and:Pack restorerolls every plugin back to it. - Persistent Disable State: Disabling a plugin (via
xin the dashboard orset_disabled()) persists state tonvim-pack-extra.jsonwithout editing your raw Lua config. - Performance Caching: Pre-compiles lazy plugins'
ftdetectfiles into a single cache block, sourced at startup so their filetypes are detected before the plugin loads. - Lazy Loading: Supports
cmd,event(with patterns),ft(filetype), andkeys(keymap) triggers to load plugins right when you need them. When a spec sets bothftandkeys, the keymaps are never bound globally β they exist only in buffers whose filetype matches, before and after the plugin loads. - Modular Configuration: Keep your config clean by using
{ import = "plugins" }to split specs across multiple files. - Fine-Grained Loading Control: Toggle plugins with
enabledorcond, and guarantee eager-load order usingpriority. - Help Tags:
:helptags are generated automatically for every managed plugin'sdoc/directory on load. - Health Check: Run
:checkhealth packto verify your Neovim version,git, the install directory, per-plugin status, and orphaned directories.
pack.nvim leverages Neovim 0.12's native vim.pack for bootstrapping. Add this snippet to the top of your init.lua:
-- 1. Enable Neovim's built-in bytecode cache (must be on Line 1 for full caching benefits)
vim.loader.enable()
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- 2. Bootstrap pack.nvim using Neovim's native vim.pack
vim.pack.add({ { src = "https://github.com/igmrrf/pack.nvim", branch = "main" } })
vim.cmd.packadd("pack.nvim")
-- 3. Initialize pack.nvim with options and plugin specs
require("pack").setup({
performance = {
vim_loader = true, -- Fallback to ensure vim.loader.enable() is called if omitted
},
use_git = false, -- Clone/update via backgrounded `git` so the UI never blocks; native vim.pack still syncs the lockfile afterwards
ui = {
border = "rounded", -- Options: "single", "double", "rounded", "solid", "shadow"
auto_open = true, -- Automatically open dashboard float after the first plugin install completes
silent = nil, -- Silences native vim.pack cmdline messages (defaults to auto_open setting)
filter = "default", -- Options: "default" (vim.ui.input), "input" (vim.fn.input), or fun(opts, cb)
icons = {
loaded = "β",
not_loaded = "β",
error = "β",
sync = "βΊ",
queued = "β",
},
},
plugins = {
{ "igmrrf/pack.nvim" }, -- So pack.nvim can manage itself (updates, status, lockfile)
{ import = "plugins" }, -- Import specs from lua/plugins/* (files can call vim.pack.add or return a spec table)
},
})
require("configs")For alternative installation patterns (e.g. vim.uv.fs_stat fallback, raw git cloning), see the examples/ directory.
pack.nvim and native vim.pack share the exact same install directory
(<stdpath("data")>/site/pack/core/opt/<name>), so any plugin already installed via
vim.pack.add() is recognized as installed immediately, with no re-clone β just list it in your spec:
require("pack").setup({
plugins = {
{ "nvim-lua/plenary.nvim" }, -- already on disk from vim.pack.add() -> just gets packadd'd
},
})After setup(), pack.nvim replaces the global vim.pack.add/vim.pack.update/vim.pack.del with
lazy-aware wrappers, so you can keep calling vim.pack.add({ ... }) and it flows through pack.nvim's
loader. The install location and lockfile are owned by native vim.pack and are not configurable.
- Raw
vim.pack.addbeforesetup(), then declared withconfig/opts: if a plugin is installed/activated via a rawvim.pack.add()call beforerequire('pack').setup()runs, and is also declared in yourpluginsspec with aconfigoropts, thatconfigwill not run. Nativevim.packalready considers the plugin active and early-returns when pack.nvim's wrapper re-adds it, so our loader never gets a chance to fire. Either declare such plugins only through pack.nvim, or avoid raw pre-setup()adds for anything that needs aconfig. - Imperative imports vs. declarative priority ordering: eager-load
priorityordering is only guaranteed among declarative plugins. Plugins registered via imperativevim.pack.addcalls inside{ import = ... }files load in import order duringsetup(), not as part of the global priority sort. - First registration wins: if the same plugin is registered both by an imperative
vim.pack.addduring an import and by a declarative spec, the first registration wins and the later spec's fields (lazy/config/keys/opts) are silently ignored; declare each plugin once, in one style.
Plugin specifications can be defined as shorthand strings ("owner/repo"), tables, or URLs. Here is a comprehensive reference of supported spec keys:
| Key | Type | Description |
|---|---|---|
[1] / src |
string |
Plugin repository ("owner/repo"), full Git URL, or local path. |
as / name |
string |
Custom name or directory alias for the plugin. |
dir |
string |
Path to a local development plugin (bypasses git cloning). |
lazy |
boolean |
When true, defers loading until triggered by cmd, event, ft, keys, or require(). |
priority |
number |
Load order priority for eager plugins (higher values load first, default 50). |
enabled |
boolean|fun():boolean |
Toggle to enable or completely skip this plugin spec. |
cond |
boolean|fun(plugin):boolean |
Conditional expression or callback function to gate plugin loading. |
main |
string |
Overrides the target module name passed to opts auto-setup. |
cmd |
string|table |
User command(s) that trigger lazy loading. |
ft |
string|table |
Filetype(s) that trigger lazy loading. |
event |
string|table |
Autocmd event(s) or pattern(s) that trigger lazy loading (e.g. "BufReadPre"). |
pattern |
string|table |
Pattern filter string or table for autocmd event lazy triggers. |
keys |
string|table |
Keymap shortcut(s) that trigger lazy loading or register keybindings. Combined with ft, the keymaps are scoped buffer-locally to matching filetypes instead of being bound globally. |
module |
string |
Custom module name for require() trigger tracking. |
dependencies |
table |
List of dependent plugin specs loaded prior to this plugin. |
init |
fun(plugin) |
Callback executed BEFORE the plugin is loaded (useful for setting vim.g options). |
opts |
table |
Options table automatically passed to require(main).setup(opts). |
config |
fun(plugin, opts)|true |
Custom callback executed AFTER the plugin is loaded (overrides default opts behavior). If true, invokes setup() without arguments. |
build |
string|fun(plugin) |
Shell command or Lua function executed post-install / update. |
branch |
string |
Track a specific git branch. |
tag |
string |
Pin to a specific git tag. |
commit |
string |
Pin to a specific git commit hash. |
version |
string |
Pin to a semver version range (e.g. "^1.0.0"). |
category |
string |
Category metadata tag for filtering in the dashboard (/cat:lsp). |
tags |
string|table |
Custom tag string or table of tags for dashboard filtering (/tag:ui). |
pack.nvim exposes programmatic Lua helper functions for configuration and key mapping:
require("pack").setup(opts): Initializes pack.nvim with user configuration and plugin specs.require("pack").add(specs): Programmatically registers and installs new plugin specs post-startup.require("pack").map_keys(keys): Registers a list of keymaps in a single call:require("pack").map_keys({ { "<leader>e", "<cmd>Oil<cr>", desc = "Open Oil" }, { "<leader>gg", function() require("snacks").lazygit() end, desc = "Lazygit", mode = { "n", "v" } }, })
require("pack.state").set_disabled(name, disabled): Programmatically enables or disables a plugin and persists state tonvim-pack-extra.json.require("pack").status(): Returns a table containing{ total = <number>, loaded = <number>, disabled = <number> }.require("pack").stats(): Returns a formatted status summary string (e.g."π¦ 12/45 loaded").require("pack").picker(): Displays an interactive plugin selector usingsnacks.picker(orvim.ui.selectfallback) to navigate directly to plugin directories.
pack.nvim includes a built-in extension for lualine.nvim:
require("lualine").setup({
extensions = { "pack" },
})For full configuration examples and migration guides, see the examples/ directory:
- Basic Bootstrap: Standard
vim.uv.fs_stat+git clonebootstrapping snippet. - Native vim.pack Bootstrap: Minimal 0.12+ native
vim.pack.addbootstrapping. - Dependency Management: Automatically clone and load dependencies (
dependencies = { ... }). - Post-Install & Build Hooks: Execute shell commands, Vim commands, or Lua functions post-update (
build = ...). - Context-Aware Initialization: Use
init,config,cond, andbuildwith richPluginobject context. - Lazy Loading Triggers: Defer plugins by
cmd,event,ft,keys, orcond. - Modular Configs: Split plugin specs cleanly using
{ import = "plugins" }. - Full Spec Reference: View an exhaustive example spec showing all available options.
- Migration Guides: Guides for migrating from lazy.nvim, packer.nvim, or imperative vim.pack.
| Command | Description |
|---|---|
:Pack |
Opens the interactive dashboard UI to view current plugin status. |
:Pack sync |
Updates all managed plugins via native vim.pack. |
:Pack update [names...] |
Updates the given plugins (any count; batched at most 5 per native call), or all plugins if no name is given. |
:Pack clean |
Removes plugin directories no longer referenced in your configuration. |
:Pack restore |
Rolls every plugin back to the native vim.pack lockfile. |
:Pack repair |
Realigns lockfile (nvim-pack-lock.json) revisions to installed plugin HEAD commits. |
:Pack build [name] |
Re-runs the build hook for one plugin (or all plugins). |
:Pack load <name> |
Immediately loads a lazy plugin. |
:Pack delete <name> |
Removes a plugin from state and deletes it via native vim.pack. |
:Pack profile |
Displays the startup profile with visual bar charts showing plugin load times. |
:Pack diff |
Displays a structured diff of pending commits for outdated plugins before updating. |
:Pack picker |
Opens interactive plugin picker (snacks.picker / vim.ui.select) to edit plugin files. |
Subcommands with a <name> argument tab-complete against your configured plugins.
| Dashboard View | Plugin Quick Details |
|---|---|
All Plugins![]() |
Detailed Info Popup (<Enter> / K)![]() |
Outdated Updates View![]() |
Outdated Plugin Info & Diff![]() |
Disabled Plugins View![]() |
Disabled Plugin Info![]() |
When inside the dashboard (opened via :Pack), you can use the following keymaps:
q- Close the dashboard or any popup window.?- Show the interactive keymap help popup.S- Sync all managed plugins (install missing & pull updates; batched at most 5 plugins per update call).s- Sync the single plugin under the cursor.C- Clean unmanaged/deleted plugin directories.Tab/Shift-Tab(or1/2/3) - Cycle or jump directly across dashboard tabs (Plugins -> Updates -> Disabled).<Space>- Toggle selection state for the plugin under the cursor.v- Toggle selection UI mode or clear active selections.<CR>- Toggle inline plugin details expansion.K- Open full detail popup tailored to the current tab (branch, HEAD commit, revision info).l- Show streaming git output logs for the plugin under the cursor.p- Display the startup load time profiling chart (:Pack profile).d- Delete the plugin under the cursor from disk.D- Delete all disabled plugins from disk.x- Toggle disable/enable for the plugin under the cursor. An already-loaded plugin requires a Neovim restart to fully unload.c- Check for outdated plugins (concurrency-limitedgit fetch).u- Update the selected or cursor plugin.U- Update all outdated plugins (batched at most 5 plugins per update call).f- Filter the dashboard by plugin name, category (cat:lsp), or tag (tag:ui).
Each tab also renders a contextual quick-help bar at the bottom with tab-relevant action shortcuts.
| Feature / Dimension | pack.nvim |
lazy.nvim |
pckr.nvim |
paq-nvim |
vim-plug |
|---|---|---|---|---|---|
| Minimum Neovim | 0.12+ (Requires vim.pack) |
0.8+ | 0.7+ | 0.5+ | Vim 7.4 / Neovim 0.2+ |
| Backend Engine | Native C (vim.pack) |
Custom Lua engine | Native packpath |
Native packpath |
Custom Vimscript engine |
| Storage Location | <stdpath("data")>/site/pack/core/opt |
<stdpath("data")>/lazy |
<stdpath("data")>/site/pack/pckr/opt |
<stdpath("data")>/site/pack/paq/opt |
~/.config/nvim/plugged |
| Lockfile Format | Native nvim-pack-lock.json |
Custom lazy-lock.json |
Custom lockfile | None | None (snapshots) |
| Codebase Size | ~2,000 lines of Lua | ~20,000+ lines of Lua | ~3,500 lines of Lua | ~600 lines of Lua | ~2,700 lines of Vimscript |
| Lazy Loading Triggers | cmd, event, ft, keys, cond |
cmd, event, ft, keys, cond, custom |
cmd, event, ft, keys, cond |
None (Eager / packadd only) |
on (cmd), for (ft) |
| Dependency Support | Yes (dependencies) |
Yes (dependencies) |
Yes (requires) |
No | No (manual order) |
| Modular Specs | Yes ({ import = "..." }) |
Yes ({ import = "..." }) |
No | No | No |
Precompiled ftdetect Cache |
Yes (pack_ftdetect_cache.lua) |
Yes | No | No | No |
| Interactive Dashboard | Floating UI with tabs, / search, commit diffs |
Full-featured floating UI dashboard | Minimal floating log buffer | Minimal log buffer | Vim split buffer |
| Startup Profiling | Built-in (:Pack profile ASCII charts) |
Built-in (Timeline breakdown) | Built-in (:Pckr profile) |
None | Built-in (:PlugStatus) |
| Build Hooks | build (shell, :cmd, fn, list) |
build (shell, :cmd, fn, list) |
run (shell, fn, list) |
build (shell, fn) |
do (shell, fn) |
| Native Plugin Adoption | Yes (adopts disk plugins via vim.pack.get) |
No (requires managed dir) | Partial | Partial | No |
| Maintenance Model | Feature-frozen (stable & bug fixes) | Active development | Maintenance | Minimal / Stable | Maintenance |
pack.nvim is feature-complete, fully tested, and production-ready.
To preserve its zero-dependency model, ultra-fast startup, and rock-solid stability, the codebase is under a strict feature freeze:
- Bug fixes & Neovim compatibility updates are actively maintained.
- New core features will only be considered if requested and endorsed by 10 or more active users via GitHub Issues/Discussions.
Several declarative spec and lazy-loading features (such as import, programmatic conditionals, advanced event pattern matching, and context-aware hook variables) were heavily inspired by zpack.nvim and its foundational homage to lazy.nvim. pack.nvim combines these elegant spec configurations with a rich asynchronous floating dashboard on top of Neovim's native vim.pack backend.





