skills/emacs-lisp-dev/SKILL.md
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.
npx skillsauth add gregoryg/aipihkal emacs-lisp-devInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 # 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.
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.
Buffer-local variables can surprise you in two ways:
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))))
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))
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 HTMLlibxml-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)
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))))
mypackage-do-thing, mypackage--internal-helper (double-dash for private).defcustom for user-facing options; defvar for internal state.when/unless over single-branch if.pcase for destructuring and multi-branch matching.;;;###autoload cookies to entry-point commands and major modes.message output via *Messages* buffer reads when needed.cursor-sensor echoes).require it in batch to confirm clean loading.ert for structured test suites when the project warrants it.rg, grep, find, sed) for discovery and then use targeted Elisp for processing.load-prefer-newer t or delete stale .elc files to ensure fresh code is loaded during development.tools
Write, review, and test Python code following PEP 8, type hints, and modern best practices (Python 3.12+). Use when writing, editing, debugging, or reviewing Python files. Enforces a mandatory verify-before-deliver rule - all written code must be tested for syntax and functionality using available shell tools before presenting to the user. Covers naming conventions, project layout, docstrings, formatting, and test authoring.
development
Write correct, idiomatic Org mode (vanilla) including denote-style optional frontmatter, agenda-aware tasks (TODO/STARTED/PAUSED/DONE, SCHEDULED/DEADLINE, repeaters, tags), and interactive executable Org Babel blocks (emacs-lisp, python, bash, sql) that produce in-buffer results. Use when asked to draft or transform .org files, planning/task documents, executable notes, or when Org syntax/agenda/Babel interactivity is needed instead of Markdown.
tools
Control and query Home Assistant via the hass-cli Python CLI. Use when Codex needs to interact with a Home Assistant instance for controlling devices (lights, switches, climate, covers, etc.), querying entity states or attributes, managing areas/devices/entities, calling services, checking system status, or any Home Assistant automation/control tasks. Supports JSON output for structured data.
testing
invoke multiple LLMs to gather different perspectives, review each other's outputs, and act as a council under the directorship of you the Council Head. Or more generally, call other LLM instances as agents to provide summaries without cluttering context.