Managing Skyrim Mods on Ubuntu with ZFS
If everything is a file, then snapshots are a time machine
2026-09-24 8d1da33As hinted at in my last post, I don't run Windows as an operating system on my home network. I do however sometimes like to modify games, sometimes I even end up playing them (to check that the mods worked). In fact that's kind of how I got into programming in the first place.
When it comes to video games that can be modded Skyrim is the Arch Linux of the gaming world, it can get so heavily modded that there are entire tools dedicated to managing which mods are loaded and in which order. The game itself is designed to run on Windows, however through the magic of Valve's Proton project, running it from Steam on Linux has become as simple as if it were native. That said most of the tooling that was built for mods is built for Windows, so if you really wanted to start modding the game on a Linux based operating system, you would need to get those working too. I guess it would be possible through Wine or somehow set up Steam to launch them through Proton, but I have played this game long enough to remember a time before mod managers and the reason they are used in the first place.
The reason for mod managers
The mechanism behind how mods in this game work is actually quite simple; the game has a Data/
folder, which contains assets like scripts, textures and sounds and there are also plugin files (.esp, .esm and .esl) that
the engine knows what to do with, those plugins are then loaded based on the configuration in a file called
Plugins.txt, so it's more than possible to just throw mods in the folder, edit the Plugins.txt and call it
a day if you only have a small set of mods.
In practice however things can fall apart quickly if you want to unload or uninstall a mod, because over
time you have merges of those texture and mesh folders in Data/ that are hard to undo if you didn't track
every single file that went in. This is where the mod managers come in, they download the mods to their
own directory and shovel the files back and forth as you enable and disable them in the interface.
This use case of uninstalling is more frequent than you might think, because a lot of the time you throw
a mod in and it breaks the game.
So, you have a file tree that you want to be able to roll back if something goes wrong. Hmm, that sounds familiar, if only someone made git for filesystems...
ZFS and datasets
Now, another thing I have mentioned in a past post is my newfound obsession with ZFS, which means that the secondary drive in this machine is actually in a zpool.
Now ZFS has a lot of nifty features, one of them is snapshots. In fact I have a tool called
sanoid set up which takes snapshots automatically of my zpool
in /tank.
We do not want daily, monthly and weekly snapshots of the game however, and we also do not want to roll back the entire drive just because we broke something in something as unimportant as a video game.
So enter datasets, which is a ZFS feature that allows us to manage a subdirectory separately from the main snapshot strategy, in our case explicitly snapshotting every time we load a mod and it works correctly.
Initially I just created a simple game directory dataset, I thought we didn't need to get more granular since Skyrim would be the only game I would install to this path, so rolling back the whole thing wouldn't be a problem (foreshadowing).
So I created:
I then did a clean install of the game by setting up tank/games as a target in Steam.
Which then puts the game at /tank/games/steamapps/common/Skyrim Special Edition.
Turning sanoid off
Because the [tank] block in /etc/sanoid/sanoid.conf is recursive = yes, the new dataset would
have inherited the hourly snapshot schedule. That's not what I want here: a snapshot every hour of
a many-GB game folder is noise, and it tells me nothing about what I changed. I want snapshots
to work like git commits, taken by hand when I've actually done something significant. So:
[tank/games]
autosnap = no
autoprune = no
When you do that, you can check that sanoid picked it up with a dry run:
# DEBUG: overriding autosnap on tank/games with value directly set in module.
# DEBUG: overriding autoprune on tank/games with value directly set in module.
Snapshots as commits
And with that we can now make a snapshot of the game in its fresh install state. Do make sure to run it for the first time before doing the snapshot though, as it does a bunch of setup on first run. Once that is all set:
ZFS lets you set your own properties on a snapshot, which means you can attach a proper commit message:
And to view the list of snapshots in this dataset:
We also get a feature that in this case functions like git diff would, with zfs diff, which tells you
which files were added (+), removed (-), modified (M) or renamed (R) since a snapshot.
You don't get line diffs, but they're binary game files anyway. What you actually want to know is
which plugins, textures etc. a mod touched, and that's exactly what you get.
The first day went something like this:
tank/games@2026-09-13-skyrim-installed
tank/games@2026-09-13-skyrim-installed-anniversary-edition
tank/games@2026-09-13-skyrim-added-git-for-text
tank/games@2026-09-13-skyrim-installed-skse
tank/games@2026-09-13-skyrim-installed-skyui
The script extender, the Linux way
A lot of mods need SKSE (the Skyrim Script Extender), and on Windows your mod manager
launches the game through it. On Linux, Steam always runs SkyrimSELauncher.exe for the game,
and there's no mod manager to hook into. So the trick is to swap the launcher out:
Steam launches what it thinks is the real Skyrim launcher, which is actually the SKSE loader, which then starts
SkyrimSE.exe with the script extender loaded.
There are two caveats though:
- The SKSE build has to match the exact game version.
- The "Verify integrity of game files" feature in Steam, or a game update, can quietly put the original launcher back.
With ZFS though, if your snapshots are up to date, you can very easily check if SKSE has been broken by running the diff command.
The day it broke
On that one faithful day; I installed a Breezehome basement mod, and then a few more mods, and SKSE started complaining. No problem, that's what snapshots are for:
It worked, but a zfs diff afterwards made me realise that setting up snapshots for the whole
/tank/games directory was a mistake. The dataset isn't just the game, it's also:
- the Proton prefix (
compatdata/489830), which holds my saves, Plugins.txt and the INIs - the shader cache
And straight after rolling back Steam tries to bring back all the saves from the cloud which means you don't have a clean diff after a rollback making debugging a... let's say "non-feature".
Splitting the dataset
The fix was to give the install folder its own child dataset, and here there was something else to be learned; You can't turn an existing folder into a dataset in place, so the data has to be copied into a new one.
With Steam closed:
# verify: prints nothing if the copies are identical
One more gotcha: the [tank/games] override in sanoid.conf doesn't carry over to child datasets. Since
[tank] is recursive = yes, the new dataset would pick up the hourly schedule again, so it needs
its own block too:
[tank/games/skyrim-se]
autosnap = no
autoprune = no
So at this point, this is what the setup looks like:
| Dataset | Contains | Roll back to undo |
|---|---|---|
tank/games/skyrim-se |
the game, Data/, mods, SKSE |
mod installs and removals |
tank/games |
Proton prefix (saves, Plugins.txt, INIs), shader cache | (rarely, it destroys saves) |
The trade-off: Plugins.txt lives in the prefix, so rolling back mods doesn't roll back the load order. After a rollback I check Plugins.txt and remove anything that's enabled but no longer on disk.
Making the commands more straightforward and fit for purpose
The full ZFS commands get tedious, so I set up a Makefile in the game folder:
And the history now reads like a changelog:
tank/games/skyrim-se@2026-09-19-split-baseline
tank/games/skyrim-se@2026-09-19-skyrim-fixed-breezehome
tank/games/skyrim-se@2026-09-19-skyrim-added-scripts
tank/games/skyrim-se@2026-09-20-skyrim-well-stocked-alchemists
tank/games/skyrim-se@2026-09-20-skyrim-fashions-of-the-huntsmen
tank/games/skyrim-se@2026-09-20-skyrim-traveler-outfits
tank/games/skyrim-se@2026-09-20-skyrim-toughened-traveler-outfits
Conclusion
So the process installing a mod looks something like this:
# copy the mod's files into Data/
# add *SomeMod.esp to Plugins.txt
# play (test the mod, obviously)
When you strip it right down, a mod manager does two jobs: it copies files into a folder and it edits a text file. The part people actually rely on it for is undo, and that turns out to be the part a filesystem can do in a way that is actually more efficient. A ZFS snapshot costs next to nothing, takes a second, and covers every file a mod touched, whether that's a plugin, a texture or a script, without needing to know anything about Skyrim.
It isn't perfect. Load order lives outside the snapshot, so I still have to check it after a rollback, and nothing tidies up old snapshots unless I do it myself. But for the price of one extra dataset and a small Makefile, I get proper history for my install without running a single Windows mod manager.
If everything is a file, then snapshots are a time machine. Well, either that or we have just sent the world eater forward in time.