emacs-lisp-dev

star 73

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.

gregoryg By gregoryg schedule Updated 3/29/2026

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:

  1. If emacs is unavailable, or required dependencies cannot be loaded in batch, tell the user clearly and treat verification as incomplete.

  2. 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   # clean up — stale .elc is a trap without (setq load-prefer-newer t)
    

    This catches unbalanced parens, unbound variables, and missing requires.

  3. Load check: emacs --batch -L <deps> -L . --eval '(require (quote FEATURE))'

  4. 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:

  1. 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))))
  1. 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.

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.
Install via CLI
npx skills add https://github.com/gregoryg/AIPIHKAL --skill emacs-lisp-dev
Repository Details
star Stars 73
call_split Forks 14
navigation Branch main
article Path SKILL.md
More from Creator