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
emacsis 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
.elcartifacts behind):emacs --batch -L <deps> -L . --eval '(byte-compile-file "FILE.el")' rm -f FILE.elc # clean up — stale .elc is a trap without (setq load-prefer-newer t)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.elfor HTMLlibxml-parse-xml-regionfor 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.
defcustomfor user-facing options;defvarfor internal state.- Prefer
when/unlessover single-branchif. - Prefer
pcasefor destructuring and multi-branch matching. - Autoloads: Add
;;;###autoloadcookies to entry-point commands and major modes.
Testing Approach
- Prefer batch-mode probes over interactive runs.
- Capture
messageoutput via*Messages*buffer reads when needed. - Keep tests minimal and terminal-friendly; TTY behavior matters (e.g.,
cursor-sensorechoes). - For gptel tools specifically: byte-compile the file, then
requireit in batch to confirm clean loading. - Use
ertfor structured test suites when the project warrants it.
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 tor delete stale.elcfiles to ensure fresh code is loaded during development.