| name | EmacsLisp |
| description | Emacs Lisp development with modern tooling and best practices. USE WHEN writing Emacs configuration, developing packages, testing, linting, publishing to MELPA/ELPA, or working with Emacs Lisp code. |
Emacs Lisp Development
Expert guidance on Emacs Lisp programming following modern best practices (2025), from configuration and scripting to package development and publishing.
Purpose
Guide Emacs Lisp development using modern tooling and conventions:
- Configuration: use-package, straight.el, package.el best practices
- Package development: Structure, conventions, and tooling
- Testing: ERT (Emacs Lisp Regression Testing)
- Code quality: package-lint, checkdoc, byte-compilation
- Publishing: MELPA, GNU ELPA, NonGNU ELPA
- Modern tools: Eask, native compilation, package-vc
Context Detection
This skill activates when:
- Working with
.elfiles - Directory contains
Eask,Cask, or package headers - User mentions Emacs configuration, packages, or MELPA
- Commands like
emacs,eask,package-lintare mentioned - Working in
.emacs.d/or similar Emacs directories
Workflow Routing
When executing a workflow, output this notification directly:
Running the **WorkflowName** workflow from the **EmacsLisp** skill...
| Workflow | Trigger | File |
|---|---|---|
| Configure | "emacs configuration", "use-package", "init.el" | workflows/Configure.md |
| Package | "create emacs package", "package structure", "elisp package" | workflows/Package.md |
| Test | "emacs tests", "ert", "test elisp", "buttercup" | workflows/Test.md |
| Lint | "lint elisp", "package-lint", "checkdoc", "byte compile" | workflows/Lint.md |
| Publish | "publish to melpa", "elpa", "submit package" | workflows/Publish.md |
| Script | "elisp script", "batch mode", "standalone elisp" | workflows/Script.md |
| Debug | "debug elisp", "edebug", "elisp debugger" | workflows/Debug.md |
| Document | "elisp documentation", "docstrings", "info manual" | workflows/Document.md |
Core Principles
- Follow conventions: Package naming, headers, docstrings
- Use modern Emacs: Target Emacs 29+ for new packages
- Write tests: Use ERT for all packages
- Lint before publishing: package-lint, checkdoc, byte-compile
- Document thoroughly: Every function, variable, and customization
- Use lexical binding: Always use
;;; -*- lexical-binding: t -*- - Native compilation: Support native-comp for performance
Modern Emacs (2025)
Emacs 29+ Features
- use-package: Built-in package configuration
- package-vc: Install packages directly from version control
- Native compilation: Faster Emacs Lisp execution
- eglot: Built-in LSP client
- tree-sitter: Fast syntax parsing
Recommended Tooling
| Tool | Purpose | Status |
|---|---|---|
| Eask | Package development, testing, linting | Modern (2022+) |
| package-lint | Lint packages for MELPA | Essential |
| checkdoc | Check docstrings | Built-in |
| ERT | Testing framework | Built-in |
| buttercup | BDD testing (alternative) | Optional |
| use-package | Configuration management | Built-in (29+) |
Package Structure
Standard Package
my-package/
├── my-package.el # Main package file
├── my-package-test.el # Tests (ERT)
├── Eask # Package development tool
├── README.md # Description and usage
├── CHANGELOG.md # Version history
└── .github/
└── workflows/
└── test.yml # CI testing
Multi-file Package
my-package/
├── my-package.el # Main entry point with autoloads
├── my-package-core.el # Core functionality
├── my-package-ui.el # UI components
├── my-package-utils.el # Utilities
├── test/
│ ├── my-package-test.el
│ └── my-package-core-test.el
├── Eask
└── README.md
Package Headers
Every Emacs Lisp package must start with standard headers:
;;; my-package.el --- Brief description -*- lexical-binding: t -*-
;; Copyright (C) 2025 Your Name
;; Author: Your Name <your.email@example.com>
;; Version: 0.1.0
;; Package-Requires: ((emacs "29.1"))
;; Keywords: convenience, tools
;; URL: https://github.com/yourusername/my-package
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Longer description of what this package does.
;;
;; Usage:
;;
;; (require 'my-package)
;; (my-package-enable)
;;; Code:
(defgroup my-package nil
"Customization group for my-package."
:group 'tools
:prefix "my-package-")
;; Package code here...
(provide 'my-package)
;;; my-package.el ends here
Coding Conventions
Naming
;; Package prefix for all symbols
(defun my-package-do-something () ; Function
...)
(defvar my-package-mode-map ; Variable
(make-sparse-keymap))
(defcustom my-package-enable-feature nil ; Customizable
"Whether to enable feature."
:type 'boolean
:group 'my-package)
;; Private/internal (double dash)
(defun my-package--internal-helper ()
"Internal helper function."
...)
Docstrings
(defun my-package-process-file (file &optional verbose)
"Process FILE and return the result.
If VERBOSE is non-nil, print progress messages.
FILE should be a path to a readable file. Returns a list
of processed items, or nil if FILE cannot be processed.
Example usage:
(my-package-process-file \"/path/to/file.txt\" t)
See also `my-package-process-directory'."
...)
(defcustom my-package-timeout 30
"Timeout in seconds for operations.
This value controls how long to wait before giving up on
long-running operations. Set to 0 to disable timeout."
:type 'integer
:group 'my-package)
Lexical Binding
Always use lexical binding (modern Emacs standard):
;;; my-package.el --- Description -*- lexical-binding: t -*-
Benefits:
- Better performance
- Proper closures
- Catches more errors
Configuration Best Practices
use-package (Built-in since Emacs 29)
;; In init.el or early-init.el
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(package-initialize)
;; Configure packages with use-package
(use-package magit
:ensure t
:bind ("C-x g" . magit-status)
:config
(setq magit-display-buffer-function
#'magit-display-buffer-same-window-except-diff-v1))
(use-package org
:ensure t
:defer t
:custom
(org-directory "~/org")
(org-agenda-files '("~/org/agenda.org"))
:config
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(python . t))))
straight.el (Alternative package manager)
;; Bootstrap straight.el
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el"
user-emacs-directory))
(bootstrap-version 6))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
;; Use packages
(straight-use-package 'use-package)
(use-package magit
:straight t)
Testing with ERT
Basic Test
;;; my-package-test.el --- Tests for my-package -*- lexical-binding: t -*-
(require 'ert)
(require 'my-package)
(ert-deftest my-package-test-addition ()
"Test that addition works correctly."
(should (= (my-package-add 2 3) 5))
(should (= (my-package-add -1 1) 0))
(should (= (my-package-add 0 0) 0)))
(ert-deftest my-package-test-error ()
"Test that errors are raised correctly."
(should-error (my-package-divide 10 0)))
(ert-deftest my-package-test-buffer ()
"Test buffer operations."
(with-temp-buffer
(insert "test content")
(should (= (point-max) 13))
(goto-char (point-min))
(should (looking-at "test"))))
(provide 'my-package-test)
;;; my-package-test.el ends here
Run Tests
# Using Emacs batch mode
emacs -batch -l my-package.el -l my-package-test.el -f ert-run-tests-batch-and-exit
# Using Eask
eask test ert my-package-test.el
# In Emacs interactively
M-x ert RET t RET
Eask (Modern Development Tool)
Eask File
;; Eask
(package "my-package"
"0.1.0"
"Brief description of my package")
(website-url "https://github.com/yourusername/my-package")
(keywords "convenience" "tools")
(package-file "my-package.el")
(script "test" "echo \"Run tests..\" && eask ert-runner")
(script "lint" "echo \"Linting..\" && eask lint package")
(source "gnu")
(source "melpa")
(depends-on "emacs" "29.1")
(development
(depends-on "ert-runner")
(depends-on "package-lint"))
Eask Commands
# Install dependencies
eask install-deps
# Run tests
eask test
# Lint package
eask lint package
eask lint checkdoc
eask lint elisp-lint
# Byte compile
eask compile
# Package for distribution
eask package
# Clean build artifacts
eask clean all
Publishing to MELPA
Requirements
- Package structure: Follow conventions
- Tests: Include tests
- Documentation: Complete docstrings
- License: GPL-compatible license
- Git repository: Public Git repo (GitHub, GitLab, etc.)
MELPA Recipe
Create a recipe in MELPA repository:
;; In melpa/recipes/my-package
(my-package :fetcher github
:repo "yourusername/my-package"
:files ("*.el"))
Submission Process
Prepare package:
eask lint package eask lint checkdoc eask compile eask testFork MELPA: https://github.com/melpa/melpa
Add recipe: Create
recipes/my-packageTest locally:
make recipes/my-package make sandbox INSTALL=my-packageSubmit PR: Create pull request to MELPA
Wait for review: MELPA maintainers will review
Publishing to GNU ELPA
Requirements
- Copyright assignment: Assign copyright to FSF (for packages >300 lines)
- License: GPL-compatible
- No external dependencies: Prefer built-in Emacs features
Submission
- Email
emacs-devel@gnu.orgwith package proposal - Provide Git repository URL
- Complete copyright assignment if needed
- Maintainers will add to GNU ELPA
Code Quality Tools
package-lint
# Install
M-x package-install RET package-lint RET
# Run
M-x package-lint-current-buffer
# Or with Eask
eask lint package
checkdoc
# Interactive
M-x checkdoc
# Batch mode
emacs -batch -l checkdoc -f checkdoc-file my-package.el
Byte Compilation
# Interactive
M-x byte-compile-file
# Batch
emacs -batch -f batch-byte-compile my-package.el
# Eask
eask compile
Common Patterns
Minor Mode
(defvar my-package-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c m") #'my-package-command)
map)
"Keymap for `my-package-mode'.")
;;;###autoload
(define-minor-mode my-package-mode
"Toggle My Package mode.
When enabled, provides enhanced features for working with files."
:lighter " MyPkg"
:keymap my-package-mode-map
:group 'my-package
(if my-package-mode
(my-package--enable)
(my-package--disable)))
(defun my-package--enable ()
"Enable My Package mode."
(add-hook 'before-save-hook #'my-package--before-save nil t))
(defun my-package--disable ()
"Disable My Package mode."
(remove-hook 'before-save-hook #'my-package--before-save t))
Autoloads
;;;###autoload
(defun my-package-enable ()
"Enable my-package globally."
(interactive)
(my-package-mode 1))
Customization
(defgroup my-package nil
"Customization group for my-package."
:group 'tools
:prefix "my-package-")
(defcustom my-package-auto-save t
"Whether to automatically save files."
:type 'boolean
:safe #'booleanp
:group 'my-package)
(defcustom my-package-file-extensions '("txt" "md")
"List of file extensions to process."
:type '(repeat string)
:group 'my-package)
Performance
Lazy Loading
;; Defer loading until needed
(use-package my-package
:defer t
:commands (my-package-enable my-package-do-something))
;; Load on file type
(use-package my-package
:mode "\\.mypkg\\'")
;; Load on hook
(use-package my-package
:hook (prog-mode . my-package-mode))
Byte Compilation
Always byte-compile packages for distribution:
- Faster execution
- Catches some errors
- Smaller file size
Native Compilation
Support native compilation (Emacs 28+):
- Automatically compiles to native code
- Significantly faster execution
- No code changes needed
Best Practices
- Use lexical binding:
;;; -*- lexical-binding: t -*- - Follow naming conventions: Use package prefix
- Write complete docstrings: Every public function/variable
- Include tests: Use ERT for all functionality
- Lint before publishing: package-lint, checkdoc
- Use autoloads: Mark entry points with
;;;###autoload - Avoid global state: Use buffer-local variables
- Handle errors: Use
condition-caseappropriately - Check Emacs version: Use
(when (version< emacs-version "29.1") ...) - Keep dependencies minimal: Prefer built-in features
Resources
Official Documentation
Tools
- Eask - Modern package development
- package-lint - Lint for packages
- elisp-lint - Comprehensive linting
- buttercup - BDD testing
Style Guides
Examples
Example 1: Create a new package
User: "Create an Emacs package for managing bookmarks"
→ Invokes Package workflow
→ Creates package structure with Eask
→ Sets up tests and CI
→ Generates package headers and boilerplate
Example 2: Publish to MELPA
User: "How do I publish my package to MELPA?"
→ Invokes Publish workflow
→ Checks package quality with linters
→ Creates MELPA recipe
→ Guides through PR submission
Example 3: Configure Emacs
User: "Set up my Emacs with use-package"
→ Invokes Configure workflow
→ Shows modern use-package patterns
→ Demonstrates package installation
→ Explains performance optimization
Philosophy: Modern Emacs Lisp development emphasizes testing, documentation, and code quality. Use modern tooling (Eask, package-lint) and follow conventions to create packages that integrate seamlessly with the Emacs ecosystem.