| name | emacs-lisp-dev |
| description | Write, review, and test Emacs Lisp code following idiomatic conventions and hard-won local best practices. Use when writing, editing, debugging, or reviewing .el files, Emacs packages, or gptel tools. Enforces a mandatory verify-before-deliver rule - all written code must be batch byte-compiled and smoke-tested before presenting to the user. Covers load-path management, buffer-local variable safety, HTML/XML parsing, file I/O patterns, and batch testing workflows. |
| license | Complete terms in LICENSE.txt |
Emacs Lisp Development
Prime Directive: Verify Before You Deliver
Every piece of Emacs Lisp code you write or edit MUST be tested before presenting it to the user.
After writing or modifying a .el file:
-
If emacs is unavailable, or required dependencies cannot be loaded in batch, tell the user clearly and treat verification as incomplete.
-
Syntax check (byte-compile as linter — do NOT leave .elc artifacts behind):
emacs --batch -L <deps> -L . --eval '(byte-compile-file "FILE.el")'
rm -f FILE.elc
This catches unbalanced parens, unbound variables, and missing requires.
-
Load check: emacs --batch -L <deps> -L . --eval '(require (quote FEATURE))'
-
Smoke test (when practical): emacs --batch -L <deps> --eval '(progn (require ...) (message "result: %s" (my-function "arg")))'
If any step fails, fix the code and re-test before responding. You have shell access—use it.
Load-Path Reality
Bare emacs --batch does NOT load the user's .emacs.d config or package paths. You must explicitly pass -L flags for every dependency directory. Ask the user for needed load-path extensions early in the session if they are not obvious from context.
Common pattern:
emacs --batch \
-L ~/projects/emacs/ai/gptel/ \
-L ~/emacs-gregoryg/ \
-f batch-byte-compile target.el
Tip: Use emacs --batch --eval '(message "%s" load-path)' to inspect defaults, then add missing paths.
Critical Safety Patterns
Buffer-Local Variable Capture (incl. "shadows global")
Buffer-local variables can surprise you in two ways:
- Scope switch: buffer-locals are tied to the current buffer. If you switch buffers with
with-current-buffer / with-temp-buffer, you are now reading the other buffer's local value (often nil). Capture them BEFORE switching:
;; WRONG - my-local-var is nil in temp buffer
(with-temp-buffer
(insert (format "%s" my-local-var)))
;; RIGHT - capture first
(let ((val my-local-var))
(with-temp-buffer
(insert (format "%s" val))))
- Shadowing: when a variable is buffer-local, the local value shadows the global value in that buffer. This means you do not get an automatic "merge" of global + local values unless you implement it.
If you want "effective" values that combine a global list with a buffer-local extension, do something like:
;; Example: combine a global allowlist with a buffer-local allowlist.
(let* ((global (default-value 'my-allowed-directories))
(local (and (local-variable-p 'my-allowed-directories)
my-allowed-directories)))
(seq-uniq (append local global) #'equal))
HTML/XML Parsing
Never use regex for HTML/XML. It is too fragile for namespaces (<html:title>), attributes, and nesting. Use:
libxml-parse-html-region + dom.el for HTML
libxml-parse-xml-region for XML
(let* ((dom (with-temp-buffer
(insert html-string)
(libxml-parse-html-region (point-min) (point-max))))
(title (dom-text (dom-by-tag dom 'title))))
title)
File I/O & Open Buffers
When writing auxiliary/sidecar files, always check find-buffer-visiting first. If the user has the file open with unsaved changes, modifying the buffer (and saving) is safer than blindly overwriting the file on disk:
(let ((buf (find-buffer-visiting filepath)))
(if buf
(with-current-buffer buf
(erase-buffer)
(insert new-content)
(save-buffer))
(with-temp-file filepath
(insert new-content))))
Style & Conventions
- Prefix all public symbols with the package name:
mypackage-do-thing, mypackage--internal-helper (double-dash for private).
- Docstrings: First line is a complete sentence. Mention argument names in CAPS.
defcustom for user-facing options; defvar for internal state.
- Prefer
when/unless over single-branch if.
- Prefer
pcase for destructuring and multi-branch matching.
- Autoloads: Add
;;;###autoload cookies to entry-point commands and major modes.
Testing Approach
- Prefer batch-mode probes over interactive runs.
- Capture
message output via *Messages* buffer reads when needed.
- Keep tests minimal and terminal-friendly; TTY behavior matters (e.g.,
cursor-sensor echoes).
- For gptel tools specifically: byte-compile the file, then
require it in batch to confirm clean loading.
- Use
ert for structured test suites when the project warrants it.
Generated Org artifacts
When Emacs Lisp creates or substantially rewrites Org files, parse a produced
artifact with vanilla Org as a separate smoke test:
emacs -Q --batch --eval \
'(progn (require '\''org)
(with-temp-buffer
(insert-file-contents "OUTPUT.org")
(org-mode)
(org-element-parse-buffer)))'
This proves that Org can read the structure; it does not prove Agenda semantics
or preservation behavior. Also assert important headings, properties, and tags,
rerun the generator to check idempotence, and test configuration-dependent Agenda
views in the user's normal Emacs when relevant.
Discovery & Exploration
- Avoid broad filesystem pokes inside Emacs batch; rely on shell tools (
rg, grep, find, sed) for discovery and then use targeted Elisp for processing.
- Set
load-prefer-newer t or delete stale .elc files to ensure fresh code is loaded during development.