Threat model for the Emacs configuration in this repo (early-init.el,
init.el).
This is a living document, not a point-in-time audit. It records the deployment, the assets, the adversaries in scope, and — explicitly — that the current threats are acceptable; no action is required at this time. It exists so that the posture is written down: anyone reading this knows what is deliberately accepted, and what change in deployment would invalidate that acceptance and force a re-evaluation.
Methodology informed by the 10-phase checklist from JayDoolan/security-audit and irfad7/claude-power-skills → security-review, but the shape here differs from a server audit: this is a single-user local program that executes code on the user's behalf, not a network service. The relevant questions are supply chain, untrusted-content execution, secrets hygiene, and the one network surface (SSH-from-iPad into the laptop's emacs daemon).
All threats identified below are accepted. No action is required. Each threat in the model is paired with the reason it is acceptable under this deployment. The only thing that would change this conclusion is a change in deployment — specifically the tripwires in the last section.
Agreed 2026-08-01. A threat is only listed if an in-scope adversary can trigger it; every listed threat is then graded against the deployment and either accepted or noted as a tripwire.
Personal Emacs configuration, single laptop, single OS user. The config is
bootstrapped via el-get (early-init.el:60-65) and uses use-package for
the rest. Package sources: GNU ELPA, MELPA Stable (preferred), MELPA
(fallback), plus ~30 el-get-bundle declarations pinned to specific commits
(early-init.el:99-209, 23 :checkout pins + 6 :url pins).
Network surface — the one that matters: Emacs runs as a daemon and is
driven over SSH from an iPad with a keyboard (server-after-make-frame-hook,
init.el:2197-2206). The threat here is the SSH channel, not Emacs itself:
if the laptop accepts SSH from anything beyond the user's own iPad on a
trusted network, anyone who reaches that SSH port effectively gets the user's
OS account. Emacs adds nothing on top of that — it is the SSH daemon's policy
that is the boundary. There is no Emacs TCP server
(make-network-process, (server-start t), etc.) — emacsclient reaches the
daemon over a local unix socket only.
Other network egress, all initiated by the user's own Emacs on the user's own behalf:
- Package archives (
early-init.el:43-51) — HTTPS toelpa.gnu.org,stable.melpa.org,melpa.org.package-check-signatureis left at its default (allow-signed), not forced tonil. - el-get bootstrap (
early-init.el:60-65) — pullsel-get-install.elfromraw.githubusercontent.comover HTTPS on first run only. - ERC (IRC client), Slack (
slackpackage), gptel (LLM API) — all configured, all credentialed from~/.secrets.el.gpg. - TRAMP — used regularly to edit files on remote hosts over SSH.
The repo is public: github.com/zed/dotemacs.d, "private": false.
Everything committed here is world-readable; this is the single fact that
elevates secrets/PII hygiene into the model.
~/.secrets.el.gpg— GPG-encrypted, loaded at startup (init.el:13). Holds IRC credentials (.secrets-irc-start,init.el:1082), Slack team registration (.secrets-slack-register-team,init.el:1764), andgptelmodel/backend (init.el:1905). Highest confidentiality value. Not in the repo; lives at~/outside this tree. Its contents are out of scope (the doc cannot read them), but the loading path is in scope.- The user's OS account / code-execution. Emacs is a code-execution
engine:
org-babel, dir-localseval,auto-mode-alist, magic-mode, hooks, advice, andcall-process/shell-commandall run arbitrary code as the user. RCE-as-the-user is the largest blast radius and the implicit default outcome of the threats below. - Integrity of the user's repos and committed work. Emacs is where the user edits and commits code; tampering via a malicious dir-local / hook / package can poison commits, signing keys, or pushes.
- What is in this public repo.
AGENTS.mddiscloses the deploy topology (daemon + iPad-over-SSH), the secret-loading path, and the existence of~/.secrets.el.gpg. Not a credential, but a reconnaissance aid — accepted, the topology is not secret.
Three, graded against the deployment above:
- A malicious file the user opens — an org/repo/file fetched or cloned
and opened in Emacs. Vectors: an org src block executed by hand
(
org-confirm-babel-evaluateisnil,init.el:1335), a dir-localseval, asafe-local-variable-valuesentry that runs code (init.el:2144-2161),auto-mode-alist/magic-mode dispatch to a hostile mode, or anorg-open-at-point/link to afile:/elisp:URL. - A reader of the public repo. Anyone on the internet reading
github.com/zed/dotemacs.d. Cannot execute anything, but can read everything committed and use it for reconnaissance against the SSH-reachable laptop. - A malicious IRC peer (residual, after the ERC
notify-sendpath was hardened). Any IRC peer can send a line that mentions the user's nick and reacheserc-global-notify(init.el:1108-1126); today that path invokesnotify-sendviacall-processwith separate argv strings, so the message body cannot break out into a shell. The residual threat is a future change to that function reintroducing interpolation — not a live exploit.
The current threats, with the reason each is accepted under this deployment:
| Threat | Adversary | Trigger / code path | Why acceptable |
|---|---|---|---|
Code-exec from an opened file (org src block, dir-locals eval, trusted safe-local-variable-values eval, magic-mode dispatch, file:/elisp: link) |
#1 malicious file | org-confirm-babel-evaluate nil (init.el:1335); safe-local-variable-values trusts an eval form (init.el:2144-2161); enable-local-variables :safe (init.el:2044) |
The user has decided that opening a file and evaluating a block / answering "yes" to a prompt is their own choice. The promptless eval paths are bounded by a benign current whitelist; the mechanism is the latent footgun, accepted. |
| Malicious or compromised package (MELPA / GNU ELPA / pinned el-get source) | supply chain | early-init.el:43-51 (archives); early-init.el:99-209 (bundles) |
Supply-chain trust is accepted by decision. Mitigant: every el-get-bundle is pinned to a fixed commit (23 :checkout + 6 :url), so "what runs" is reproducible; package-check-signature stays at its default. |
| Public-repo reconnaissance / PII leak | #2 repo reader | the repo is private: false; AGENTS.md discloses topology and the secret-loading path |
The topology is not secret. Verified scan finds no credential/env/key/token committed: ~/.secrets.el.gpg and ~/.custom.el both live at ~/, outside the repo tree (early-init.el:54), so a git add . in the repo cannot reach them. The recon surface is accepted. |
ERC notify-send re-regression |
#3 IRC peer | erc-global-notify (init.el:1108-1126) |
Currently hardened (call-process, no shell, separate argv). Not a live exploit; accepted as a "don't reintroduce string interpolation here" note for future edits. |
| TRAMP autosave to an "unsafe" temp dir | local (out of scope) | tramp-allow-unsafe-temporary-files t (init.el:2043) |
Single-user trusted machine — no other OS user to race or snoop. The variable name is alarming but the impact needs a multi-user host, which is out of scope. |
- Opening random org/repo files = trusted. Dir-locals,
safe-local-eval, and answering "yes" to local-variable prompts are the user's own choice. - Package supply chain = trusted. MELPA / GNU ELPA / the pinned el-get sources are trusted as-is.
- Local single-user = trusted environment. No other OS users, no tmp-file race hardening, no local-privilege boundary assumed.
- The user's own OS account on the laptop, if compromised. Game over regardless; out of scope.
- Availability / config breakage. Not a security concern; not graded.
| Boundary | What crosses it | Protection |
|---|---|---|
| iPad → laptop SSH → emacs daemon | keystrokes / emacsclient over the SSH channel, then a local unix socket |
SSH policy (not Emacs); Emacs adds no TCP server |
| Emacs → package archives | HTTPS package + recipe downloads | TLS; package-check-signature at default (allowed) |
Emacs → ~/.secrets.el.gpg |
GPG-decrypt at startup (init.el:13) |
GPG; loopback pinentry in TUI (init.el:9-12) |
IRC server → ERC → erc-global-notify |
an IRC msg reaches call-process |
passed as separate argv, no shell (init.el:1108-1126) |
| Opened file → Emacs | dir-locals, safe-local-variable-values, org src blocks, auto-mode |
prompt-by-default for non-whitelisted; whitelisted eval is accepted |
| Public GitHub repo → anyone | every committed file | none — it is public (private: false); no credentials are in tree |
No authentication or authorization inside Emacs — it is a single-user local program with no network service to authenticate. The only security boundary is "you can reach the laptop's SSH port" + "you have the user's GPG passphrase". Everything that would be HIGH under an internet-exposed-Emacs model is either impossible (no TCP server) or accepted (the user trusts what they open).
The acceptance above holds only under the current deployment. Re-evaluate this document (and treat the previously-accepted threats as live) if any of these change:
- The laptop's SSH port becomes reachable beyond the user's own iPad on a trusted network — port-forward, public IP, a cloud tunnel not scoped to the user's identity. Then the daemon effectively becomes a remote code-exec surface as the user; the boundary is the SSH daemon's policy, not Emacs, but the acceptance of "trusted environment, single user" flips. Revise before exposure, not after.
- Emacs gains a TCP server —
(server-start t),server-use-tcp, a package that opens a port, an LSP/eglot server exposed outward. There is no Emacs-level auth model today by design; a TCP listener would need one. - The repo gains a real in-tree secret — a
.env, a scratch secret, or an in-treecustom.el. Today~/.custom.eland~/.secrets.el.gpglive at~/, outside the repo, so there is nothing to protect; that geometry must be preserved (or a.gitignoreadded) if it ever changes. - The whitelisted
evalinsafe-local-variable-valuesgrows beyond its current benign form, orenable-local-variablesis widened from:safeto:all. Either re-opens promptless code-exec on file-open. erc-global-notifyis rewritten to interpolatenick/msginto a shell string. The currentcall-processform must be preserved.