Backpack Emacs is a self-documenting GNU Emacs starter kit inspired by Bedrock and Doom Emacs. It requires Emacs 29.1 or newer.
Users declare what features they want through a concise DSL (gear!) in their
private config file (~/.backpack.d/init.el or $XDG_CONFIG_HOME/backpack/init.el).
Backpack loads only what the user requests and auto-enables sensible defaults
that can be opted out of.
The configuration DSL is leaf.el (not use-package), and the package
manager is elpaca. Both are vendored as git submodules under
base-packages/ so no network access is needed for bootstrap.
Backpack organises features into three levels:
| Concept | Keyword style | Example | Description |
|---|---|---|---|
| Pouch | :keyword |
:editing |
A category of related features |
| Gear | symbol |
go |
A feature module (one .el file) |
| Flag | symbol |
lsp, -treesit |
An option that modifies a gear's behaviour |
Flags prefixed with - represent features that are on by default and can be
disabled by the user. Flags without - are opt-in.
Example user configuration:
(gear!
:ui
(theme doom-one)
:editing
(go -treesit lsp)
(python lsp)
nix
:config
(default hide-menu-bar hide-tool-bar no-splash)
:tools
magit
:checkers
spellchecking)All live under lisp/:
| File | Purpose |
|---|---|
backpack.el |
Main module (~300 lines): bootstrap, startup optimisations, directory layout, gear loading, orchestration |
backpack-pouch.el |
The gear!/gearp!/gear-with-any-flagp! macro system; backpack--extract-gear-form for split init loading |
backpack-platform.el |
Platform and system detection (OS, WSL) |
backpack-defaults.el |
Sensible global defaults (native comp, security, file locations) |
backpack-sync.el |
Package synchronisation (elpaca install/build/activate, sync-mode form filtering, gc-mode advice) |
backpack-gc.el |
Orphaned package cleanup (backpack-gc command) |
backpack-treesit.el |
Tree-sitter grammar declaration, installation, state tracking, and introspection |
backpack-email-utils.el |
backpack/mu4e-easy-context helper macro |
backpack-inventory.el |
Self-documenting inventory browser (M-x backpack-inventory) |
backpack-yaml-ls.el |
yaml-language-server LSP protocol extensions for Eglot (schema selection, schema browsing) |
Each gear is a standalone .el file under lisp/gears/<pouch>/<gear>.el.
Gear files do not use provide -- they are loaded directly via load calls
in backpack-load-gear-files. Every gear file self-gates using gearp! checks
so it is safe to load all of them unconditionally.
emacs-backpack/
├── early-init.el # Emacs entry point; loads backpack.el, calls backpack-start
├── ensure.el # Batch-mode package sync (backpack ensure)
├── gc.el # Batch-mode orphan package cleanup (backpack gc)
│
├── lisp/ # Core Backpack Emacs Lisp library
│ ├── backpack.el # Main module: bootstrap, startup optimisations, orchestration
│ ├── backpack-pouch.el # gear!/gearp! configuration query system
│ ├── backpack-platform.el # Platform and system detection (OS, WSL)
│ ├── backpack-defaults.el # Sensible global defaults (native comp, security, file locations)
│ ├── backpack-sync.el # Package synchronisation (elpaca install/build/activate)
│ ├── backpack-gc.el # Orphaned package cleanup
│ ├── backpack-email-utils.el # mu4e context helper
│ ├── backpack-inventory.el # Self-documenting inventory browser
│ ├── backpack-treesit.el # Tree-sitter grammar declaration, installation, and introspection
│ ├── backpack-yaml-ls.el # yaml-language-server LSP protocol extensions
│ └── gears/ # All feature modules, organised by pouch
│ ├── config/
│ │ └── default.el
│ ├── ui/
│ │ ├── theme.el
│ │ └── treesit.el
│ ├── completion/
│ │ ├── corfu.el
│ │ ├── eglot.el
│ │ ├── marginalia.el
│ │ ├── nerd-icons-completion.el
│ │ ├── orderless.el
│ │ └── vertico.el
│ ├── tools/
│ │ ├── cool-motions.el
│ │ ├── eldoc.el
│ │ ├── envrc.el
│ │ ├── magit.el
│ │ └── whitespaces.el
│ ├── checkers/
│ │ └── spellchecking.el
│ ├── email/
│ │ └── mu4e.el
│ ├── term/
│ │ ├── eshell.el
│ │ └── vterm.el
│ ├── ai/
│ │ └── anvil.el
│ └── editing/
│ ├── c.el
│ ├── cmake.el
│ ├── cpp.el
│ ├── emacs-lisp.el
│ ├── go.el
│ ├── haskell.el
│ ├── hyprland.el
│ ├── json.el
│ ├── latex.el
│ ├── lua.el
│ ├── make.el
│ ├── markdown.el
│ ├── nix.el
│ ├── objc.el
│ ├── org.el
│ ├── python.el
│ ├── rst.el
│ ├── rust.el
│ ├── terraform.el
│ ├── toml.el
│ └── yaml.el
│
├── base-packages/ # Vendored dependencies (git submodules)
│ ├── elpaca/ # Package manager
│ ├── leaf.el/ # Configuration DSL
│ └── leaf-keywords.el/ # Extended leaf keywords
│
├── bin/
│ └── backpack # Shell CLI (backpack ensure, backpack gc)
│
├── test/
│ ├── all-tests.el # Test runner
│ ├── startup-time.el # Startup time benchmark
│ └── pouch/
│ └── backpack-pouch.el # Unit tests for gear!/gearp! and backpack--extract-gear-form
│
├── etc/scripts/
│ ├── prepare-and-run.sh # Test helper: copy config to tmpdir, run tests
│ └── for-each-emacs.sh # Run tests against multiple Emacs versions
│
├── .cache/ # Runtime cache (gitignored)
│ ├── etc/ # Important data files
│ └── nonessentials/ # Deletable cache (elpaca builds, tree-sitter grammars)
│
├── devenv.nix # Nix dev environment (Emacs 29.1 through rolling)
├── devenv.yaml # Nix inputs for multiple Emacs versions
├── .github/workflows/ci.yml # CI: Nix + devenv across Emacs versions
├── .dir-locals.el # Per-project Emacs settings
├── .envrc # direnv integration
└── .gitmodules # Submodule declarations
early-init.el
└─ load lisp/backpack.el
├─ Checks Emacs >= 29.1
├─ Adds base-packages/ to load-path
├─ (require 'leaf), (require 'leaf-keywords)
├─ (require 'backpack-platform), (require 'backpack-defaults)
├─ (require 'backpack-pouch), (require 'backpack-email-utils), (require 'backpack-inventory), (require 'backpack-treesit)
├─ (require 'backpack-sync), (require 'backpack-gc)
├─ Sets up elpaca from base-packages/ (offline, no internet)
└─ Defines backpack-start, backpack-finalize, backpack-load-gear-files
└─ (backpack-start t)
├─ Creates required directories (.cache/etc, .cache/nonessentials, etc.)
├─ Parses user init ($backpack-user-dir/init.el) via backpack--extract-gear-form
│ ├─ Splits into (GEAR-FORM . REST-FORMS)
│ └─ The (gear! ...) form and everything else are separated
├─ Evaluates the gear! form (populates backpack--gear)
├─ Calls backpack-load-gear-files
│ └─ Loads ALL gear files in explicit order (each self-gates with gearp!)
│ Gear defaults are now set -- user overrides come next
├─ Evaluates REST-FORMS from init.el (user customizations override gear defaults)
├─ Loads custom.el
├─ Adds backpack-finalize as advice on command-line-1
└─ On finalize: runs backpack-after-init-hook → activates packages via elpaca
→ runs backpack-user-after-init-hook (packages fully loaded)
This split-loading order ensures that user customizations (e.g. setq,
set-face-attribute, with-eval-after-load) always override defaults set by
gear files, since the user's non-gear! forms are evaluated after all gears
have loaded.
Backpack provides two hooks that run during startup, in this order:
| Hook | When it runs | Purpose |
|---|---|---|
backpack-after-init-hook |
After command-line-1, before GC restore |
Package activation (elpaca-process-queues). Internal use only — do not add gear functions here. |
backpack-user-after-init-hook |
After backpack-after-init-hook completes |
Packages fully loaded and configured. Safe for gear setup code that depends on packages being ready. |
Why two hooks? Elpaca defers leaf body forms (including add-hook) until
after package activation. If a gear registers a function on
backpack-after-init-hook via leaf's :hook keyword, that add-hook happens
inside backpack-after-init-hook (during elpaca-process-queues). By the
time the function is added, run-hooks has already started iterating — the new
function may be missed. backpack-user-after-init-hook runs after
backpack-after-init-hook completes, so any functions added during package
activation are guaranteed to run.
When to use which hook:
backpack-user-after-init-hook: Gear setup that needs packages loaded (e.g.(anvil-enable),(anvil-server-start)). Use this in leaf:hook.backpack-after-init-hook: Internal Backpack machinery only (backpack--activate-packages).
bin/backpack ensure
→ emacs --batch -l ensure.el
├─ Sets backpack-mode to 'sync
├─ Loads backpack.el, user init.el
├─ Calls backpack-load-gear-files (queues packages for install/build)
├─ Waits for elpaca to install/build all packages
├─ Activates enable-on-sync packages (e.g., treesit-auto)
├─ Installs tree-sitter grammars
└─ Exits
bin/backpack gc [--dry-run]
→ emacs --batch -l gc.el
├─ Collects declared packages from gear files
├─ Compares against installed packages
└─ Deletes orphaned packages (or reports in dry-run)
A typical gear file looks like this:
;; Declare tree-sitter languages (if applicable)
(when (and (gearp! :editing go)
(not (gearp! :editing go -treesit)))
(backpack-treesit-langs! go gomod)
(add-to-list 'major-mode-remap-alist '(go-mode . go-ts-mode)))
;; Main leaf block
(leaf go-mode
:doc "Support for Go programming language in Emacs"
:when (gearp! :editing go)
:ensure (go-mode :ref "0ed3c5227e7f622589f1411b4939c3ee34711ebd")
:hook
((go-mode-hook go-ts-mode-hook) . electric-pair-local-mode)
:config
;; Nested leaf for an opt-in sub-feature
(leaf eglot
:doc "Language Server Protocol support for go-mode"
:when (gearp! :editing go lsp)
:doctor ("gopls" . "the official LSP implementation provided by the Go team")
:hook ((go-mode-hook go-ts-mode-hook) . eglot-ensure)))There are four patterns for controlling when a gear or sub-feature loads:
Pattern A -- Opt-in gear (user must list it):
(leaf magit
:when (gearp! :tools magit)
...)Pattern B -- Default-on gear (user must negate to disable):
(leaf ws-butler
:unless (gearp! :tools -whitespaces)
...)Pattern C -- Opt-in flag within a gear:
(leaf eglot
:when (gearp! :editing go lsp)
...)Pattern D -- Default-on flag (on unless user negates):
(unless (gearp! :editing go -display-line-numbers)
(display-line-numbers-mode +1))Backpack extends leaf.el with two custom keywords:
:doctor-- Declares external binaries used by a feature.:fonts-- Declares fonts required by a feature. Takes cons pairs of("font-name" . "description").
These keywords are metadata-only during normal load. They are parsed by
backpack-inventory.el for the self-documenting help system.
The :doctor keyword supports two formats. The old (simpler) format uses a cons
pair and is treated as an optional dependency:
;; Old format -- backward compatible, treated as optional
:doctor ("gopls" . "the official LSP implementation")The new format extends the cdr to a list, adding a requirement level:
;; Required -- gear won't work properly without this binary
:doctor ("gopls" . ("the official LSP implementation" required))
;; Optional -- nice to have (same as old format semantically)
:doctor ("impl" . ("generates method stubs" optional))
;; Conflicts -- this binary conflicts with another; user should have
;; one or the other, but not both
:doctor ("nil" . ("an incremental analysis assistant" (conflicts "nixd")))Requirement levels:
| Level | Meaning |
|---|---|
required |
The gear needs this binary to function correctly |
optional (or omitted) |
Nice to have, not necessary for the gear to work |
(conflicts "other-name") |
Conflicts with another binary; user should pick one |
Both formats can be mixed freely in the same :doctor declaration. Omitting
the level entirely (old cons-pair format) is equivalent to optional.
The inventory browser groups tools by level when a gear uses mixed levels: "Required tools:", "Optional tools:", and "Conflicting tools:" sections. When all tools share the same level (the common case), a single "External tools:" header is used instead.
All packages use pinned git refs via :ensure:
:ensure (go-mode :ref "0ed3c5227e7f622589f1411b4939c3ee34711ebd")The :ensure keyword is aliased to :elpaca internally.
Multiple :ensure entries can appear in a single leaf block -- leaf merges
duplicate keyword values by appending. The :config body is attached to the
last :ensure package's elpaca form; all preceding packages are installed
as standalone (elpaca pkg-spec) calls. When each package needs its own config,
use separate leaf blocks.
The eglot gear adds a -hover flag (default-on). When active, the LSP
textDocument/hover results display via eldoc in the echo area or childframe.
Users can opt out with (gearp! :completion eglot -hover) to remove
eglot-hover-eldoc-function from eldoc-documentation-functions.
The gear also sets eldoc-documentation-strategy to
eldoc-documentation-compose-eagerly in eglot-managed buffers so that
signature help, hover, and highlight results display as they arrive.
On Emacs < 31 running in a terminal, the corfu gear loads corfu-terminal
(and its dependency popon) to provide overlay-based completion popups as a
fallback for childframes (which don't work in TTY). Emacs 31+ supports
childframes in terminals natively, so corfu-terminal is gated behind
:emacs< 31.
The eldoc gear provides a box flag for rich documentation display:
- GUI frames:
eldoc-box-hover-at-point-modeshows docs in a childframe at point (via theeldoc-boxpackage). - TTY frames: childframes are unavailable; instead, the echo area shows
a concise single-line summary (
eldoc-echo-area-use-multiline-pnil,eldoc-echo-area-prefer-doc-buffermaybe, truncation hint enabled). - Daemon mode: both paths work per-frame -- the
eldoc-mode-hooklambda checks(display-graphic-p)at runtime to decide per-buffer.
C-h . (remapped by eglot to eldoc-doc-buffer) shows full documentation in
a right-side window. C-u C-h . dismisses it. In GUI frames, C-h . also
turns off eldoc-box-hover-at-point-mode (saving its state), and C-u C-h .
restores it.
The side window routing uses backpack--display-eldoc-side-window, registered
in display-buffer-alist for the *eldoc* buffer.
The anvil gear exposes Emacs as an MCP (Model Context Protocol) server so AI
agents can call Emacs capabilities directly. It uses backpack-user-after-init-hook
for startup (not backpack-after-init-hook) because elpaca defers leaf body
forms until after package activation — any add-hook on
backpack-after-init-hook during elpaca finalization would be too late.
Core modules are always loaded when the gear is active. Optional modules are controlled by opt-in flags:
| Flag | Modules enabled | Notes |
|---|---|---|
ide |
ide |
xref, diagnostics, imenu |
state |
sqlite, org-index |
Persistent KV store |
cron |
cron |
Scheduled task runner |
http |
http, sqlite, org-index |
HTTP with ETag cache (needs state) |
browser |
browser |
Web capture via agent-browser CLI |
Example user configuration:
(gear! :ai anvil) ;; core only
(gear! :ai (anvil ide state)) ;; core + selective optional
(gear! :ai (anvil ide state http browser)) ;; everything| Pattern | Example | Usage |
|---|---|---|
backpack-* |
backpack-emacs-dir, backpack-start |
Public API / variables |
backpack--* |
backpack--gear, backpack--treesit-langs |
Internal/private symbols (double hyphen) |
backpack/* |
backpack/mu4e-easy-context |
User-facing utility functions |
gear! |
gear! |
Declarative macro (bang = side-effectful) |
gearp! |
gearp! |
Predicate macro (p = predicate) |
*-p |
backpack-sync-mode-p |
Boolean predicate functions |
backpack--*-h |
backpack--reset-file-handler-alist-h |
Hook functions (h suffix, Doom convention) |
-flag |
-treesit, -display-line-numbers |
Negation/opt-out flags in gear! |
Gear files do not use a backpack-gear- prefix. Leaf blocks are named after
the package they configure (e.g., go-mode, corfu, jinx).
M-x backpack-inventory opens a hierarchical, navigable help buffer with three
levels:
- Pouch listing -- all pouches with descriptions and gear counts
- Gear listing -- all gears in a pouch with enabled/disabled/default-on status
- Gear detail -- full info: description, flags, external tools, fonts, example
gear!snippet
Navigation:
RET-- drill into a pouch or gearlorDEL-- go back (browser-like history stack)g-- refresh (re-scan files)q-- quit
The inventory uses a Doom Emacs-inspired visual style:
- Header bar (
header-line-format) shows a clickable<- Go backlink (when history exists), breadcrumb path, and context-appropriate key hints aligned to the right. - Icons from
nerd-icons(package icon for pouches, gear icon for gears) with graceful fallback to plain text whennerd-iconsis not installed. - Custom faces (
backpack-inventory-pouch-face,backpack-inventory-gear-face, etc.) that inherit from standardfont-lock-*faces so themes control colours. - Cursor/mouse highlight using
cursor-faceandmouse-faceproperties with:inverse-video t-- when point or mouse hovers over an interactive item, the text colour becomes the background creating a "selected pill" effect. Requirescursor-face-highlight-mode(Emacs 29.1+, enabled automatically). - Tooltips via
help-echoreplace the old[*] = enabledlegend. Status indicators, flag names, and interactive items all show contextual help on hover or when point rests on them.
The system parses gear source files on demand using Emacs's read function.
It does not require annotations or metadata files. It discovers pouches by
scanning subdirectories of lisp/gears/, and gears by listing .el files in
each pouch directory.
For each file, it reads S-expressions and recursively walks them to extract:
gearp!/gear-with-any-flagp!calls (determines gear names, flags, and default-on status):docstrings from leaf blocks:doctorentries (external tool requirements):fontsentries (font requirements):when/:unlesscontext (determines whether a feature is opt-in or default-on)
It handles all the edge cases in the codebase: (not (gearp! ...)) context
flipping, (or ...) / (and ...) compound conditions, cross-gear references,
gear names that differ from filenames (e.g., envrc.el defines the direnv
gear), and the gear-with-any-flagp! macro used in the theme system.
Parsing adds zero startup cost since it only runs when the user invokes the command.
Tests use ERT (Emacs Regression Testing). The test suite lives in test/.
With Nix/devenv available:
devenv test # Runs tests across all Emacs versions (29.1 through rolling)Manually:
emacs --batch -l test/all-tests.el -f ert-run-tests-batch-and-exitThe devenv.nix environment provides Emacs versions 29.1, 29.2, 29.3, 29.4,
30.1, and rolling. CI runs devenv test which executes
etc/scripts/for-each-emacs.sh to test against all versions.
- All Emacs Lisp files use
lexical-binding: t. - There are no
defcustomvariables in the project. Configuration is entirely through thegear!/gearp!macro system. - The user's
gear!declaration is the single source of truth for what features are active. It is stored inbackpack--gearat runtime.
To add a new gear:
- Create
lisp/gears/<pouch>/<gear-name>.el. - Use
gearp!in:when/:unlessto gate the gear's leaf blocks. - Include a
:docstring on each leaf block for the inventory system. - If external tools are needed, use the
:doctorkeyword. - If fonts are needed, use the
:fontskeyword. - Add a
loadcall tobackpack-load-gear-filesinlisp/backpack.el. Gear files are not auto-discovered for loading -- they must be explicitly listed. (The inventory system discovers them from the filesystem, but the load order is explicit.) - If the gear supports tree-sitter, use
backpack-treesit-langs!to declare the needed grammars, gated behind(not (gearp! :pouch gear -treesit)).
To add a new pouch:
- Create a directory
lisp/gears/<pouch-name>/. - Add gear files inside it.
- Add a description to
backpack-inventory--pouch-descriptionsinlisp/backpack-inventory.el. - Optionally add it to
backpack-inventory--pouch-orderif you want it to appear in a specific position in the inventory browser.
All external packages must use pinned git refs. Never use :ensure t or
unpinned refs. This ensures reproducible builds.
When a sub-feature should be on by default (like tree-sitter support or line numbers), gate it with a negation check:
(unless (gearp! :editing go -display-line-numbers)
(display-line-numbers-mode +1))This means the feature is active unless the user explicitly adds -display-line-numbers
to their gear declaration. For top-level gear defaults, use :unless (gearp! :pouch -gearname).
Some gears reference other gears (e.g., haskell.el checking (gearp! :editing org)
for org-babel integration). These are dependency checks, not gear definitions.
The inventory parser handles these by preferring the gear entry from the file
whose name matches the gear.