Rahul Juliato: Taming Emacs Cache and Temporary Files
You open your favorite editor, and, out of the box, Emacs writes state
files all over the place. It puts bookmarks in ~/.emacs.d/, drops
#auto-saves# next to every file you edit, leaves .~lockfiles~ in
your projects, scribbles recentf, history, saveplace,
projects, transient/history.el, tramp, network-security.data,
multisession/, url/, image-dired/, erc/, rcirc/... you get
the picture. The features are great, don't get me wrong. The default
locations are what annoy me.
Most of those files are not noise. They are the state that makes Emacs feel like Emacs across restarts. So you cannot just delete them. But you also do not want them spread across N different directories with cryptic names. (I know I don't.)
I want one directory I control, every cache-bound variable pointing
inside it, and a switch to flip it between "persistent inside my
config" and "ephemeral in /tmp". No external package, no naming
scheme to memorize, just a small pattern I can read top to bottom in
init.el.
My emacs-solo config used
to hard-code a single relative path under user-emacs-directory for
all these cache files. It worked, but it was inflexible. A user
opened an issue asking if the cache location could be changed without
forking the config, since they wanted those files somewhere else
entirely. So I redesigned it around a defcustom root and an alist
of relative paths, switchable through M-x customize. The version
below is the same idea with the emacs-solo names dropped.
Our goal
Patch these three pieces:
→ cache directory: holds every piece of mutable state Emacs writes during a session.
→ alist map: lists every variable that needs to be pointed at that directory.
→ helpers: one resolve a key to an absolute path, and another pre-creates every directory so we never get "no such file or directory" warnings at startup.
In practice, a .emacs.d directory ends up looking like this:
.
├── cache
│ ├── auto-saves
│ ├── erc
│ ├── image-dired
│ ├── multisession
│ ├── rcirc
│ ├── transient
│ ├── url
│ └── ...
├── eln-cache
│ └── 32_0_50-25c5b284
├── eshell
│ └── history
├── init.el
└── tree-sitter
├── libtree-sitter-markdown-inline.dylib
└── libtree-sitter-markdown.dylibOur switchable base directory
Start with a defcustom for the root. Using defcustom (instead of
defvar) lets you change the value through M-x customize without
editing init files, and the choice persists in custom.el:
(defcustom my/cache-directory
(expand-file-name "cache/" user-emacs-directory)
"Base directory for Emacs cache files.
All entries in `my/cache-paths' are resolved relative to this
directory. Choose one of the presets or supply any custom path.
Changes take effect after restarting Emacs."
:type `(choice
(const :tag "Inside Emacs config (cache/ in user-emacs-directory)"
,(expand-file-name "cache/" user-emacs-directory))
(const :tag "System temp (/tmp/emacs-cache/)" "/tmp/emacs-cache/")
(directory :tag "Custom directory"))
:group 'my)
The default puts everything under ~/.emacs.d/cache/, which keeps
the rest of ~/.emacs.d/ clean and lets you back up or delete the
whole cache as one directory. The /tmp/ preset is for sessions that
should leave no trace. The directory choice lets you point at any
custom path.
I use user-emacs-directory instead of a literal ~/.emacs.d/
because Emacs 29 added the --init-directory flag, which lets you
launch Emacs against any config directory. I use it constantly to try
other people's configs side by side with my own without touching
~/.emacs.d/. Anchoring on user-emacs-directory means the cache
follows whichever config Emacs was started with, instead of every
alternate config writing into the default ~/.emacs.d/ and stepping
on each other.
In my early-init.el I already load custom-file, but I reload it
here too. customize writes its changes to custom.el immediately,
so if you change the cache directory mid-session through M-x customize and then restart, you want the new value to apply before
the rest of init.el runs and calls my/cache--path:
(when custom-file
(load custom-file 'noerror 'nomessage))
The when guard is there because in a brand-new config custom-file
is nil, and (load nil 'noerror 'nomessage) signals
(wrong-type-argument stringp nil) and aborts the rest of
init.el. The 'noerror flag suppresses file-error, not type
errors.
This whole reload is a bit of a hack. Loading custom-file twice
during startup just to get a defcustom value hot before the next
form is not what Emacs intends. It only matters the first time you
switch presets. Without it, you would wonder why your new
/tmp/emacs-cache/ is empty while your old ~/.emacs.d/cache/ keeps
receiving writes.
Our single alist as the source of truth
The next piece is the mapping from keys to relative paths.
How you organize this alist is your call. The list below is what I
wire through my/cache-path in my own config, but plenty of users
will split things differently. I deliberately keep tree-sitter/,
eln-cache/, and eshell/ outside this alist. tree-sitter and
eln-cache are populated by long-running, expensive processes
(grammar compilation, native compilation) that I want to survive when
I reset my cache, so they live next to the config under
user-emacs-directory directly. eshell carries my command history
and aliases, which I treat more like dotfiles than throwaway state.
Your workflow will pull these differently, and that is fine.
(defvar my/cache-paths
'(;; Files:
(bookmark-file . "bookmarks")
(ielm-history-file-name . "ielm-history.eld")
(project-list-file . "projects")
(recentf-save-file . "recentf")
(savehist-file . "history")
(save-place-file . "saveplace")
(transient-history-file . "transient/history.el")
(transient-levels-file . "transient/levels.el")
(transient-values-file . "transient/values.el")
(tramp-persistency-file-name . "tramp")
(nsm-settings-file . "network-security.data")
;; Directories:
(auto-saves . "auto-saves/")
(auto-saves-sessions . "auto-saves/sessions/")
(multisession-directory . "multisession/")
(url-configuration-directory . "url/")
(image-dired-dir . "image-dired/")
(erc-log-channels-directory . "erc/logs/")
(erc-image-cache-directory . "erc/images/")
(rcirc-log-directory . "rcirc/logs/"))
"Alist of (KEY . RELATIVE-PATH) for cache locations.
RELATIVE-PATH is resolved against `my/cache-directory'.
A trailing slash on RELATIVE-PATH marks the entry as a directory.")
The convention I follow: keys are usually the names of the Emacs
variables they will fill. That keeps grep useful for both the
definition and the usage.
A trailing slash on the value marks a directory (this matches
directory-name-p in Emacs). No trailing slash means a file. The
next helper uses that distinction.
Our helpers
The first helper turns a key into an absolute path:
(defun my/cache--path (key)
"Return the absolute path for KEY in `my/cache-paths'."
(let ((rel (cdr (assq key my/cache-paths))))
(unless rel
(error "my/cache--path: Unknown key %S" key))
(expand-file-name rel my/cache-directory)))
assq does an eq lookup on symbol keys. The unless rel branch is
there because typos in :custom blocks are silent otherwise: you
would get nil and Emacs would happily write to nil, which fails
in confusing ways. Better to error at config load.
The second helper pre-creates every directory the alist mentions, so we never get "directory does not exist" warnings from packages on first run:
(defun my/cache--ensure-dirs ()
"Create every directory referenced by `my/cache-paths'.
Entries ending in `/' are created directly; other entries have their
parent directory created."
(dolist (entry my/cache-paths)
(let* ((abs (my/cache--path (car entry)))
(dir (if (directory-name-p abs)
abs
(file-name-directory abs))))
(make-directory dir t))))
(my/cache--ensure-dirs)
If the entry is a directory (directory-name-p returns t for
values ending in /), it is created as-is. If the entry is a file,
only the file's parent directory is created.
The t argument to make-directory is the "parents" flag, the
equivalent of mkdir -p. So transient/history.el correctly creates
<cache>/transient/ even though we never list it explicitly.
Wiring it into use-package :custom
Now every cache-bound variable points at our helper. Inside a
use-package emacs block (or wherever you set built-ins):
(use-package emacs
:ensure nil
:custom
(bookmark-file (my/cache--path 'bookmark-file))
(ielm-history-file-name (my/cache--path 'ielm-history-file-name))
(project-list-file (my/cache--path 'project-list-file))
(recentf-save-file (my/cache--path 'recentf-save-file))
(savehist-file (my/cache--path 'savehist-file))
(save-place-file (my/cache--path 'save-place-file))
(transient-history-file (my/cache--path 'transient-history-file))
(transient-levels-file (my/cache--path 'transient-levels-file))
(transient-values-file (my/cache--path 'transient-values-file))
(nsm-settings-file (my/cache--path 'nsm-settings-file))
(multisession-directory (my/cache--path 'multisession-directory))
(url-configuration-directory (my/cache--path 'url-configuration-directory))
;; The two below are *not* backups; they keep auto-save state
;; without scattering `#file#' next to every edited buffer.
(create-lockfiles nil) ; no `.#file' lock files at all
(make-backup-files nil) ; no `file~' tilde backups at all
(auto-save-default t)) ; auto-save *is* kept, just redirected below
On the three settings at the bottom:
→ create-lockfiles nil and make-backup-files nil are personal
taste. Lockfiles warn another Emacs not to clobber your buffer, but
they break frontend tooling that watches directories for changes
(TypeScript, Vite, esbuild). The tilde backups I find redundant
with modern VCS and auto-save.
→ auto-save-default t stays on because auto-save is what
rescues you when Emacs crashes. We just want it somewhere else.
Redirecting auto-saves
Auto-save needs two extra settings because Emacs uses path transforms for it instead of single-value variables:
;; We want auto-save, but no #file# cluttering, so everything goes
;; under our cache/ tree. Directories are pre-created.
(setq auto-save-list-file-prefix (my/cache--path 'auto-saves-sessions)
auto-save-file-name-transforms
`((".*" ,(my/cache--path 'auto-saves) t)))
auto-save-list-file-prefix controls where the "list of files with
pending auto-saves" lives. This is what M-x recover-session reads.
Pointing it at our auto-saves/sessions/ directory means you can
still recover after a crash, without ~/.emacs.d/auto-save-list/
cluttering your config.
auto-save-file-name-transforms is a list of (REGEX REPLACEMENT UNIQUIFY) triples. The t at the end is the uniquify flag, which
encodes the original path into the filename so two files with the
same name in different directories do not collide in the auto-save
folder.
Both end up in <cache>/auto-saves/.
TRAMP, viper, and other late bindings
Some variables are not safely set in :custom, because they are
defined inside packages that load later. For those, use setopt
(which respects defcustom setters) inside the :config block:
(setopt tramp-persistency-file-name (my/cache--path 'tramp-persistency-file-name))
(setopt viper-custom-file-name (my/cache--path 'viper-custom-file-name))
setopt is the modern equivalent of setq for customizable
variables. It runs any :set function the variable defines, which
tramp-persistency-file-name does (it triggers a reload). Plain
setq would skip that and leave TRAMP confused.
What you get
→ ~/.emacs.d/cache/ (or wherever you point it) contains
everything. du -sh tells you how much state Emacs is hoarding.
rm -rf resets it all without touching your config.
→ M-x customize-variable RET my/cache-directory lets you flip
between persistent and ephemeral modes without editing init
files. Useful for screencasts where you want zero history showing,
or for testing whether a problem is config or state.
→ New packages are a two-line change. When you adopt, say,
newsticker, you add one entry to my/cache-paths and one
(my/cache--path 'newsticker-dir) line in the package's
:custom. The directory is auto-created on next restart.
→ You can grep for it. grep my/cache--path init.el lists every
place Emacs writes state, and the alist tells you what kind.
The complete code
Here is everything in one block. Copy this into some temporary folder
inside the init.el file. After that, cd into this temp directory
and run emacs --init-directory=./. You can them navigate files, use
Emacs features and check where the created files end up to.
(defcustom my/cache-directory
(expand-file-name "cache/" user-emacs-directory)
"Base directory for Emacs cache files."
:type `(choice
(const :tag "Inside Emacs config (cache/ in user-emacs-directory)"
,(expand-file-name "cache/" user-emacs-directory))
(const :tag "System temp (/tmp/emacs-cache/)" "/tmp/emacs-cache/")
(directory :tag "Custom directory"))
:group 'my)
(when custom-file
(load custom-file 'noerror 'nomessage))
(defvar my/cache-paths
'(;; Files:
(bookmark-file . "bookmarks")
(ielm-history-file-name . "ielm-history.eld")
(project-list-file . "projects")
(recentf-save-file . "recentf")
(savehist-file . "history")
(save-place-file . "saveplace")
(transient-history-file . "transient/history.el")
(transient-levels-file . "transient/levels.el")
(transient-values-file . "transient/values.el")
(tramp-persistency-file-name . "tramp")
(nsm-settings-file . "network-security.data")
;; Directories:
(auto-saves . "auto-saves/")
(auto-saves-sessions . "auto-saves/sessions/")
(multisession-directory . "multisession/")
(url-configuration-directory . "url/")
(image-dired-dir . "image-dired/")
(erc-log-channels-directory . "erc/logs/")
(erc-image-cache-directory . "erc/images/")
(rcirc-log-directory . "rcirc/logs/"))
"Alist of (KEY . RELATIVE-PATH) for cache locations.")
(defun my/cache--path (key)
"Return the absolute path for KEY in `my/cache-paths'."
(let ((rel (cdr (assq key my/cache-paths))))
(unless rel
(error "my/cache--path: Unknown key %S" key))
(expand-file-name rel my/cache-directory)))
(defun my/cache--ensure-dirs ()
"Create every directory referenced by `my/cache-paths'."
(dolist (entry my/cache-paths)
(let* ((abs (my/cache--path (car entry)))
(dir (if (directory-name-p abs)
abs
(file-name-directory abs))))
(make-directory dir t))))
(my/cache--ensure-dirs)
(use-package emacs
:ensure nil
:custom
(bookmark-file (my/cache--path 'bookmark-file))
(ielm-history-file-name (my/cache--path 'ielm-history-file-name))
(project-list-file (my/cache--path 'project-list-file))
(recentf-save-file (my/cache--path 'recentf-save-file))
(savehist-file (my/cache--path 'savehist-file))
(save-place-file (my/cache--path 'save-place-file))
(transient-history-file (my/cache--path 'transient-history-file))
(transient-levels-file (my/cache--path 'transient-levels-file))
(transient-values-file (my/cache--path 'transient-values-file))
(nsm-settings-file (my/cache--path 'nsm-settings-file))
(multisession-directory (my/cache--path 'multisession-directory))
(url-configuration-directory (my/cache--path 'url-configuration-directory))
(create-lockfiles nil)
(make-backup-files nil)
(auto-save-default t)
:config
(setq auto-save-list-file-prefix (my/cache--path 'auto-saves-sessions)
auto-save-file-name-transforms
`((".*" ,(my/cache--path 'auto-saves) t)))
(setopt tramp-persistency-file-name (my/cache--path 'tramp-persistency-file-name)))
Adapt the alist to the packages you actually use.
Other Resources
If you want to read more on the topic:
→ https://www.gnu.org/software/emacs/manual/html_node/emacs/Auto-Save-Files.html
→ https://www.gnu.org/software/emacs/manual/html_node/elisp/Variable-Definitions.html#index-defcustom
If you would rather install a package than maintain your own alist,
the no-littering
package is a popular ready-made alternative that covers a wide set of
variables out of the box.
The version of this code I actually run, alist entries and all, lives
in my emacs-solo config
under the CACHE PATHS heading of init.el. If you end up adapting
this for your own setup, I would love to hear which variables you
added that I forgot.
-1:-- Taming Emacs Cache and Temporary Files (Post Rahul Juliato)--L0--C0--2026-05-22T16:46:00.000Z
































