| name | Emacs Ecosystem |
| description | This skill should be used when the user asks to "write elisp", "emacs config", "init.el", "use-package", ".el file", "emacs lisp", or "magit". Provides comprehensive Emacs ecosystem patterns and best practices. For org-mode, use org-ecosystem skill. |
| version | 2.1.0 |
Provide comprehensive patterns for Emacs Lisp, configuration management, package systems, and major packages including Magit and LSP integration. For org-mode patterns, see org-ecosystem skill.
<elisp_fundamentals>
S-expressions as code and data (homoiconicity). Prefix notation for all operations.
Emacs Lisp data types: symbol, cons cell, list, vector, hash-table, string, number
;; symbol: Named objects
'foo
:keyword
;; cons_cell: Pair
(cons 1 2) ; => (1 . 2)
;; list: Linked cons cells
'(1 2 3)
;; vector: Fixed-size array
[1 2 3]
;; hash-table: Key-value store
(make-hash-table)
;; string: Text
"hello"
;; number: Integer or float
42
3.14
</example>
Define functions with defun
(defun my-function (arg1 arg2)
"Docstring describing the function."
(+ arg1 arg2))
Local variable binding with let and let*
(let ((x 1)
(y 2))
(+ x y))
(let* ((x 1)
(y (+ x 1))) ; y can reference x
y)
</example>
Conditional forms: if, when, unless, cond, pcase
(if condition
then-form
else-form)
(when condition
body-forms...)
(unless condition
body-forms...)
(cond
(condition1 result1)
(condition2 result2)
(t default-result))
(pcase value
('symbol (handle-symbol))
((pred stringp) (handle-string))
(_ (handle-default)))
</example>
Iteration patterns: dolist, dotimes, cl-loop, seq functions
(dolist (item list)
(process item))
(dotimes (i 10)
(process i))
(cl-loop for item in list
collect (transform item))
(seq-map #'transform sequence)
(seq-filter #'predicate sequence)
(seq-reduce #'fn sequence initial)
</example>
Anonymous functions with lambda
(lambda (x) (* x 2))
(mapcar (lambda (x) (* x 2)) '(1 2 3))
;; lambda self-quotes — #' is optional when passing it directly
(mapcar (lambda (x) (+ x 1)) list)
</example>
Define macros with defmacro. Use backquote for templates, comma for evaluation
(defmacro with-temp-message (msg &rest body)
"Execute BODY with MSG displayed temporarily."
`(let ((message-log-max nil))
(message "%s" ,msg)
(unwind-protect
(progn ,@body)
(message nil))))
Modern init.el organization
;;; init.el --- Emacs configuration -*- lexical-binding: t; -*-
;;; Commentary:
;; Personal Emacs configuration
;;; Code:
;; Bootstrap package manager
(require 'package)
(setq package-archives
'(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")))
(package-initialize)
;; use-package is built-in since Emacs 29; no installation needed
(eval-when-compile
(require 'use-package))
;; Configuration sections...
(provide 'init)
;;; init.el ends here
</example>
Declarative package configuration with use-package keywords
Does the package need lazy loading or declarative configuration?
Use use-package for clean, maintainable configuration
Use require for simple packages with no configuration needs
(use-package corfu
:ensure t
:defer t
:hook (prog-mode . corfu-mode)
:bind (:map corfu-map
("C-n" . corfu-next)
("C-p" . corfu-previous))
:custom
(corfu-auto t)
(corfu-cycle t))
Keywords:
- :ensure - Install package if not present
- :defer - Lazy load (t or seconds)
- :hook - Add to mode hooks
- :bind - Define keybindings
- :custom - Set customizable variables
- :init - Run before package loads
- :config - Run after package loads
- :commands - Autoload commands
- :after - Load after specified packages
- :if/:when/:unless - Conditional loading
Key binding patterns: global-set-key, define-key, use-package :bind
;; Global keybinding
(global-set-key (kbd "C-c l") #'org-store-link)
;; Mode-specific
(define-key emacs-lisp-mode-map (kbd "C-c C-e") #'eval-last-sexp)
;; With use-package
(use-package magit
:bind (("C-x g" . magit-status)
("C-x M-g" . magit-dispatch)))
;; Keymap definition
(defvar my-prefix-map (make-sparse-keymap)
"Keymap for my custom commands.")
(global-set-key (kbd "C-c m") my-prefix-map)
(define-key my-prefix-map (kbd "f") #'find-file)
</example>
Hook management with add-hook and use-package :hook
;; Add function to hook
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
;; Remove function from hook
(remove-hook 'prog-mode-hook #'display-line-numbers-mode)
;; Lambda in hook (discouraged for removability)
(add-hook 'after-save-hook
(lambda () (message "Saved!")))
;; With use-package
(use-package flycheck
:hook (prog-mode . flycheck-mode))
</example>
Modify existing functions with advice-add and advice-remove
(defun my-after-save-message (orig-fun &rest args)
"Show message after save."
(apply orig-fun args)
(message "Buffer saved at %s" (current-time-string)))
(advice-add 'save-buffer :around #'my-after-save-message)
;; Remove advice
(advice-remove 'save-buffer #'my-after-save-message)
</example>
Define customizable variables with defgroup and defcustom
(defgroup my-package nil
"My package customization."
:group 'convenience
:prefix "my-package-")
(defcustom my-package-option t
"Enable my-package option."
:type 'boolean
:group 'my-package)
(defcustom my-package-list '("a" "b")
"List of strings."
:type '(repeat string)
:group 'my-package)
</example>
Built-in package manager for Emacs. Reliable and sufficient for most workflows.
;; Commands:
;; - package-install - Install a package
;; - package-delete - Remove a package
;; - package-refresh-contents - Update package list
;; - package-list-packages - Browse packages
(require 'package)
(setq package-archives
'(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")))
(package-initialize)
;; Install a package
(package-install 'magit)
</example>
Built-in since Emacs 29. The standard declarative way to configure packages. No installation needed on Emacs 29+.
;; use-package is built-in since Emacs 29; just require it
(eval-when-compile
(require 'use-package))
;; Declarative package configuration
(use-package magit
:ensure t
:bind ("C-x g" . magit-status))
</example>
Functional package manager with Git integration. Still widely used, but elpaca is gaining adoption for reproducible package management.
;; Bootstrap
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el"
user-emacs-directory)))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el")
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
;; Use with use-package
(straight-use-package 'use-package)
(setq straight-use-package-by-default t)
;; Install package
(use-package magit
:straight t)
</example>
Modern async package manager gaining adoption for reproducible package management. An alternative to straight.el with improved performance.
;; Bootstrap
(defvar elpaca-installer-version 0.7)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
;; ... (bootstrap code)
;; Use with use-package
(elpaca elpaca-use-package
(elpaca-use-package-mode))
(use-package magit
:ensure t)
</example>
Git porcelain for Emacs
Basic Magit setup with use-package
(use-package magit
:ensure t
:bind (("C-x g" . magit-status)
("C-x M-g" . magit-dispatch)
("C-c M-g" . magit-file-dispatch)))
Magit status buffer keybindings
;; s - Stage file/hunk
;; u - Unstage file/hunk
;; c c - Commit
;; P p - Push
;; F p - Pull
;; b b - Checkout branch
;; b c - Create branch
;; l l - Log current branch
;; d d - Diff
Magit configuration settings
(setq magit-save-repository-buffers 'dontask)
(setq magit-display-buffer-function
#'magit-display-buffer-same-window-except-diff-v1)
(setq magit-diff-refine-hunk 'all)
GitHub/GitLab integration with Forge
(use-package forge
:after magit
:ensure t)
<lsp_integration>
<decision_tree name="when_to_use">
Do you need LSP features like completion, go-to-definition, and diagnostics?
<if_yes>Use eglot (built-in, recommended default). Use lsp-mode only for advanced configurations requiring features beyond eglot.</if_yes>
<if_no>Use basic major modes without LSP overhead</if_no>
</decision_tree>
Built-in LSP client (Emacs 29+). Recommended default for most use cases. Tightly integrated with Emacs core, leveraging built-in completion (completion-at-point), Flymake for diagnostics, and project.el for project management.
(use-package eglot
:ensure nil ; built-in since Emacs 29
:hook ((python-mode . eglot-ensure)
(python-ts-mode . eglot-ensure)
(typescript-ts-mode . eglot-ensure)
(rust-ts-mode . eglot-ensure))
:config
(setq eglot-autoshutdown t)
(setq eglot-events-buffer-size 0)
;; Emacs 30+: improved tree-sitter integration with eglot
(setq eglot-report-progress nil))
;; Custom server configuration
(add-to-list 'eglot-server-programs
'(rust-ts-mode . ("rust-analyzer")))
</example>
Feature-rich LSP client for advanced configurations. Use when eglot does not meet requirements (e.g., DAP integration, custom UI features via lsp-ui).
(use-package lsp-mode
:ensure t
:hook ((python-mode . lsp-deferred)
(typescript-mode . lsp-deferred))
:commands (lsp lsp-deferred)
:custom
(lsp-keymap-prefix "C-c l")
(lsp-idle-delay 0.5)
(lsp-log-io nil)
:config
(lsp-enable-which-key-integration t))
(use-package lsp-ui
:ensure t
:hook (lsp-mode . lsp-ui-mode)
:custom
(lsp-ui-doc-enable t)
(lsp-ui-sideline-enable t))
</example>
LSP completion with corfu (recommended) or company. Corfu works with Emacs built-in completion-at-point and pairs well with eglot. Cape provides additional completion-at-point backends.
;; With corfu + cape (current best practice)
(use-package corfu
:ensure t
:custom
(corfu-auto t)
(corfu-cycle t)
:init
(global-corfu-mode))
(use-package cape
:ensure t
:init
(add-hook 'completion-at-point-functions #'cape-dabbrev)
(add-hook 'completion-at-point-functions #'cape-file))
;; With company (traditional, still maintained)
(use-package company
:ensure t
:hook (after-init . global-company-mode)
:custom
(company-idle-delay 0.2))
</example>
<modern_packages>
Vertical completion UI. Part of the current best-practice completion stack: vertico (UI), orderless (matching), marginalia (annotations), consult (commands), embark (actions).
(use-package vertico
:ensure t
:init (vertico-mode))
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic)))
(use-package marginalia
:ensure t
:init (marginalia-mode))
(use-package consult
:ensure t
:bind (("C-s" . consult-line)
("C-x b" . consult-buffer)
("M-g g" . consult-goto-line)))
</example>
Display available keybindings
(use-package which-key
:ensure t
:diminish
:init (which-key-mode))
Native tree-sitter integration (Emacs 29+, improved in Emacs 30). Emacs 30.2 includes enhanced tree-sitter support with better fontification, indentation, and navigation. Use *-ts-mode variants for tree-sitter-backed major modes.
(setq treesit-language-source-alist
'((python "https://github.com/tree-sitter/tree-sitter-python")
(javascript "https://github.com/tree-sitter/tree-sitter-javascript")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript"
"master" "typescript/src")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript"
"master" "tsx/src")))
;; Install grammars
(mapc #'treesit-install-language-grammar
(mapcar #'car treesit-language-source-alist))
;; Remap modes to tree-sitter variants
(setq major-mode-remap-alist
'((python-mode . python-ts-mode)
(javascript-mode . js-ts-mode)
(typescript-mode . typescript-ts-mode)
(css-mode . css-ts-mode)
(json-mode . json-ts-mode)))
;; Emacs 30+: treesit-auto can manage grammar installation
;; and mode remapping automatically
</example>
<treesit_mode_availability>
Correctly detecting whether a tree-sitter major mode will actually work, versus merely existing as a symbol.
On Emacs 29.1+, the *-ts-mode functions (for example json-ts-mode, python-ts-mode) are autoloaded built-ins. Because they are autoloaded, their symbols are always fboundp regardless of whether the tree-sitter grammar shared library (libtree-sitter-LANG.so / .dylib) is installed. So `(fboundp 'json-ts-mode)` returns non-nil even when activating that mode would fail with "language grammar for LANG is unavailable". fboundp answers "is this mode defined?" not "can this mode run?".
Check grammar availability with `treesit-language-available-p`, which returns non-nil only when the grammar for a language exists and can be loaded. It takes a language symbol (for example `json`), not a mode symbol, so a mode-to-language mapping is required. `treesit-ready-p` is a higher-level convenience that also verifies readiness and can emit a diagnostic message; prefer it when you want the standard user-facing warning, and `treesit-language-available-p` when you want a silent boolean.
;; Map ts-mode symbols to their grammar language symbols, because the
;; language name is not always the mode-name prefix (js-ts-mode -> javascript).
(defvar my-ts-mode-language-alist
'((json-ts-mode . json)
(js-ts-mode . javascript)
(python-ts-mode . python)))
(defun my-ts-mode-available-p (mode)
"Return non-nil if MODE is a ts-mode whose grammar is loadable."
(when-let ((lang (alist-get mode my-ts-mode-language-alist)))
(and (fboundp mode)
(treesit-language-available-p lang))))
</example>
When a mode-to-language mapping is maintained separately from the list of modes a package actually dispatches to, the two tables drift: a new ts-mode is added to the dispatch list but its grammar language is never registered, so the availability check silently returns nil and the mode is never selected.
Add a unit test that asserts every ts-mode referenced by the package appears in the mode-to-language mapping (and vice versa). This turns a silent runtime fallthrough into a fast, deterministic test failure whenever the tables diverge.
;; my-output-mode-alist is the dispatch list; my-ts-mode-language-alist is
;; the grammar mapping. Assert every ts-mode in the former is registered.
(ert-deftest my-ts-mode-alist-sync ()
(dolist (entry my-output-mode-alist)
(let ((mode (cdr entry)))
(when (string-suffix-p "-ts-mode" (symbol-name mode))
(should (assq mode my-ts-mode-language-alist))))))
<keymap_testing>
Reliably asserting keymap contents in unit tests. Keymaps are a data structure, and the convenient lookup APIs have edges that produce false negatives.
`lookup-key` and `where-is-internal` are lossy for test assertions. For a key sequence that is only a prefix of a longer binding, `lookup-key` returns an integer (the number of events consumed) rather than a command, which is easy to misread as "bound to something". Bindings that live inside a composed keymap (built with `make-composed-keymap`) or a nested prefix keymap can also be missed depending on how the lookup is issued.
For composed or prefix-heavy keymaps, walk the raw keymap structure recursively with `map-keymap`, descending into nested keymaps yourself, and assert on the commands you collect. This inspects the actual structure instead of trusting a resolver that can return prefix-depth integers or skip composed layers.
(defun my-keymap-commands (keymap)
"Collect all commands bound anywhere in KEYMAP, recursively."
(let (acc)
(map-keymap
(lambda (_event binding)
(cond
((keymapp binding)
(setq acc (append acc (my-keymap-commands binding))))
((commandp binding)
(push binding acc))))
keymap)
acc))
Named function keys must use angle-bracket syntax. `(kbd "")` returns the named-key vector `[left]`, which is what keymaps store for the arrow key. `(kbd "[left]")` instead returns the six literal characters `[`, `l`, `e`, `f`, `t`, `]`. Looking a binding up with the wrong form fails to match, and because `[` is itself a self-inserting prefix, `lookup-key` can return an integer partial-match, disguising the mistake.
Always write named keys with angle brackets in both bindings and test lookups: `""`, `""`, `""`, `""`, `""`, `""`.
Code that dispatches a command with `call-interactively` requires that the target satisfy `commandp`, i.e. it must be an interactive function. When a test replaces a real command (for example a navigation command) with a plain lambda to observe calls, `call-interactively` signals `(wrong-type-argument commandp ...)` because the stub is not interactive.
Give mock lambdas an `(interactive)` form when the code under test invokes them via `call-interactively`.
(cl-letf (((symbol-function 'forward-char)
(lambda (&rest _) (interactive) (setq nav-called t))))
...)
A mode's keymap and the mode entry point are frequently defined in different files (the keymap via `defvar-keymap` in the main feature file, helper commands in a sibling file). A test that requires only the helper feature can observe an unbound or empty keymap and assert against nothing.
Require the feature that actually defines the keymap before asserting on it; do not assume the keymap lives in the file whose name resembles "keymap".
<bytecompile_verification_hazards>
Byte-compilation artifacts and load order can make tests lie: a run can pass or fail against code that is not the source you just edited.
By default Emacs prefers the compiled file: with both LIB.el and LIB.elc present on the same load-path entry, `load` uses LIB.elc even when LIB.el is newer, emitting only a warning (which is easy to miss in batch output). A native-compiled .eln is preferred over .elc, which is preferred over .el. Consequently a stale .elc can hide a source fix: the test exercises old bytecode, so a passing test does not prove the patch works, and a failing test may not reflect the current source.
Before trusting a batch ERT/byte-compile result, either delete source-tree .elc artifacts, or set `load-prefer-newer` to t in the batch invocation so `load` picks whichever of .el/.elc is newest by modification time. Better still, byte-compile to a temporary destination so verification never leaves .elc files in the source tree. If a result contradicts a source change, suspect stale bytecode first.
# Batch verification that will not silently run stale bytecode:
# remove source-tree .elc, force newest-source loading, then run ERT.
find . -name '*.elc' -delete
emacs -Q --batch \
--eval '(setq load-prefer-newer t)' \
-L . -L test \
-l ert -l my-feature -l my-feature-test \
-f ert-run-tests-batch-and-exit
Macros are expanded at compile time in the file that calls them. When a macro is defined in one file and invoked in another, recompiling only the macro-defining file is not enough: the call-site file still carries an old expansion (or, if interpreted, resolves the macro at run time), and can fail with `invalid-function` or call a stale expansion.
Compile the macro-defining file and all of its call-site files together, then run the tests with the same load-path set. Treat a macro's callers as part of its compilation unit.
A batch test run fails at load time, before any test executes, if a required feature's directory is absent from the load-path. Transitive requires matter: a test-support file that requires feature A, which in turn requires feature B, needs both A's and B's directories on `-L`, or the loader errors first.
Pass one `-L DIR` for every directory that contributes a required feature, including transitive dependencies and test-support helpers, not just the directory holding the test file.
Tests that inspect the structure of macro output are brittle because expansion shape varies. A `defun`-generating macro can expand to `(defalias NAME #'(lambda ...))` rather than a literal `defun`, and `macroexpand` is a top-level contract only: it may fully expand the outermost macro (for example into a `progn`) while leaving nested macro calls inside `let` untouched.
Normalize the expansion to a canonical shape before asserting on heads, membership, or tail forms, so tests stay stable across byte-compiled and directly-macroexpanded paths. Do not hard-code one particular expansion layout.
<autoload_cookie_safety>
Where ;;;###autoload cookies are safe to place, so that generated autoload files contain autoload calls rather than executable code.
The autoload machinery (`loaddefs-generate`) copies the form following a `;;;###autoload` cookie verbatim into the generated loaddefs file, except for a fixed set of recognized definition forms which it converts into safe `autoload` calls: `defun`, `defmacro`, `cl-defun`, `cl-defmacro`, and `define-overloadable-function`. Put a cookie before anything else, such as a custom macro invocation (`defun/foo ...`, a mode-defining macro) or a side-effecting top-level form (`(some-register ...)`), and the whole form is copied raw into loaddefs and executed at load time. That runs side effects unconditionally and can fail if the macro is not yet defined when loaddefs loads.
Only place a bare cookie before a real `defun`/`defmacro` (or the other recognized forms). To autoload a name produced by a custom macro, write the explicit form on the line after the cookie so you control exactly what is recorded:
`;;;###autoload (autoload 'my-command "my-file")`
Otherwise, remove the unsafe cookie.
When a package is installed directly from source (`package-vc`, `use-package :vc`), the package manager may traverse and byte-compile the `test/` directory during install. On Emacs 30.x, `.elpaignore` and a README `:ignored-files ("test/")` declaration do not reliably stop `package--compile` from descending into tests, so compilation fails on test-only files that require unavailable test helpers.
A repo-side approach that has worked on Emacs 30.x is to add `test/.dir-locals.el` binding `no-byte-compile` to t, i.e. `((emacs-lisp-mode . ((no-byte-compile . t))))`. This lets the installer traverse the tests while skipping their byte-compilation. Because `no-byte-compile` is fundamentally a per-file variable honored when each file is compiled, verify the behavior against your target Emacs version rather than assuming it suppresses compilation of every file in the tree.
<testability_design>
Structuring Elisp so that behavior is reachable by unit tests without stubbing macros, and so that load order stays explicit.
Output produced through a macro boundary such as `with-help-window` is awkward to test: assertions have to stub the macro via `eval` tricks or rebind its `symbol-function`, which couples tests to expansion details.
Extract the rendering into a small, pure-ish helper that writes into the current buffer, and keep the public command a thin wrapper around that helper plus the window-opening macro. Tests then call the helper inside `with-temp-buffer` and assert on buffer contents directly. The seam is the point where side-effecting presentation meets pure content generation.
Macros used only within one feature still force every call-site file to know the macro at compile time. Left inline in a large feature file, they blur the data/logic boundary and make load order implicit.
Move feature-local macros into a sibling `*-macros.el` module, keep runtime functions in the original file, and have the original `require` the macros module. Verify by byte-compiling both files. This makes load order explicit and shrinks the feature file.
A family of nearly identical interactive commands invites a parallel data table describing them, which becomes a second source of truth that drifts from the definitions.
Define the family with a declarative `defmacro` that expands into the command definitions, passing per-command differences as explicit forms. When the macro invocations are the only consumers of the parallel table, delete the table so the macro forms are the single source of truth.
<upstream_contribution>
A generalized recon checklist for contributing to an established Emacs Lisp project before opening a pull request. The goal is to discover a project's conventions from its own artifacts rather than guessing.
Emacs packages vary widely in commit style, changelog format, naming, and test harness. Submitting against the wrong conventions causes review churn. Every convention is discoverable from files already in the repository.
- Commit style: read CONTRIBUTING plus `git log` for the actual subject/body norm (imperative ~50-char subject with 72-char wrapped body is common; conventional-commit prefixes like feat:/fix:/docs: appear in many projects even without a rule).
- Changelog: find the file and format (a `CHANGELOG.org` in Org markup vs a `NEWS` file), including any symbol-quoting convention (for example `~symbol~` in Org, or `` `nil' `` in docstrings).
- Naming: confirm the private/public prefix split (private `pkg--`, public `pkg-`, sometimes with a group-specific middle segment).
- Test harness: determine how tests run (a `make test` target, `eask`, `ert-runner`), the test-file layout (`test/pkg-*.el`), test tags used to skip environment-specific cases, and the mocking idiom in use (commonly `cl-letf` on `symbol-function`).
- Compatibility gate: note the minimum supported Emacs version and CI matrix, and whether byte-compilation is treated as an error (`byte-compile-error-on-warn`) so your change must compile cleanly there.
- Formatting commits: check whether whitespace/formatting-only changes must be a separate commit recorded in `.git-blame-ignore-revs`.
For MELPA submission specifics (recipe format, package-lint/checkdoc gates, PR mechanics), see the melpa-packaging skill.
</upstream_contribution>
<context7_integration>
<usage_pattern>
Resolve library ID (known: /websites/emacsdocs)
Workflow guidance
Step completed
Fetch documentation with specific topic
Workflow guidance
Step completed
Emacs Lisp programming patterns
Package configuration patterns
Org mode configuration
Magit usage and configuration
Hook usage patterns
</usage_pattern>
<common_queries>
Key binding patterns
Function definition
Advice system usage
Customization variables
</common_queries>
</context7_integration>
<best_practices>
Enable lexical-binding in all Elisp files: -- lexical-binding: t; --
Use #'function-name for function references (enables byte-compiler warnings)
Document functions with docstrings
Namespace all symbols with package prefix
Prefer seq.el functions for sequence operations
Use pcase for complex pattern matching
Use defcustom for user-configurable options
Use provide at end of file
Prefer :custom over setq in use-package
Use :hook instead of add-hook in use-package
Lazy load packages with :defer, :commands, or :hook
Use native-compilation when available (Emacs 28+)
Prefer eglot for LSP (built-in since Emacs 29, recommended default)
Use tree-sitter *-ts-mode variants when available (Emacs 29+, improved in 30.2)
Use the modern completion stack: vertico, orderless, marginalia, consult, corfu, cape
use-package is built-in since Emacs 29; no need to install it
Use Emacs 30.x as the baseline and defer to the active package set for the exact stable point release
</best_practices>
<anti_patterns>
Using dynamic binding when lexical is needed
Add lexical-binding: t to file header
Hardcoding absolute paths
Use expand-file-name, user-emacs-directory, or locate-user-emacs-file
Requiring packages at top level unconditionally
Use autoload, use-package with :defer, or eval-after-load
Modifying global state without restoration
Use let-binding or save-excursion/save-restriction
Adding lambdas to hooks (hard to remove)
Define named functions and add those
Using setq for defcustom variables
Use customize-set-variable or :custom in use-package
Using deprecated cl library
Use cl-lib with cl- prefixed functions
Using eval-after-load with string
Use with-eval-after-load or use-package :config
Complex logic in early-init.el
Keep early-init.el minimal (frame settings, package setup)
Using lsp-mode when eglot meets all requirements
Use eglot (built-in since Emacs 29, preferred default). Only use lsp-mode for DAP integration or lsp-ui features not available in eglot.
Using company-mode for in-buffer completion
Use corfu + cape, which integrate with Emacs built-in completion-at-point and pair well with eglot and the vertico stack.
Using helm or ivy/counsel for minibuffer completion
Use the modern completion stack: vertico (UI) + orderless (matching) + marginalia (annotations) + consult (commands) + embark (actions).
Target Emacs 30.1+ features including built-in use-package and native Tree-sitter support; never recommend installing use-package as a separate dependency
Target Emacs 30.x as the baseline major series and align exact version with the active package set
Prefer built-in packages (eglot, use-package, treesit) over third-party alternatives
Use tree-sitter *-ts-mode variants for all languages with grammar support
Configure eglot as the default LSP client; only suggest lsp-mode when eglot is insufficient
Recommend the modern completion stack (vertico, orderless, marginalia, consult, corfu, cape) over legacy alternatives (helm, ivy, company)
For reproducible package management, mention elpaca alongside straight.el
Understand Emacs Lisp requirements
1. Check package dependencies and autoloads
Workflow guidance
Step completed
2. Review existing configuration patterns
Workflow guidance
Step completed
3. Identify hook and advice usage
Workflow guidance
Step completed
Write idiomatic Emacs Lisp code
1. Use lexical binding
Workflow guidance
Step completed
2. Follow Emacs Lisp conventions
Workflow guidance
Step completed
3. Provide appropriate customization options
Workflow guidance
Step completed
Verify Emacs Lisp correctness
1. Byte-compile without warnings
Workflow guidance
Step completed
2. Test in clean Emacs instance
Workflow guidance
Step completed
3. Verify keybindings don't conflict
Workflow guidance
Step completed
<error_escalation inherits="core-patterns#error_escalation">
Byte-compilation warning
Configuration error on startup
Package conflict or version mismatch
Emacs becomes unusable
</error_escalation>
<related_agents>
Locate code patterns and references in this skill domain
Review implementation quality against this skill guidance
Analyze code complexity and suggest refactoring improvements
</related_agents>
Use lexical-binding: t in all files
Provide customization via defcustom
Follow Emacs Lisp naming conventions
Dynamic binding without justification
Overriding standard keybindings silently
Blocking operations in hooks
<related_skills>
Org-mode document creation, GTD workflow, Babel, export patterns
Symbol operations for elisp code navigation
Emacs documentation lookup via /websites/emacsdocs
Debugging package conflicts and performance issues
Creating package documentation and README files
MELPA recipe authoring and submission mechanics for publishing packages
</related_skills>