Supply-chain hygiene for Emacs: LLM review of package upgrades

fidelramos1 pts0 comments

blog.fidelramos.net<br>– Supply-chain hygiene for Emacs: LLM review of package upgrades

Fidel Ramos

Emacs has no sandbox. Every package you install runs arbitrary Lisp with your full<br>privileges: your files, your credentials, your network. This makes me really scared of<br>installing new packages, or upgrading existing ones.

The packages most of us use are not vetted artifacts: MELPA builds from some repository<br>(usually unsigned) commit, many packages are maintained by a single person, and nobody<br>audits what lands in a given update. A maintainer's account takeover, or a repository<br>sold to a new owner, turns M-x package-upgrade-all into remote code execution on your<br>machine. There is even a subtler vector unique to this ecosystem: recipe<br>redirection. A one-line change to a MELPA recipe can silently point an existing package<br>name at a different repository.

The answer isn't to stop upgrading, but to make upgrades deliberate : pin every package<br>to an exact commit and review the diffs before merging anything.

Manually reviewing diffs is not only cumbersome but needs a high level of expertise in<br>Emacs Lisp, so my solution is to have an LLM do a first-pass security audit of the<br>changes before they get to execute.

This post showcases the setup I use, built on<br>straight.el,<br>Magit and gptel.

Table of Contents<br>The setup at a glance

Step 1: verify the straight.el bootstrap

Step 2: pin everything

Step 3: review diffs before merging

Step 4: LLM-powered review of the diff

The upgrade workflow

Caveats and final thoughts

The setup at a glance

straight.el's own bootstrap script is verified against a pinned SHA-256 checksum<br>before it is evaluated.

Every package (and every recipe repository) is pinned to a commit in a lockfile under<br>version control.

Upgrades are fetch-first: git fetch to pull new incoming changes, git merge and<br>actually upgrade only what passes review.

A custom Magit view displays all incoming fetched-but-unmerged diffs across all<br>straight repos.

A custom gptel command reviews the full diff using an LLM.

If the changes are clean, have straight.el merge the changes and update the<br>lockfile.

gptel reviewing a straight.el packages upgrade

Step 1: verify the straight.el bootstrap

straight.el installs<br>itself by downloading<br>and evaluating install.el at startup. That's remote code at face value, so my<br>early-init.el pins its SHA-256 and stops on mismatch:

;; SHA-256 of straight.el's install.el; update after reviewing changes at<br>;; https://github.com/radian-software/straight.el/commits/develop/install.el<br>(defconst my-straight-install-el-sha256<br>"e29e07d52d16d4136971f0a822cb6a1a6e1e764a1cb9fe67cccbc7c048aba553")

(defvar bootstrap-version)<br>(let ((bootstrap-file<br>(expand-file-name<br>"straight/repos/straight.el/bootstrap.el"<br>(or (bound-and-true-p straight-base-dir)<br>user-emacs-directory)))<br>(bootstrap-version 7))<br>(unless (file-exists-p bootstrap-file)<br>(with-current-buffer<br>(url-retrieve-synchronously<br>"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"<br>'silent 'inhibit-cookies)<br>;; verify bootstrap script against its pinned checksum before eval<br>(require 'url-http)<br>(when url-http-end-of-headers<br>(delete-region (point-min) url-http-end-of-headers)<br>(delete-region (point-min)<br>(progn (skip-chars-forward " \t\n") (point))))<br>(let ((checksum (secure-hash 'sha256 (current-buffer))))<br>(unless (string= checksum my-straight-install-el-sha256)<br>(error "straight.el bootstrap checksum mismatch!")))<br>(goto-char (point-max))<br>(eval-print-last-sexp)))<br>(load bootstrap-file nil 'nomessage))

Copy

Step 2: pin everything

straight.el installs packages as git clones, which makes review natural. The lockfile pins<br>them all:

M-x straight-freeze-versions writes every package's exact commit to<br>straight/versions/default.el (packages and recipe repositories, MELPA<br>included).

Commit the lockfile. Reviewing git diff straight/versions/default.el after an<br>upgrade tells you exactly what moved.

M-x straight-thaw-versions restores those revisions if you ever need to roll back.

Step 3: review diffs before merging

The key to a safe upgrade is that fetching and merging are separate steps. M-x<br>straight-fetch-all downloads new upstream commits without changing any checkout. Then I<br>review what would change, per repo, before M-x straight-merge-all applies anything.

I then use a custom Magit function to display a single combined diff view of every repo<br>with incoming upstream commits:

;; https://magit.vc/<br>(use-package magit<br>:bind (("C-c v U" . my-straight-incoming-diffs))<br>:preface<br>(defun my-straight-repo-behind-p (repo)<br>"Return non-nil if REPO's current branch is behind its upstream."<br>(let* ((default-directory repo)<br>(lines (magit-git-lines "status" "--porcelain=2" "--branch")))<br>(seq-some (lambda (line)<br>(string-match-p "^# branch\\.ab [+][0-9]+ -[1-9]" line))<br>lines)))<br>(defun my-straight-incoming-diffs ()<br>"Concatenate the full patches of all incoming upstream changes.<br>Create a `diff-mode' buffer listing, for every straight.el...

straight bootstrap review package install diffs

Related Articles