modde
modde is a game mod manager written in Rust that runs natively on Linux, macOS, and Windows. It gives you declarative, reproducible mod management with a virtual-filesystem deployment that keeps your game directory clean, git-backed save vaults, profile experiments, and graph-based conflict detection — and it installs Wabbajack modlists and Nexus Collections natively, no Windows VM required.
Every platform is a first-class target. Install modde with your system’s package manager, a direct download, or Cargo — and if you happen to use Nix, modde is also a flake, which gives you a reproducible install and the option to declare your mod profiles as code through a home-manager module.
- Project site: https://modde.tartanoglu.com/
- Source: https://codeberg.org/caniko/rs-modde
What modde does
- Virtual filesystem deployment — a symlink farm overlays mods without touching the original game files, with atomic rollback. See Deployment & VFS.
- Wabbajack & Nexus — install
.wabbajackmodlists and Nexus Collections, browse and search Nexus, and resolve downloads from many backends. See Wabbajack modlists and Nexus Mods. - Profiles & experiments — per-game mod sets with a non-destructive, git-like experiment stack. See Profiles.
- Save vaults — git-backed save snapshots with SHA-256 fingerprinting and pre-restore compatibility warnings. See Save management.
- Conflict detection — graph-based collision analysis with severity classification. See Conflicts & load order.
- Tools & executables — MangoHud, vkBasalt, GameMode, ReShade, OptiScaler, and Proton, plus named external executables with overwrite capture. See Tools & overlays and Executables.
Where to start
- New here? Read Installation, then Quick start and the end-to-end Your first profile walkthrough.
- Want to know if your game is supported? See Supported games — modde ships 15 titles across seven engine families, plus user-defined games.
- Curious how it compares to Mod Organizer 2? See the MO2 parity & capability audit.
- Looking for a command or option? See the CLI reference, the Home-Manager module, the settings file, and the Architecture overview.
Status claims in this documentation use a deliberately conservative vocabulary —
Done, Partial, and Not shipped — anchored to the canonical
docs/capability-matrix.toml in the repository. The FAQ answers the
most common questions.
Installation
modde runs on Linux, macOS, and Windows — all first-class. Every release ships
two binaries: the modde command-line tool and the modde-ui desktop app. Install
them through your platform’s native package manager, a direct download, Cargo, or —
if you use Nix — a flake with a declarative home-manager module.
There is no single “blessed” method. Pick whatever fits how you already manage software:
| Platform | Native packages | Also available |
|---|---|---|
| Linux | AUR · COPR · apt · Flatpak | AppImage · tarball · Cargo · Nix |
| macOS | Homebrew | tarball · Cargo · Nix |
| Windows | winget · Scoop · Chocolatey | zip · Cargo |
Both binaries are included in every package except cargo install modde-cli, which
builds the CLI only. After installing, jump to the Quick start.
Linux
Arch Linux (AUR)
Three packages are published to the AUR; install one with your preferred helper
(yay, paru, …):
yay -S modde-bin # prebuilt from the signed release tarball (fastest)
yay -S modde # build the tagged release from source
yay -S modde-git # track the development branch
modde-bin depends on glibc >= 2.38 (satisfied by current Arch systems). All three
provide the modde and modde-ui binaries and conflict with one another. Release
tags are signed by the maintainer GPG key
818D507F1E62139F8A17EAA64623DEA06FDACFE1, also exported in
keys/maintainers.gpg.
Fedora / RHEL (COPR)
sudo dnf copr enable caniko/rs-modde
sudo dnf install modde modde-ui
The COPR builds the RPMs from the signed release source. Prerelease builds are
published to the separate caniko/rs-modde-testing project.
Debian / Ubuntu (apt)
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://modde.rs/apt/key.gpg.asc | sudo gpg --dearmor -o /etc/apt/keyrings/modde.gpg
echo "deb [signed-by=/etc/apt/keyrings/modde.gpg] https://modde.rs/apt/ stable main" \
| sudo tee /etc/apt/sources.list.d/modde.list
sudo apt update
sudo apt install modde modde-ui
The repository is signed with a dedicated key (fingerprint
CCFE4A8461DF8778F5227684B6DB8F177A951E1B), separate from the maintainer
tag-signing key and the minisign release key. See
SECURITY.md
for the signing-key policy and rotation procedure.
Flatpak
The desktop app is published to Flathub:
flatpak install flathub com.tartanoglu.modde
flatpak run com.tartanoglu.modde
The Flatpak ships modde-ui (the GUI). For scripting with the modde CLI, use one
of the other channels.
AppImage
Self-contained, no installation required:
chmod +x modde-ui-<version>-x86_64.AppImage
./modde-ui-<version>-x86_64.AppImage
A CLI AppImage (modde-<version>-x86_64.AppImage) is published alongside the GUI
one. Download both from the
releases page.
Linux direct download
Grab the tarball for your architecture from the releases page and extract it:
tar xzf modde-<version>-x86_64-linux.tar.gz # or aarch64-linux
./modde --help
Verify the download first — see Verifying releases.
macOS
Homebrew
brew tap caniko/modde https://codeberg.org/caniko/homebrew-modde
brew install modde
The formula installs both modde and modde-ui on Apple Silicon and Intel Macs
(it also works on Linux/Linuxbrew).
macOS direct download
modde ships ad-hoc-signed macOS binaries (no Apple Developer ID, no notarization). macOS quarantines downloaded binaries, so clear the quarantine attribute once after extracting:
tar xzf modde-<version>-aarch64-darwin.tar.gz # or x86_64-darwin on Intel
xattr -dr com.apple.quarantine modde modde-ui
./modde --help
Subsequent runs work without further intervention. If you would prefer notarized binaries (Apple Developer ID, $99/yr), open an issue to fund or contribute it.
Windows
winget
winget install Caniko.Modde
Scoop
scoop bucket add modde https://codeberg.org/caniko/scoop-modde
scoop install modde
Chocolatey
choco install modde
Each Windows package installs modde.exe and modde-ui.exe on your PATH.
Windows direct download
Download modde-<version>-x86_64-windows.zip from the
releases page and extract it. The
.exe artifacts are Authenticode-signed; verify the signature before running:
Get-AuthenticodeSignature .\modde.exe
Get-AuthenticodeSignature .\modde-ui.exe
Both should report Status : Valid. On Linux you can verify the same files with
osslsigncode verify -in modde.exe.
Cargo
modde publishes its CLI crate to crates.io. This builds the modde binary from
source (the GUI lives in a separate crate that is not published to crates.io):
cargo install modde-cli
Because it compiles locally, you need a build environment:
- A Rust 2024 edition toolchain (recent stable
rustc/cargo). - SQLite and OpenSSL development headers (
openssl-syswill not build without OpenSSL — see Troubleshooting).
On Debian/Ubuntu, for example:
sudo apt install ca-certificates gcc pkg-config \
libssl-dev libsqlite3-dev libdbus-1-dev \
libwayland-dev libxkbcommon-dev libvulkan-dev
On Fedora: sudo dnf install gcc pkg-config openssl-devel sqlite-devel dbus-devel wayland-devel libxkbcommon-devel vulkan-loader-devel.
Build from source
git clone https://codeberg.org/caniko/rs-modde.git
cd rs-modde
nix develop . -c cargo build --release
# Binaries at target/release/modde and target/release/modde-ui
The Nix dev shell provides every system library and is the authoritative build and test environment:
nix develop . -c cargo test --workspace
A plain cargo build/cargo test outside the dev shell often fails in
openssl-sys (and the GUI fails to link without the Wayland/libxkbcommon/Vulkan
libraries). Either use the dev shell or install the headers listed under
Cargo. See Troubleshooting.
Nix
If you use Nix, modde is also a flake. This is not required and not “the” way to install it — but it is a reproducible option, and through the home-manager module it lets you declare your mod profiles as code.
Run or install
# Run without installing
nix run codeberg:caniko/rs-modde
# Install into your profile (both modde and modde-ui)
nix profile install codeberg:caniko/rs-modde
The Nix build wraps each binary with a CA-certificate bundle, so HTTPS downloads from Nexus and friends work with no extra configuration.
Flake input + home-manager module
Add modde to your flake inputs:
{
inputs.modde = {
url = "codeberg:caniko/rs-modde";
inputs.nixpkgs.follows = "nixpkgs";
};
}
Then import the home-manager module and declare profiles — Wabbajack lists, Nexus Collections, and tool overlays — that deploy on activation:
{ inputs, ... }:
{
imports = [ inputs.modde.homeManagerModules.modde ];
programs.modde = {
enable = true;
profiles.lotf = {
game = "skyrim-se";
installMode = "await-game"; # wait until the game is installed
wabbajackList = {
url = "https://example.com/lotf.wabbajack";
hash = "sha256-...";
};
};
};
}
modde waits for launcher-managed game installs; it does not install the base game itself. For every option, see the Home-Manager module reference. You can also drop the package straight into a system or user closure:
environment.systemPackages = [ inputs.modde.packages.x86_64-linux.modde ];
Development shell
nix develop codeberg:caniko/rs-modde
# ...or, in a checkout:
nix develop
The shell carries the pinned Rust 2024 toolchain, coverage tooling, archive
extractors, the docs tooling, the simit release CLI, and every system library the
workspace links against.
Verifying releases
Every release is built from a GPG-signed Git tag, and every artifact ships with a signed checksum manifest. Tarballs, AppImages, and source RPMs additionally ship Sigstore bundles and SLSA provenance.
Step 1 — verify the signed checksum manifest
Download the artifact plus SHA256SUMS.txt and SHA256SUMS.txt.minisig from the
same release, then:
minisign -Vm SHA256SUMS.txt -p keys/minisign.pub
sha256sum -c SHA256SUMS.txt --ignore-missing
The minisign public key is pinned at keys/minisign.pub. If it ever changes, treat
the release as a key-rotation event and verify the new key from an independent,
maintainer-controlled channel before trusting it.
Step 2 — verify Sigstore signature and SLSA provenance
cosign verify-blob \
--bundle modde-<version>-x86_64-linux.tar.gz.cosign.bundle \
--certificate-identity-regexp '.*caniko/rs-modde.*' \
--certificate-oidc-issuer-regexp '.*' \
modde-<version>-x86_64-linux.tar.gz
cosign verify-blob-attestation \
--bundle modde-<version>-x86_64-linux.tar.gz.intoto.bundle \
--type slsaprovenance1 \
--certificate-identity-regexp '.*caniko/rs-modde.*' \
--certificate-oidc-issuer-regexp '.*' \
modde-<version>-x86_64-linux.tar.gz
The SLSA predicate records the source Git commit, the flake.lock digest, the
release-workflow digest, and the Attic substituter trust root used for release
builds. Each release also ships CycloneDX (*.cdx.json) and SPDX (*.spdx.json)
SBOMs. See
SECURITY.md
for SBOM scanning and the full signing-key policy.
Troubleshooting
error: cannot find flake 'codeberg:caniko/rs-modde'
codeberg: is a flake-registry shorthand. On older Nix or a trimmed registry, use
the explicit Git URL and make sure flakes are enabled:
nix --extra-experimental-features 'nix-command flakes' \
run "git+https://codeberg.org/caniko/rs-modde"
# /etc/nix/nix.conf (or nix.settings on NixOS)
experimental-features = nix-command flakes
Home-Manager module not recognized
error: The option 'programs.modde' does not exist means the module was not
imported. Confirm both halves are present and that inputs is threaded into the
module (via extraSpecialArgs/specialArgs):
inputs.modde.url = "codeberg:caniko/rs-modde";
# ...and in your home-manager configuration:
imports = [ inputs.modde.homeManagerModules.modde ];
The attribute is homeManagerModules.modde (note the trailing .modde). See the
Home-Manager module reference.
openssl-sys build failure outside the Nix shell
A cargo build/cargo install/cargo test on a bare host typically fails in
openssl-sys with Could not find directory of OpenSSL installation. modde does
not vendor OpenSSL. Either build inside nix develop . -c …, or install the headers
and point pkg-config at them (libssl-dev pkg-config libsqlite3-dev on
Debian/Ubuntu; openssl-devel pkg-config sqlite-devel on Fedora). If a build also
fails to link with wayland/xkbcommon/vulkan errors, install the GUI system
libraries listed under Cargo — or just use the Nix shell.
See also
- Quick start — define and deploy your first profile
- Your first profile — an end-to-end walkthrough
- Home-Manager module reference — every option
- Settings file & environment — the non-Nix config
SECURITY.md— signing keys, SBOMs, and rotation policy
Quick Start
modde runs natively on Linux, macOS, and Windows, and gives you two ways to drive the same engine:
- Imperative (CLI) —
modde detect,modde profile create,modde install …,modde deploy,modde play. The same commands work on every platform. Best for interactive, day-to-day curation where you add and reorder mods by hand. - Declarative (home-manager) — if you use Nix, describe a profile in your home-manager configuration and let an activation script install and deploy it on rebuild. A reproducible, version-controlled option for Wabbajack lists and Nexus Collections.
Both paths share one database, one mod store, and one save vault, so you can start imperatively and pin things down declaratively later (or vice versa) without losing state.
This page is the fast tour. If you want a single narrated end-to-end run, read Your first profile instead. For installing modde itself, see Installation.
The imperative path (CLI)
The CLI walks the whole lifecycle in explicit steps. A minimal run looks like:
# 1. See which games modde found across Steam and Heroic
modde detect
# 2. Create a profile for one of them
modde profile create my-skyrim --game skyrim-se
# 3. Add mods to it (pick whichever source applies — see below)
modde install mod https://www.nexusmods.com/skyrimspecialedition/mods/12345 \
--profile my-skyrim
# 4. Build and deploy the symlink farm into the game directory
modde deploy --profile my-skyrim --game skyrim-se
# 5. Deploy (again) and launch, capturing saves on exit
modde play my-skyrim --game skyrim-se
Detect
modde detect
modde detect auto-discovers installed games across Steam and Heroic (GOG,
Epic, Sideload) and prints their ids and install paths. Use the printed id
(e.g. skyrim-se) for every subsequent --game flag. If your game is not
detected, you can still create a profile and pass paths explicitly, or register
a user-defined game with modde game add.
Create a profile
A profile ties a set of mods to a specific game.
modde profile create NAME --game ID
# e.g.
modde profile create my-skyrim --game skyrim-se
Game ids are the short identifiers in the
supported games table (skyrim-se,
cyberpunk2077, stellar-blade, and so on). 15 games ship today; generic,
user-defined games are also possible via a GameSpec TOML (see
Game support). See
Profile management for listing, switching, forking,
locking, and the experiment stack.
Add mods
Pick the source that matches what you are installing:
# A single Nexus mod (downloads via CDN — needs a Premium key)
modde install mod https://www.nexusmods.com/skyrimspecialedition/mods/12345 \
--profile my-skyrim
# A mod with a FOMOD installer, driven by a saved choices file
modde install mod <url> --profile my-skyrim --fomod-config my-choices.toml
# A whole Wabbajack modlist
modde install wabbajack /path/to/modlist.wabbajack \
--profile my-modlist \
--game-dir "/path/to/Skyrim Special Edition"
# A Nexus Collection
modde install nexus-collection <slug> --profile my-collection
Set up Nexus authentication first with modde nexus auth (see
Nexus Mods). For FOMOD details — generating a choices
template and inspecting options — see the FOMOD guide.
Collections (and Wabbajack lists) auto-lock the profile to preserve the
curator’s intended load order. See Profile management
for forking a locked profile into a freely editable copy. Wabbajack lists that
read vanilla game files (Wabbajack GameFileSourceDownloader entries — Legends
of the Frost is one example) need --game-dir so modde can read and verify
those local files during installation.
Deploy
# Deploy the active profile
modde deploy
# Deploy a specific profile
modde deploy --profile my-skyrim --game skyrim-se
See what deploy actually does below.
Play
modde play my-skyrim --game skyrim-se
modde play switches to the profile (swapping saves), deploys, launches the
game through the detected launcher, and auto-captures saves on exit. Useful
flags: --no-switch, --no-deploy, --no-capture. See
Playing a game.
The declarative path (home-manager)
If you use Nix, modde is also a flake with a home-manager module, so you can declare your mod profiles as code and let them deploy on rebuild. This is one optional path — the CLI above works the same everywhere — but it is handy for reproducible, version-controlled setups. See Installation for adding the flake input and module.
Declared in home-manager, the simplest profile is just a game id:
programs.modde = {
enable = true;
profiles.my-skyrim = {
game = "skyrim-se";
};
};
After home-manager switch, modde deploys your profiles automatically via an
activation script. There is nothing else to run for a plain profile.
Install from a Wabbajack modlist
To install a Wabbajack modlist, add wabbajackList
and point gameDir at your installed game. modde reads the manifest, downloads
the referenced archives (primarily from Nexus — needs an API key), and deploys
the list.
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key"; # sops-nix compatible
profiles.living-skyrim = {
game = "skyrim-se";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition";
wabbajackList = {
url = "https://example.com/modlist.wabbajack";
hash = "sha256-...";
};
};
};
hash is the Nix fetch hash for the .wabbajack file itself — see
Computing a Wabbajack hash below. Skyrim SE lists
that reference vanilla game files (Wabbajack GameFileSourceDownloader
entries — Legends of the Frost is one example) need gameDir so modde can read
and verify those local files during installation.
For .wabbajack files you already have in the Nix store (via requireFile or a
custom fetcher), use a local path instead of url + hash:
programs.modde.profiles.living-skyrim = {
game = "skyrim-se";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition";
wabbajackList = {
path = /nix/store/...-Living-Skyrim.wabbajack;
};
};
Install from a Nexus Collection
programs.modde.profiles.my-collection = {
game = "skyrim-se";
nexusCollection = {
slug = "collection-slug";
version = "1.0.0";
};
};
Collections (and Wabbajack lists) auto-lock the profile to preserve the curator’s intended load order. See Profile management for forking a locked profile into a freely editable copy.
Declaring a profile before the game exists
modde never installs the base game; you install it through Steam or Heroic, and
modde waits. If the game is not installed yet, set installMode = "await-game"
(or simply leave gameDir unset). Activation then skips install/deploy
non-fatally and prints the next step instead of failing the whole rebuild:
programs.modde.profiles.living-skyrim = {
game = "skyrim-se";
installMode = "await-game";
wabbajackList = {
url = "https://example.com/modlist.wabbajack";
hash = "sha256-...";
};
};
Install the game through its launcher, set gameDir, change installMode back
to "auto" (or remove it), and rebuild. modde then installs and deploys the
profile. Wabbajack install runs before deploy only when gameDir exists and
contains the expected game-content directory (e.g. Data/ for Skyrim SE). For
every option, see the Home-Manager module reference.
Computing a Wabbajack hash
The declarative wabbajackList.url + hash pair needs the Nix fetch hash
of the .wabbajack file (SRI format, sha256-…). This is the hash of the
archive, not the Wabbajack-internal archive hashes inside the manifest. Compute
it with either of these:
# Modern Nix (preferred): prints the SRI hash and stores the file
nix store prefetch-file "https://example.com/modlist.wabbajack"
# Or via the flake prefetch helper
nix flake prefetch "https://example.com/modlist.wabbajack"
Both print a sha256-… value you paste verbatim into hash. If you already
have the file locally:
nix hash file ./modlist.wabbajack # SRI sha256- hash of a local file
Wabbajack registry pages usually link through to an authored-files URL for the real
.wabbajackarchive. Some of those CDN links now resolve through Wabbajack’s chunked download page rather than a plain file response, so a bare prefetch (andpkgs.fetchurl) can 404 even though modde’s chunk-aware downloader works. In that case, download the file first and use the local path:modde wabbajack download "<registry-url-or-machine-url>" --output ./list.wabbajack nix hash file ./list.wabbajackThen either set
wabbajackList.path = ./list.wabbajack;(nohashneeded for a path source) or feed it through your own fetcher.
modde can also write the home-manager snippet for you:
modde wabbajack hm-snippet "<url-or-file>" \
--profile living-skyrim \
--game skyrim-se \
--game-dir "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition"
First-run gotchas
A short checklist of things that trip people up on the first profile:
- Install the base game first. modde waits for launcher-managed installs; it
never installs Steam or the game itself. For declarative Wabbajack profiles
before the game exists, use
installMode = "await-game"so activation does not fail the rebuild. - Set up Nexus auth before installing anything from Nexus. Run
modde nexus auth(or setnexus.apiKeyFile). CDN downloads require a Nexus Premium subscription;modde nexus statustells you whether your key is valid and Premium. See Nexus Mods. gameDir/--game-diris required for lists that read vanilla files. Skyrim SE lists withGameFileSourceDownloaderentries fail without it.- Use the detected game id. Run
modde detectand copy the exact id; a typo’d--gameis the most common “nothing happens” cause. - The
hashis the file’s Nix hash, not a manifest hash. Compute it as above; a mismatch aborts the fetch by design. - Deploy is reversible and non-destructive. It does not modify your original
game files, and
modde rollbackrestores the previous deployment. - Adopt existing saves before your first switch. If you already have saves,
run
modde save adopt --game skyrim-se --profile my-skyrimso a later profile switch does not park them unexpectedly. See Save management.
What deploy does
modde deploy builds a symlink farm and links it into the game directory
without touching your original game files. The pipeline has three phases:
- Build — modde resolves the load order and maps each relative file path to its winning source. For any path provided by multiple mods, the mod later in the load order wins; hidden files and profile-level overrides are applied here.
- Materialize — the farm is written to a staging directory
(
~/.local/share/modde/staging/<profile>/); each file becomes a symlink to the winning mod’s content. - Deploy — the materialized staging tree is symlinked into the game’s mod directory.
The previous deployment is preserved in staging.bak/, so
modde rollback --profile my-skyrim atomically swaps back. Wabbajack profiles
deploy differently: their pre-built file layouts are hardlinked (or copied)
straight into the game directory, bypassing the conflict-resolving symlink farm.
Verify a deployment with modde verify --profile my-skyrim. Full detail lives
in the Deployment & VFS guide.
See also
- Your first profile — the same flow as one narrated walkthrough
- Installation — get modde onto your system
- Profile management — switch, fork, lock, experiment
- Wabbajack modlists — installing
.wabbajacklists - Nexus Mods — authentication, mods, Collections
- Deployment & VFS — how deploy works in depth
- Playing a game —
modde playend to end - Supported games — game ids and coverage
Your first profile
This walkthrough takes you from a fresh modde install to a deployed, playable
Skyrim Special Edition profile with a couple of mods, all from the command line.
The commands are identical on Linux, macOS, and Windows, and every one uses the
skyrim-se game id. If you use Nix and prefer the declarative home-manager
workflow, see the Quick Start; this page is the imperative
tour for newcomers.
By the end you will have:
- modde installed and a Nexus key configured
- Skyrim SE detected
- a profile named
first-run - two mods installed (one plain Nexus mod, one with a FOMOD installer)
- a conflict report you understand
- the profile deployed into the game directory
- the game launched and a save captured into the vault
0. Before you start
modde never installs the base game. Install Skyrim Special Edition through Steam or Heroic first and launch it once so the launcher writes its files. modde will detect that install in the next step.
You also need a Nexus Mods account. Downloading mod files over CDN links requires a Nexus Premium subscription.
1. Install modde
Install modde however suits your platform — a native package manager, a direct
download, Cargo, or, if you use Nix, the flake. Every channel ships both the
modde CLI and the modde-ui desktop app. A few examples:
# Arch Linux (AUR)
yay -S modde-bin
# Fedora / RHEL (COPR)
sudo dnf copr enable caniko/rs-modde && sudo dnf install modde modde-ui
# Debian / Ubuntu (apt) — see Installation for the keyring setup
sudo apt install modde modde-ui
# macOS (Homebrew)
brew tap caniko/modde https://codeberg.org/caniko/homebrew-modde
brew install modde
# Windows (winget)
winget install Caniko.Modde
# Any platform with Cargo (builds the CLI from source)
cargo install modde-cli
If you use Nix, modde is also a flake — nix run codeberg:caniko/rs-modde -- detect
to try it without installing, or nix profile install codeberg:caniko/rs-modde
to add both binaries to your profile. The home-manager module additionally lets
you declare your profiles as code.
See Installation for the full channel list, the exact
commands, the macOS Gatekeeper quarantine-clear step, the Windows Authenticode
signature check, and release verification. From here on, the walkthrough assumes
modde is on your PATH.
2. Configure Nexus authentication
Save your Nexus API key to the system keyring once:
modde nexus auth
This prompts for your key and stores it securely in your OS keyring (the secret-service on Linux, Keychain on macOS, the Credential Manager on Windows). Confirm it worked — and check whether you have Premium — with:
modde nexus status
If you manage secrets declaratively, set nexus.apiKeyFile in home-manager
instead. The full credential precedence (OAuth token, key file, env var,
keyring, …) is documented in the Nexus Mods guide.
3. Detect your game
modde detect
modde scans Steam and Heroic (GOG, Epic, Sideload) and prints every game it
found with its id and install path. You should see a row for Skyrim Special
Edition with the id skyrim-se. Copy that id; you will pass it as --game to
nearly every command.
If Skyrim does not appear, make sure it is installed and has been launched once.
You can still proceed by passing paths explicitly, or register a custom game
with modde game add (see Supported games).
4. Create a profile
A profile is a named, per-game collection of mods with its own load order, save assignments, and deployment state. Create one:
modde profile create first-run --game skyrim-se
Confirm it exists and see which profile is active:
modde profile list --game skyrim-se
modde profile active --game skyrim-se
Profiles are cheap. Later you can fork first-run to experiment without risking
your working setup — see Profile management.
5. Add your first mod (a plain Nexus mod)
Install a single mod straight from its Nexus page URL into the profile. modde fetches the file list, picks the latest main file, downloads it over CDN (Premium required), extracts it, and adds it to the profile:
modde install mod \
https://www.nexusmods.com/skyrimspecialedition/mods/12345 \
--profile first-run
Replace 12345 with the real mod id from the URL of the mod you want. A SkyUI-
or USSEP-style utility mod is a good, low-risk first pick.
6. Add a second mod (one with a FOMOD installer)
Many larger Skyrim mods ship a FOMOD installer that asks questions (texture
resolution, compatibility patches, optional features). modde detects
fomod/ModuleConfig.xml automatically. For a reproducible, non-interactive
install you can record your answers in a config file first.
Inspect what a mod offers without installing it:
modde fomod inspect /path/to/extracted-mod
Generate a choices template, edit it, then install with it applied:
modde fomod generate /path/to/extracted-mod --format toml > my-choices.toml
# edit my-choices.toml to pick the options you want
modde install mod \
https://www.nexusmods.com/skyrimspecialedition/mods/67890 \
--profile first-run \
--fomod-config my-choices.toml
Your selections are saved to the profile and replayed on future deployments, so the result is deterministic. The FOMOD guide covers generating, inspecting, and applying configs in depth.
7. Analyze conflicts
Two mods that both ship the same file will collide; only one can win. modde resolves collisions by load order priority — the mod later in the list wins. Before deploying, see what overlaps:
modde collisions --profile first-run
The report groups conflicts by severity:
| Level | Meaning |
|---|---|
| Critical | Will cause game issues |
| Major | Likely visible problems |
| Cosmetic | Minor visual differences |
By default cosmetic conflicts are hidden; add --all to see them. To get
ready-to-run commands that hide files which never win anyway:
modde collisions --profile first-run --all --suggest-hides
For a broader health check (missing masters, Form 43 plugins, and more), run:
modde diagnostics --game skyrim-se --profile first-run
For Bethesda games you can also sort plugin order with the LOOT masterlist
(modde loot sort --game skyrim-se). Conflict resolution, per-file hiding, and
plugin load order are all covered in the
Conflicts & load order guide.
8. Deploy
Now build the symlink farm and link it into the game directory:
modde deploy --profile first-run --game skyrim-se
Deployment is non-destructive: it never edits your original game files, stages
everything under ~/.local/share/modde/staging/first-run/, and keeps the
previous deployment in staging.bak/ so you can modde rollback at any time.
Check that deployed files match their sources with:
modde verify --profile first-run
What each phase does — Build, Materialize, Deploy — is detailed in the Deployment & VFS guide.
9. Play
Launch the modded game. modde play switches to the profile (swapping saves),
deploys, launches through the detected launcher, and auto-captures saves when
the game exits:
modde play first-run --game skyrim-se
A note on launchers:
- Heroic / direct launch — modde waits for the game to exit, then captures saves automatically.
- Steam launch — Steam returns immediately, so modde hands save capture to its launch wrapper. If a save is not captured, run capture manually (next step).
Skip steps with --no-switch, --no-deploy, or --no-capture. See
Playing a game.
10. Capture a save
modde keeps a git-backed save vault per game, with one branch per profile
and a mod fingerprint embedded in each snapshot. After modde play exits, your
save should already be captured. To capture explicitly at any point:
modde save capture --game skyrim-se --profile first-run -m "first run, level 5"
If you launched through Steam and the wrapper has not captured yet:
modde save auto-capture --game skyrim-se
Browse the history and restore an earlier snapshot when you want:
modde save history --game skyrim-se --profile first-run
modde save restore <commit-id> --game skyrim-se --profile first-run
If you already had saves before setting up modde, adopt them once with
modde save adopt --game skyrim-se --profile first-run so the first profile
switch handles them cleanly. Fingerprinting, watching, and assignments are
covered in Save management.
Where to go next
You now have a complete, reproducible modded profile. Branch out from here:
- Profile management — fork
first-run, lock load order, and use the experiment stack to try changes non-destructively - Wabbajack modlists — install a full
.wabbajacklist instead of hand-picking mods - Nexus Mods — Collections, the
nxm://handler, and update checks - Conflicts & load order — deeper conflict resolution and Bethesda plugin sorting
- Tools & overlays — MangoHud, ReShade, OptiScaler, Proton settings, and running xEdit/BodySlide with overwrite capture
- Save management — fingerprinting, watching, and restore
See also
- Quick Start — the same flow, condensed, plus the declarative home-manager path
- Installation — install channels and verification
- Supported games — every shipping game id
- Deployment & VFS — how deploy works
- Playing a game —
modde playreference
Supported Games
This table is intentionally conservative:
Donemeans the game path is shipped end to end.Partialmeans some core pieces exist, but major workflows are still missing or not yet trustworthy.Not shippedmeans the capability should not be treated as available.- The canonical status baseline for this page lives in
docs/capability-matrix.tomlin the repository.
Bethesda titles
| Game | ID | Overall status | Scanner | Conflict detection | Save tracking |
|---|---|---|---|---|---|
| Skyrim Special Edition | skyrim-se | Done | Yes | Yes | Done |
| Skyrim Anniversary Edition | skyrim-ae | Done | Yes | Yes | Done |
| Fallout 4 | fallout4 | Done | Yes | Yes | Done |
| Fallout 76 | fallout76 | Partial | Yes | Yes | Partial (server-side / local cache only) |
| Starfield | starfield | Partial | Yes | Yes | Done |
Other games
| Game | ID | Overall status | Scanner | Conflict detection | Save tracking |
|---|---|---|---|---|---|
| Cyberpunk 2077 | cyberpunk2077 | Done | Yes | Yes | Done |
| Stellar Blade | stellar-blade | Partial | Yes | Yes | Done |
| Baldur’s Gate 3 | baldurs-gate3 | Partial | Yes | Yes | Done |
| Stardew Valley | stardew-valley | Partial | Yes | Yes | Done |
| Fallout: New Vegas | fallout-new-vegas | Partial | Yes | Yes | Done |
| The Elder Scrolls IV: Oblivion | oblivion | Partial | Yes | Yes | Done |
| The Elder Scrolls IV: Oblivion Remastered | oblivion-remastered | Partial | Yes | Yes | Done |
| Mount & Blade II: Bannerlord | bannerlord | Partial | Yes | Yes | Done |
| The Witcher 3: Wild Hunt | witcher3 | Partial | Yes | Yes | Done |
| Subnautica 2 | subnautica2 | Partial | Yes | Yes | Done |
Wabbajack game mapping
When installing Wabbajack modlists, the manifest game names are mapped to modde game IDs:
| Wabbajack name | modde ID |
|---|---|
SkyrimSpecialEdition | skyrim-se |
Fallout4 | fallout4 |
Fallout76 | fallout76 |
Starfield | starfield |
Cyberpunk2077 | cyberpunk2077 |
FalloutNewVegas | fallout-new-vegas |
FalloutNV | fallout-new-vegas |
Oblivion | oblivion |
OblivionRemastered | oblivion-remastered |
Per-game guides
Each shipped title has a dedicated guide with its engine, mod directory, save location, installer quirks, and Linux/Proton notes:
- Creation Engine: Skyrim SE/AE, Fallout 4, Fallout 76, Starfield
- Gamebryo: Fallout: New Vegas, Oblivion
- REDengine: Cyberpunk 2077, The Witcher 3
- Unreal 4/5: Stellar Blade, Subnautica 2, Oblivion Remastered
- Other engines: Baldur’s Gate 3 (Larian), Stardew Valley (SMAPI), Mount & Blade II: Bannerlord
Adding game support
modde ships built-in game plugins for the titles above. Each implements the GamePlugin trait, which provides archive analysis and mod-layout recognition, filesystem scanning and mod discovery, conflict classification, Wine/Proton DLL-override detection, and save-game tracking.
Beyond the built-in list, you can register user-defined games at runtime with modde game add (or import a shareable GameSpec TOML). This is the Generic game support capability — it ships today as a Partial feature: deployment, conflict classification, and launcher integration work, but there is no bespoke scanner or save tracker for arbitrary titles. See Generic & user-defined games for the workflow and the TOML schema.
Skyrim Special Edition / Anniversary Edition
Skyrim Special Edition (skyrim-se) and Anniversary Edition (skyrim-ae) are
modde’s most complete, end-to-end-tested titles. They share this guide because
they are the same game build: identical Steam App ID, install directory, INI
files, archive formats, and Nexus domain. The Anniversary Edition is the Special
Edition plus Creation Club content, so everything below applies to both ids
unless explicitly noted.
Both are marked Done in the
supported-games matrix: the scanner, conflict detection,
and save tracking are all shipped and user-reachable, and the Wabbajack +
home-manager install path described at the end is modde’s strongest worked
example.
Engine and overall status
| Property | Value |
|---|---|
| Engine | Bethesda Creation Engine |
| Game ids | skyrim-se, skyrim-ae (share this guide) |
| Steam App ID | 489830 (both editions) |
| Steam install dir | Skyrim Special Edition |
| Mod directory | Data/ |
| Archive formats | .bsa, .ba2 |
| INI files (per-profile) | Skyrim.ini, SkyrimPrefs.ini, SkyrimCustom.ini |
| Nexus domain | skyrimspecialedition |
| Nexus numeric game id | 1704 |
| Plugin system | Yes (plugins.txt + LOOT) |
| Scanner | Done |
| Conflict detection | Done (loose files + BSA/BA2 contents) |
| Save tracking | Done |
skyrim-ae is registered as a distinct game so you can target it explicitly, but
launcher detection reports skyrim-se by default because AE reuses the SE Steam
app and install directory. When in doubt, use skyrim-se; pick skyrim-ae only
if a modlist or collection demands the AE id specifically.
How modde detects the install
modde locates Skyrim through its launcher ids, then derives the per-user data paths from the Steam App ID. On Linux and macOS Skyrim runs through Proton/Wine, so those paths live inside the compatibility prefix; on Windows the game runs natively and the same paths are their native Windows locations.
-
Steam. modde resolves
489830and theSkyrim Special Editiondirectory under your Steam libraries. Heroic GOG/Epic ids are not set for Skyrim SE/AE (it is a Steam title), so detection is Steam-based. -
Proton prefix (Linux/macOS). Save and
plugins.txtpaths are read from the Proton compatibility prefix keyed by App ID:<steam>/steamapps/compatdata/489830/pfx/drive_c/users/steamuser/...
For Wabbajack lists and any modlist that references the installed base game, you
should still point modde at the game directory explicitly with --game-dir
(CLI) or gameDir (home-manager) — see Linux/Proton notes and known
gotchas.
Mod directory and deploy strategy
Every Bethesda title deploys into the game’s Data/ directory. modde computes
the mod directory as <install>/Data and stages mod content there. The deploy
itself is symlink-based, consistent with all Bethesda games, so the game’s own
Data/ stays a virtual composite of the active profile rather than a
destructively merged folder.
Two file classes get special treatment:
- Loose files (meshes, textures, scripts, INIs) are linked into
Data/under their relative paths. - Archives (
.bsa,.ba2) are deployed as-is, not extracted, because the Creation Engine loads them natively. They are placed directly intoData/.
modde recognizes a “bare” extracted layout — a folder that already looks like a
Data/ tree — by these root markers, matched case-insensitively:
- Root directories:
data,meshes,textures,scripts,interface,sound,music,materials,seq,shadersfx,strings - Root file extensions:
esp,esm,esl,bsa,ba2
This lets modde correctly stage both Data-rooted archives and FOMOD output
without you having to flatten them by hand.
See Deployment for the general symlink model and how overwrite/overrides priority works.
What scanning finds
The Skyrim scanner walks Data/ and reconstructs your installed mods, using
plugins.txt as the authoritative source of load order and enabled state.
It works in two passes:
- Authoritative pass. Every plugin listed in
plugins.txtthat actually exists on disk is emitted as a discovered mod (plugin/<filename>, confidence0.95). For each plugin stem, the scanner also pulls in companion archives that share the stem — both<stem>.bsa/<stem>.ba2and the<stem> - Textures.bsa/.ba2texture-archive convention. - Unmanaged pass. Any
.esp/.esm/.eslinData/not already seen viaplugins.txtis emitted with lower confidence (0.8) so disabled or externally-dropped plugins still surface.
Plugin extensions recognized: .esp, .esm, .esl. Archive extensions paired
with a plugin: .bsa, .ba2. Each discovered file records its Data/-relative
path and size.
See Scanning for how discovered mods feed adoption and stale-duplicate detection.
Conflict classification specifics
Skyrim uses the Bethesda collision classifier, which assigns a severity to every
overlapping file by extension — and, crucially, indexes the contents of
.bsa/.ba2 archives so two mods that ship conflicting files inside
archives are still flagged.
| Severity | Extensions |
|---|---|
| Dangerous | esp, esm, esl, pex, dll, psc |
| Config | ini, cfg, json, toml, xml |
| Cosmetic | dds, png, tga, jpg, nif, hkx, fuz, wav, xwm, swf, btr, bto, btt, bsa, ba2 |
So two mods overwriting the same .dds are a cosmetic (last-wins, usually safe)
conflict, while two mods providing the same plugin record or compiled script
(.pex/.psc) or native DLL are flagged as dangerous and surfaced
prominently. Archive contents (.bsa/.ba2) are read and merged into the
collision map, so the analysis is not limited to loose files.
See Conflicts for how severities drive the conflict report and resolution order.
Save tracking
Save tracking is Done for Skyrim SE/AE. modde reads saves from the Proton
prefix:
<steam>/steamapps/compatdata/489830/pfx/drive_c/Users/steamuser/Documents/My Games/Skyrim Special Edition/Saves
What it captures and fingerprints:
- It tracks
*.esssave files (and*.bakare recognized but skipped as backup copies). The Skyrim.skseco-save written alongside each.essis explicitly excluded from tracking — it is a SKSE side file, not a save of record. - For each
.ess, modde parses the binary header (magicTESV_SAVEGAME) and extracts the save number and character name, producing labels likeLydia — Save 14. If the header is unreadable or has the wrong magic (e.g. a save from another game dropped in the folder), the file is still captured but without a label. - Slot category is inferred from the filename:
Autosave*→auto,Quicksave*→quick, everything else →manual. A multi-save capture is summarized grouped by character (e.g.Lydia (slots 14, 15); Orc Mage (slot 7)).
Save profiles are enabled for Skyrim, so saves are captured per modde profile — letting you swap modlists without cross-contaminating character saves.
This save fingerprinting matters because Skyrim saves bake in your active
plugins: removing or changing a save-breaking mod can corrupt or invalidate an
existing character. modde treats the following extensions as save-breaking,
which is what triggers a save fingerprint before a destructive change: .esp,
.esm, .esl, .pex, .dll, .psc. Cosmetic-only changes (.nif, .dds,
.bsa/.ba2, audio, etc.) do not.
See Saves for the general capture/restore workflow.
Plugin and load-order handling
Skyrim is one of the few engines with full plugin/load-order support in modde.
plugins.txt
modde reads and writes the standard Bethesda plugins.txt format inside the
Proton prefix:
~/.local/share/Steam/steamapps/compatdata/489830/pfx/drive_c/users/steamuser/AppData/Local/Skyrim Special Edition/plugins.txt
Format rules: a leading * marks a plugin enabled, no prefix means
disabled, and # lines are comments. modde-written files start with a
generated header comment (# This file is generated by modde. Do not edit manually.).
modde keeps plugin load order separate from mod install priority — “which mod’s
files win on disk” is independent from “which .esp loads first” — mirroring
MO2’s dual-pane model.
LOOT sorting
modde ships a pure-Rust LOOT masterlist parser (no libloot FFI). It downloads
the public LOOT masterlist for Skyrim SE/AE from
loot/skyrimse and converts its rules to modde’s internal load-order rules:
after→ load-after rulerequires→ load-after rule (and a missing-master signal if absent)incompatible→ incompatible-pair rule
Rules are generated only for plugins that are actually in your active set, and
matching is case-insensitive. skyrim-se and skyrim-ae share the same
masterlist URL.
modde loot sort --game skyrim-se
Plugin diagnostics
modde reads the binary TES4 header of each active plugin (only the first ~1 KB) to run two Skyrim-specific checks plus a shared one:
| Diagnostic | Severity | What it means | Suggested fix |
|---|---|---|---|
| Missing master | Error | A plugin lists a master file that is not in the active load order; the game crashes on load | Install and enable the mod providing that master |
| Form 43 plugin | Warning | A plugin uses the Oldrim/LE format (Form 43) instead of SSE’s Form 44; can cause crashes in SE/AE | Open it in the SSE Creation Kit and re-save to convert to Form 44 |
| Orphaned overrides | Info | The profile’s overrides/ directory contains files (highest priority, wins over all mods) | Review the files to confirm they are intentional |
The Form 43 check is gated to skyrim-se/skyrim-ae specifically, since the
Oldrim-vs-SSE plugin format distinction is unique to Skyrim. ESM/ESL flags are
read from the same header.
modde loot validate --game skyrim-se
Installer specifics
- FOMOD. Scripted and basic FOMOD installers are supported; FOMOD output is
recognized as a
Data/-rooted layout via the bare-layout markers above, so the selected options stage correctly intoData/. See the FOMOD installer guide. - BSA/BA2. Archives are deployed as-is into
Data/and indexed for conflict analysis — they are never unpacked. - Wabbajack. Skyrim SE is modde’s best-supported Wabbajack target. modde
parses Wabbajack manifests, including
GameFileSourceDownloaderentries (local vanilla game files referenced by the list), Wabbajack CDN authored-files archives, Nexus downloads, and ModDB-style mirror pages. See Wabbajack and the worked example below. - REDmod / pak / SMAPI do not apply to Skyrim — those are Cyberpunk,
UE4/5, and Stardew constructs respectively. Skyrim’s content model is loose
files plus
.bsa/.ba2archives plus.esp/.esm/.eslplugins.
Gaming tools that matter for this title
- Proton. Skyrim SE/AE run under Proton; modde derives its save and
plugins.txtpaths from the Proton prefix automatically once the game is installed. - SKSE (Skyrim Script Extender). Many mods depend on SKSE. modde does not
install SKSE for you, but you can register the SKSE loader as a named
executable and launch it through modde — see the executable-management
workflow (
modde tool add-executable/modde exec) in the tools guide. - OptiScaler. modde ships no OptiScaler profiles for Skyrim
(
optiscaler_profilesis empty for both ids); OptiScaler/upscaler tooling is relevant to the UE4/5 titles, not Creation Engine Skyrim.
Linux/Proton notes and known gotchas
These notes concern the game’s runtime on Linux and macOS, where Skyrim runs
through Proton/Wine. On Windows the game runs natively, so the prefix-specific
points below do not apply — saves, plugins.txt, and INI files live at their
native Windows locations. modde itself runs natively on all three platforms.
--game-dir/gameDiris required for vanilla-referencing modlists. Many Wabbajack lists — Legends of the Frost is the canonical example — reference vanilla SkyrimDatafiles as install inputs (GameFileSourceDownloader). modde must read and hash-verify those local game files before staging the list, so you have to point it at the installed game directory. Without it, the install fails with a clear missing---game-direrror rather than silently producing a broken profile.installMode = "await-game"(home-manager). You can declare a Wabbajack profile before Skyrim is installed. Inawait-gamemode, activation prints the next step and exits successfully instead of failing; once Skyrim is installed through Steam/Heroic, setgameDirand switchinstallModeto"auto"(or remove it).- Authored-files availability. Wabbajack lists can break when their upstream
authored-files archives are taken down (HTTP 404). modde runs an availability
preflight and fails fast, reporting exactly which authored-file ids are
missing, before starting bulk downloads. If you have exact local copies, import
them by hash with
modde wabbajack import-archive(name-only matches are refused). - Form 43 plugins. Oldrim-era mods ported carelessly to SE/AE can carry the
Form 43 header and crash; run
modde loot validate --game skyrim-seto catch them.
Worked example
CLI: install a Wabbajack list and deploy
# Install a Wabbajack modlist, verifying vanilla game files under --game-dir.
modde install wabbajack /path/to/lotf.wabbajack \
--profile skyrim \
--game-dir "/path/to/Skyrim Special Edition"
# Sort the load order against the LOOT masterlist and check for problems.
modde loot sort --game skyrim-se
modde loot validate --game skyrim-se
# Deploy the profile into the game's Data/ directory.
modde deploy --profile skyrim --game skyrim-se
If you have local copies of authored archives that were pulled upstream:
modde wabbajack import-archive /path/to/list.wabbajack \
/path/to/Archive-A.7z \
/path/to/Archive-B.7z
Home Manager: declarative Wabbajack profile
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.skyrim = {
game = "skyrim-se";
installMode = "auto";
gameDir = "/path/to/Skyrim Special Edition";
wabbajackList = {
url = "https://authored-files.wabbajack.org/...";
hash = "sha256-...";
};
};
};
gameDir is required for lists (like Legends of the Frost) whose manifest
references local vanilla game files; modde verifies them before staging.
If Skyrim is not installed yet, declare the profile in awaiting mode and fill in
gameDir after installing the game through Steam or Heroic:
programs.modde.profiles.skyrim = {
game = "skyrim-se";
installMode = "await-game";
wabbajackList = {
url = "https://authored-files.wabbajack.org/...";
hash = "sha256-...";
};
};
Activation prints the next step and continues; once Skyrim exists, set gameDir
and switch installMode to "auto".
Note: a
wabbajackListprofile cannot also set anexusCollection— the two install sources are mutually exclusive.
See also
- Supported games
- Installation
- Wabbajack modlists
- Deployment
- Conflicts
- Scanning
- Saves
- FOMOD installers
- Executables & tools
- Troubleshooting
- Parity reference
Fallout 4
Fallout 4 (game_id fallout4, Steam App ID 377160) is a fully shipped
Bethesda title in modde: scanning, conflict detection, plugin load order, and
save tracking all work end to end. Its overall status is Done in the
supported-games table and the
parity audit. It is one of five Creation Engine games
modde drives through a single data-driven BethesdaGame plugin, sharing its
deploy strategy and conflict logic with Skyrim SE/AE, Fallout 76, and Starfield.
Engine & overall status
| Property | Value |
|---|---|
| Engine family | Bethesda Creation Engine (EngineFamily::Bethesda) |
game_id | fallout4 |
| Display name | Fallout 4 |
| Steam App ID | 377160 |
| Heroic (GOG) app ID | 1998527297 |
| Nexus domain | fallout4 (numeric game ID 1151) |
| Wabbajack name | Fallout4 |
| Mod directory | Data/ (relative to the install root) |
| Archive format | .ba2 (Bethesda Archive v2) |
| Managed INI files | Fallout4.ini, Fallout4Prefs.ini, Fallout4Custom.ini |
| Save format | .ess (FO4_SAVEGAME magic) |
| Plugin system | Yes (plugins.txt, ESP/ESM/ESL) |
| Save profiles | Enabled (supports_save_profiles = true) |
| Overall status | Done |
Because Fallout 4 is part of the Bethesda plugin family, it inherits the full
Creation Engine feature set documented for Skyrim SE/AE and in
the Conflicts & load order guide:
plugins.txt read/write, a pure-Rust LOOT masterlist parser, binary plugin
header validation (Form 43 / missing-master detection), and archive-aware
conflict analysis.
Install detection
modde locates a Fallout 4 install through its registered launcher IDs, in this priority order:
- Steam — App ID
377160, install directorysteamapps/common/Fallout 4. This is the primary, best-tested detection path. - Heroic (GOG) — GOG app ID
1998527297, for the GOG build (on Linux/macOS run through Heroic + Proton/Wine; on Windows it runs natively).
There is no Epic launcher mapping for Fallout 4. The install root is the
directory that contains Fallout4.exe and the Data/ folder; you can also point
modde at it explicitly with gameDir (home-manager) or --game-dir (CLI) when
auto-detection cannot reach it.
Proton prefix
On Linux, Fallout 4 runs under Proton, so the Windows-side paths modde needs
(plugins.txt, the save folder, the per-user My Games tree) live inside the
Steam compatibility prefix rather than in your real home directory. modde
resolves them under the App-ID-keyed compatdata prefix:
~/.local/share/Steam/steamapps/compatdata/377160/pfx/drive_c/users/steamuser/
plugins.txt is read from and written to:
.../compatdata/377160/pfx/drive_c/users/steamuser/AppData/Local/Fallout4/plugins.txt
and saves from the My Games tree (see Save tracking). If the
prefix has not been created yet — for example you have installed the game but
never launched it — these paths will not exist, and modde degrades gracefully:
the scanner falls back to a plain directory walk and the save tracker reports no
saves rather than erroring.
Mod directory & deploy strategy
The mod directory is Data/ under the install root (mod_directory returns
install.join("Data")). Every plugin, archive, and loose asset Fallout 4 loads
lives here.
modde deploys with its standard symlink farm: mods are staged in
~/.local/share/modde/staging/<profile>/, conflicts are resolved into a single
winning layout, and that layout is symlinked into Data/. Your real game
install is never overwritten, and a rollback restores the previous deployment
atomically. See Deployment & VFS for the full build →
materialize → deploy pipeline and modde rollback.
.ba2 archives are deployed as-is, not extracted — the Creation Engine loads
them natively, so modde places the archive straight into Data/ and lets the
engine mount it. This matches the bare-layout recognizer, which treats a top-level
Data/, loose .esp/.esm/.esl/.ba2 files, or the usual asset folders
(meshes/, textures/, scripts/, interface/, sound/, materials/,
strings/, …) as a valid Bethesda mod root even when the archive was packed
without a containing folder.
What scanning finds
The Fallout 4 scanner (FALLOUT4_SCANNER, a BethesdaScanner) discovers
installed mods by walking Data/ and cross-referencing plugins.txt:
- Authoritative pass. It reads
plugins.txtfrom the Proton prefix and, for every listed plugin that exists on disk, emits aplugin/<filename>mod at confidence 0.95. For each plugin it also pairs companion archives that share the plugin’s stem — both<stem>.ba2/<stem>.bsaand the texture-archive convention<stem> - Textures.ba2— so a plugin and its assets are reported as one mod. - Unmanaged pass. It then scans
Data/for any.esp/.esm/.eslfiles not present inplugins.txt(disabled or hand-dropped plugins) and reports them at confidence 0.8, again pulling in same-stem archives.
Scan results feed duplicate detection: each discovered mod exposes a Data-relative
footprint (plugin/Foo.esp → foo.esp) so modde can spot files that are already
deployed loose in the game folder versus managed by a profile. See
Scanning installed mods for the shared workflow.
Conflict classification
Conflicts are classified by the shared BethesdaCollisionClassifier, which is
archive-aware: it reads the file listing inside each .ba2/.bsa and folds
those entries into the same collision map as loose files, so two archives that
both ship textures/foo.dds are flagged even though neither was extracted.
Severity is assigned per file extension:
| Severity | Extensions | Meaning |
|---|---|---|
| Dangerous | .esp, .esm, .esl, .pex, .dll, .psc | Plugins, compiled/source Papyrus scripts, native DLLs — overrides here change game logic and can break saves |
| Config | .ini, .cfg, .json, .toml, .xml | Settings files; later mod wins but the change is usually intentional |
| Cosmetic | .dds, .png, .tga, .jpg, .nif, .hkx, .fuz, .wav, .xwm, .swf, .btr, .bto, .btt, .bsa, .ba2 | Meshes, textures, sound, animation, UI — last-writer-wins is normally safe |
This is the same severity ladder modde uses to decide which collisions to surface loudly. See Conflict detection for how severities drive the conflict report and how load-order priority resolves a winner.
Mod-safety classification
Independently of collisions, modde tags whole mods as save-breaking when they
contain .esp/.esm/.esl/.pex/.dll/.psc, and cosmetic when they only
contain art/sound/archive/config assets. This drives the warnings you see before
enabling a mod mid-playthrough.
Plugin & load-order handling
Fallout 4 has a real plugin system (has_plugin_system() is true), and modde
manages it as a first-class, dual-pane concern:
plugins.txtread/write. modde parses the standard format —*Plugin.esp= enabled,Plugin.esp= disabled,#…= comment — from the Proton prefix and writes it back in the same*-prefixed form.- Dual-pane order. Mod install priority (“which mod’s files win”) is kept
separate from plugin load order (“which
.esploads first”), stored in a dedicatedplugin_ordertable per profile — MO2’s core insight. - LOOT masterlist. modde ships a pure-Rust parser for LOOT’s public YAML
masterlist; for Fallout 4 it pulls the
loot/fallout4repository and turns theafter/requires/incompatiblerules for your active plugins into modde load-order rules. - Header validation. A binary header reader inspects the first ~1 KB of each plugin to extract the form version, ESM/ESL flags, and master list, flagging Form 43 plugins and missing masters before they cause a crash on load.
# Auto-sort the Fallout 4 load order against the LOOT masterlist
modde loot sort --game fallout4
# Validate plugin headers (Form version, missing masters)
modde loot validate --game fallout4
Not yet shipped for any Bethesda title: plugin pinning (“lock load order”), a dedicated Archives tab, archive content preview/packing, and per-mod INI tweak editing. LOOT sorting prints rules to the terminal rather than a rich report. See the MO2 parity audit for the full feature comparison.
Save tracking
Save tracking for Fallout 4 is Done and opted into the per-profile save
layer (with_save_profiles(true)).
modde locates saves inside the Proton prefix at:
.../compatdata/377160/pfx/drive_c/Users/steamuser/Documents/My Games/Fallout4/Saves
The Fallout 4 save tracker (FALLOUT4_SAVE_TRACKER) scans that directory for
.ess save files (.bak backups are skipped) and, for each one, parses the
binary header to fingerprint it:
- It verifies the
FO4_SAVEGAMEmagic so a stray Skyrim/Starfield save in the same folder is not mislabelled. - From the header it reads the save number (the slot ID that increments each
save) and the player name (a length-prefixed UTF-8 string), producing a
label like
Sole Survivor — Save 7. - Slot type is inferred from the filename:
Autosave…→auto,Quicksave…→quick, everything else →manual. - A save whose header cannot be parsed is still captured, just without a label.
Captures are summarized newest-first and grouped by character, so a capture
message reads like capture: 3 saves — Sole Survivor (slots 5, 6); …. See
Save tracking & profiles for committing, browsing, and
restoring save snapshots.
Installer specifics
- FOMOD is fully supported. When a downloaded mod contains a
fomod/ModuleConfig.xml, modde detects it and runs the installer — interactively via the GUI wizard, or non-interactively from a saved choices file. Generate a template withmodde fomod generate, inspect options withmodde fomod inspect, and replay choices with--fomod-config. See the FOMOD installer guide. - BA2 archives are deployed verbatim (never repacked or extracted), and their contents are still indexed for conflict analysis as described above.
- Loose-file mods (a bare
Data/tree or top-level asset folders) are recognized by the Bethesda bare-layout policy without any installer. - Fallout 4 does not use REDmod (Cyberpunk), Unreal
.pak, or SMAPI installers — those belong to other engine families. Its installer story is FOMOD + loose files + BA2, the standard Creation Engine toolkit.
Gaming tools & overlays
Fallout 4 launches through modde’s Tools & Overlays layer like any other game. The settings that matter most for this title:
proton— pin a Proton/GE-Proton runner, setextra_env, override the Wine prefix, or force DLL overrides. Fallout 4 Script Extender (F4SE) and many script-extender-dependent mods need the right runner and, occasionally, forced overrides;dll_override_mode=forcedwithforced_dll_overrides=…is how you apply them through modde’s launcher integration.mangohud,vkbasalt,gamemode,reshade— performance overlay, post-processing, the Feral GameMode daemon, and the ReShade shader framework, enabled and configured per game withmodde tool enable/configure.- Overwrite capture. Tools that rewrite files in
Data/(xEdit/FO4Edit, BodySlide, Material Editor) can be run throughmodde tool run … --game fallout4so their output is captured into an__overwrite__mod and survives redeployment.
modde ships no OptiScaler profile for Fallout 4 (
optiscaler_profilesis empty for this title). OptiScaler can still be applied as a generic tool, but there is no curated Fallout-4-specific upscaling profile the way there is for Stellar Blade.
Linux / Proton notes & gotchas
These notes apply to the game’s runtime on Linux and macOS, where Fallout 4 runs
through Proton/Wine. On Windows the game runs natively — there is no Wine prefix,
and plugins.txt, the My Games/Fallout4 tree, and the INI files live at their
native Windows locations. modde itself runs natively on all three platforms.
- Launch the game once first. Until Proton has created the
compatdata/377160prefix,plugins.txt, theMy Games/Fallout4save tree, and the per-prefix INI files do not exist. Run the game once so the prefix is populated, then let modde manage it. - INI files live in the prefix.
Fallout4.ini,Fallout4Prefs.ini, andFallout4Custom.iniare under the prefix’sMy Games/Fallout4, not your Linux$HOME. modde patches them comment- and formatting-preserving rather than rewriting them wholesale. CreatingFallout4Custom.iniand enabling loose-file loading is still your responsibility, exactly as on Windows. - Case sensitivity. Bethesda assets assume Windows’ case-insensitive paths. The bare-layout recognizer matches the standard asset directories case-insensitively, but a poorly packed mod with an unexpected directory name can still mis-deploy on a case-sensitive Linux filesystem — check the conflict report if assets do not load.
- Script extender. F4SE is installed into the game root like on Windows and
launched through the script-extender executable; configure the runner with the
protontool and, if a plugin needs it, forced DLL overrides. - GOG via Heroic. The GOG build (Heroic app ID
1998527297) works, but Steam- Proton is the most exercised path; the GOG prefix layout differs, so verify
the detected
gameDirand save path if you use it.
- Proton is the most exercised path; the GOG prefix layout differs, so verify
the detected
Worked example
Home-Manager profile
{ inputs, ... }:
{
imports = [ inputs.modde.homeManagerModules.modde ];
programs.modde = {
enable = true;
profiles.fo4-vanilla-plus = {
game = "fallout4";
# Leave gameDir unset (or installMode = "await-game") until Steam/Heroic
# has installed Fallout 4 and you have launched it once to build the prefix.
installMode = "await-game";
};
};
}
Once Steam has installed Fallout 4 and you have run it once, set gameDir to the
install root (the folder containing Fallout4.exe and Data/) and rebuild;
modde deploys the profile through its activation script.
CLI
# Install a Nexus mod into a Fallout 4 profile, applying saved FOMOD choices
modde install mod https://nexusmods.com/fallout4/mods/12345 \
--profile fo4-vanilla-plus \
--fomod-config my-choices.toml
# Discover what is already in Data/
modde scan --game fallout4
# Sort and validate the load order against the LOOT masterlist
modde loot sort --game fallout4
modde loot validate --game fallout4
# Deploy, then launch through Proton
modde deploy --profile fo4-vanilla-plus --game fallout4
modde play --game fallout4
# Capture xEdit/FO4Edit edits into an __overwrite__ mod
modde tool run /path/to/FO4Edit --game fallout4 -- -quickautoclean
See also
- Supported games — the status baseline and full game list
- Skyrim SE/AE, Starfield — sibling Creation Engine titles
- Deployment & VFS — the symlink-farm deploy pipeline
- Conflict detection — severities and winner resolution
- Scanning installed mods — what the scanner reports
- Save tracking & profiles — committing and restoring saves
- FOMOD installer — interactive and declarative installs
- Tools & Overlays — Proton, OptiScaler, ReShade, overwrite capture
- Parity audit — what is
DonevsPartial
Fallout 76
Fallout 76 (game_id = fallout76) is one of the five Bethesda Creation
Engine titles modde ships with. It shares the Bethesda deploy strategy, archive
format, and INI machinery with Skyrim SE/AE, Fallout 4, and Starfield, but it is
the odd one out: it is an always-online game whose saves live on Bethesda’s
servers, and whose loose-archive mods are activated through an INI line rather
than a local plugins.txt. Because of that, modde marks the title Partial
overall, with Partial save tracking in particular.
Honest status: deployment, BA2 scanning, conflict classification, and launcher integration are real and working. What is not fully solved is save tracking (saves are server-side; only a local cache is visible) and load-order parity with single-player Bethesda games (Fallout 76 has no LOOT masterlist equivalent). See Supported games and the parity reference for the canonical status baseline.
Engine and overall status
| Property | Value |
|---|---|
| Engine | Bethesda Creation Engine (EngineFamily::Bethesda) |
modde game_id | fallout76 |
| Steam App ID | 1151340 |
| Nexus domain | fallout76 (numeric game id 2590) |
| Wabbajack name | Fallout76 |
| Overall status | Partial |
| Scanner | Yes (loose-archive scanner) |
| Conflict detection | Yes (shared Bethesda classifier) |
| Save tracking | Partial — server-side; local cache only |
| Plugin system | Reported as present, but no local load-order file |
The game is registered in crates/modde-games/src/registry.rs with the
Bethesda engine family, the shared Bethesda collision classifier, a
Fallout-76-specific archive scanner, and a Bethesda save tracker tagged as
“FO76”. The plugin metadata lives in
crates/modde-games/src/bethesda/mod.rs as the FALLOUT76 BethesdaGame
record.
How modde detects the install
modde locates Fallout 76 the same way it finds every other Bethesda title: by its launcher IDs. The registration carries:
steam_app_id = "1151340"steam_dir = "Fallout76"(the folder understeamapps/common/)
There are no Heroic GOG or Epic IDs for Fallout 76 — it ships on Steam (and the Microsoft Store / Bethesda launcher, which modde does not detect). On Linux the Steam copy runs under Proton, so modde resolves both the install root and the per-game Proton prefix from the Steam App ID:
~/.local/share/Steam/steamapps/common/Fallout76/ # install root
~/.local/share/Steam/steamapps/compatdata/1151340/pfx/ # Proton prefix
The Proton prefix matters for two things modde reads under
drive_c/.../AppData/Local/Fallout76/: the (largely unused for FO76)
plugins.txt location and the My Games documents tree where the local save
cache lands.
Mod directory and deploy strategy
Like all Bethesda games, the mod directory is Data/ under the install
root:
~/.local/share/Steam/steamapps/common/Fallout76/Data/
mod_directory() for the Bethesda plugin returns install.join("Data"), so
modde deploys mod files into Data/ through its virtual-filesystem symlink
farm — the same overlay mechanism used for every game. Your real Data/
directory stays clean; modde links the active profile’s files in and removes
them on profile switch or undeploy. See the
Deployment & VFS guide for how the overlay is built
and torn down.
When modde extracts an archive whose contents look like a bare Bethesda layout
(top-level meshes/, textures/, scripts/, interface/, sound/,
strings/, … or loose .esp/.esm/.esl/.bsa/.ba2 files), it recognizes
it as a Data-rooted mod and lays it down accordingly. This is the
BareLayoutPolicy shared across Bethesda titles and is case-insensitive.
What scanning finds
Fallout 76 uses a dedicated scanner that differs from the Skyrim/Fallout 4
scanner. The single-player Bethesda scanner walks plugins.txt for an
authoritative load order; Fallout 76 has no meaningful local load order, so it
uses BethesdaArchiveScanner instead.
The Fallout 76 scanner (FALLOUT76_SCANNER):
- scans the
Data/directory only; - discovers loose
.ba2archive files (the Fallout 76 archive extension); - skips game-shipped archives by ignoring any file whose name starts with
the
SeventySixprefix (those are vanilla content, not mods); - assigns each discovered archive a mod id of the form
archive/<stem>and a confidence of0.8(a heuristic match, since there is no load-order file to corroborate it); - maps a mod id back to its on-disk footprint as
data/<stem>.ba2, which lets modde detect stale duplicates and reconcile a scanned archive against a managed mod.
In short: scanning Fallout 76 returns the loose .ba2 mods you have dropped in
Data/, excluding Bethesda’s own SeventySix*.ba2 archives. Unlike Skyrim or
Fallout 4, it does not pair plugins with companion archives or read enabled
state from a load-order file, because Fallout 76 mods are activated through the
INI rather than a plugin list. See the
Scanning guide for the general scan workflow.
Conflict classification
Fallout 76 uses the shared Bethesda collision classifier, the same one used
by Skyrim and Fallout 4. modde indexes the contents of .bsa/.ba2 archives
(via the archive index reader) and classifies a file collision by its
extension:
| Severity | Extensions (representative) |
|---|---|
| Dangerous | esp, esm, esl, pex, dll, psc |
| Config | ini, cfg, json, toml, xml |
| Cosmetic | dds, png, tga, jpg, nif, hkx, fuz, wav, xwm, swf, btr, bto, btt, bsa, ba2 |
So two mods that both overwrite a texture inside their .ba2 archives produce a
Cosmetic collision (last-deployed wins, cosmetically), whereas two mods that
ship the same script or plugin produce a Dangerous collision worth your
attention. The classifier reads inside archives, so it can flag conflicts
between the contents of two .ba2 files, not just same-named archives.
Caveat for Fallout 76 specifically: Bethesda actively detects and bans certain client modifications in the online game. modde’s conflict severity is about file overwrites, not about whether a given mod is permitted online. Treat
Dangerous-class mods (loose scripts, DLLs, plugins) with extra care on an online title.
See the Conflicts & load order guide for how severities surface and how overwrite order is resolved.
Save tracking
This is where Fallout 76 differs most from its single-player siblings, and why
its save tracking is Partial.
Fallout 76 is an online game: the authoritative save is on Bethesda’s
servers. The local .sav/.ess-style files in the Proton prefix are at best a
partial cache, not your real character. modde represents this honestly:
-
The Fallout 76 save tracker uses the magic header
FO76_SAVEGAMEto identify local Bethesda save files (the same binary header family as Skyrim’sTESV_SAVEGAMEand Fallout 4’sFO4_SAVEGAME). -
When a save header parses, modde fingerprints the save number (a unique, incrementing slot id) and the player name read from the header.
-
The tracker is flagged
is_fo76 = true, so every capture is labelled with an explicit warning. Instead of the normalcapture: …prefix, Fallout 76 captures read:capture (FO76 cache — server saves not tracked): …
That warning is the whole point: modde will fingerprint and snapshot what is on disk, but it tells you plainly that this is a local cache and that your real progress lives server-side and is not under modde’s control. The save directory modde looks in is resolved from the Proton prefix:
.../compatdata/1151340/pfx/drive_c/Users/steamuser/Documents/My Games/Fallout 76/Saves
(my_games_dir = "Fallout 76" for this title.) Do not rely on modde save
profiles to roll a Fallout 76 character back and forth the way you would for
Skyrim — the server state will not follow. For the general save-vault model
(git-backed vaults, fingerprints, auto-capture), see the
Save management guide.
Plugins and load order
has_plugin_system() returns true for every Bethesda game, and modde knows the
Fallout 76 INI files it manages per profile:
Fallout76.iniFallout76Prefs.iniFallout76Custom.ini
But in practice Fallout 76 has no plugins.txt-driven load order parity with
single-player Bethesda titles. There is no LOOT masterlist for Fallout 76, and
the game does not consume a plugins.txt the way Skyrim and Fallout 4 do.
Instead, loose .ba2 archives are activated by listing them in the
sResourceArchive2List / custom archive lines of Fallout76Custom.ini. That is
why the scanner is archive-based rather than plugin-based, and why you should
think in terms of “which archives are listed in my custom INI” rather than
“what is my load order”. modde manages the INI files as part of a profile; you
edit the archive list there to enable a loose .ba2 mod.
Installers (FOMOD, BA2, and friends)
Fallout 76 mods come in two common shapes, and modde handles both:
- Loose / archived files (
.ba2or bareData/layouts). Drop-in mods are recognized by the Bethesda bare-layout policy and deployed intoData/. A single.ba2is the canonical Fallout 76 mod unit. - FOMOD installers. modde includes a FOMOD engine (re-exported from
fomod-oxide) shared by all Bethesda titles, so a Fallout 76 mod packaged as a FOMOD withModuleConfig.xmlis installed through the same interactive or declarative flow as a Skyrim FOMOD. See the FOMOD installer guide.
There is no REDmod, pak, or SMAPI handling here — those belong to
Cyberpunk 2077, Unreal titles, and Stardew Valley respectively. Fallout 76 is
archive-and-INI, plus FOMOD packaging.
Gaming tools for this title
Fallout 76 carries no built-in OptiScaler profile in its registration
(optiscaler_profiles is empty) — unlike Stellar Blade, which ships a
community-dxgi preset. You can still attach the generic tools modde supports to
a Fallout 76 profile through the home-manager module or modde tool commands:
| Tool | Use on Fallout 76 |
|---|---|
proton | Select the Proton runtime and set Wine/Proton DLL overrides for the prefix |
mangohud | Performance HUD overlay |
gamemode | System performance tuning at launch |
vkbasalt | Vulkan post-processing (sharpening, CAS) |
reshade | D3D post-processing for the Wine-backed game |
optiscaler | DLSS/FSR/XeSS upscaling — usable, but no curated FO76 preset ships |
The most relevant of these for an online Proton title is proton itself, for
runtime selection and DLL overrides. See the Tools & overlays
guide for how tools attach to a profile and the
Playing a game guide for the deploy-launch-capture flow.
Linux and Proton notes / known gotchas
The prefix-specific points here describe the game’s runtime on Linux and macOS, where Fallout 76 runs through Proton/Wine. On Windows the game runs natively, so there is no Wine prefix and the local cache and INI files live at their native Windows locations. modde itself runs natively on all three platforms.
- Always-online; saves are server-side. This is the single biggest gotcha.
modde can snapshot the local cache but cannot version your real character.
Every Fallout 76 capture is labelled
FO76 cache — server saves not tracked. - Anti-cheat / bannable mods. Bethesda enforces what is allowed in the live
game. modde’s
Dangerousseverity flags file risk, not online-policy risk; loose scripts/DLLs/plugins can get you actioned regardless of how modde classifies the overwrite. Mod conservatively on a live online account. - Activation is via
Fallout76Custom.ini, not a load order. If a.ba2mod does nothing after deploy, confirm its archive name is listed in the custom INI’s archive list — there is noplugins.txtto enable it. - Vanilla archives are skipped on scan. Anything named
SeventySix*is treated as base-game content and excluded from discovery, so it will not show up as a “mod”. - No Heroic path. Detection is Steam-only for this title; there are no GOG or Epic launcher IDs.
Worked example
Home-Manager profile
A minimal Fallout 76 profile. Because saves are server-side, there is little point in elaborate save automation here — keep the profile focused on deploying loose-archive mods and selecting a Proton runtime:
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.fo76 = {
game = "fallout76";
installMode = "auto";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Fallout76";
tools = {
proton.enable = true;
gamemode.enable = true;
mangohud.enable = true;
};
};
};
CLI
# Confirm modde sees the install (Steam App ID 1151340).
modde game list
# Scan Data/ for loose .ba2 mods (skips SeventySix* vanilla archives).
modde scan --game fallout76
# Inspect conflicts between deployed archives' contents.
modde conflicts --game fallout76
# Deploy the active profile's mods into Data/ and launch under Proton.
modde play --game fallout76
# Snapshot the local save cache. Note the FO76 server-side warning in output.
modde save auto-capture --game fallout76
modde save auto-capture --game fallout76 will print a capture line prefixed
with capture (FO76 cache — server saves not tracked), reflecting that the real
character lives on Bethesda’s servers.
See also
- Supported games — the canonical status table
- Fallout 4 and Starfield — sibling Creation Engine titles with full single-player save tracking
- Deployment & VFS
- Mod scanning
- Conflicts & load order
- Save management
- FOMOD installer
- Tools & overlays
- Playing a game
- Parity reference
Starfield
Starfield is one of modde’s five built-in Bethesda Creation Engine titles. It
shares the data-driven BethesdaGame plugin with Skyrim SE/AE and the Fallout
games, so its deploy strategy, conflict classification, and plugin handling are
the same engine-wide machinery — only the per-title metadata (app ID, save
folder, INI names, archive extension) differs.
Engine & overall status
| Property | Value |
|---|---|
| Engine family | Bethesda Creation Engine |
| modde game id | starfield |
| Display name | Starfield |
| Steam App ID | 1716740 |
| Nexus domain | starfield |
| Nexus numeric game id | 4187 |
| Wabbajack name | Starfield |
| Mod directory | Data/ (under the install root) |
| Archive extension | .ba2 |
| Managed INI files | StarfieldPrefs.ini, StarfieldCustom.ini |
| Save extension | .sfs |
Overall status: Partial. The scanner, conflict classifier, plugin/load-order
handling, and diagnostics are all wired in and exercised, but Starfield’s overall
path is held at Partial because the broader modding workflow around it is still
maturing relative to the fully battle-tested Skyrim SE / Fallout 4 paths.
Save tracking is Done. Starfield ships a dedicated .sfs save tracker that
is reachable end to end through the same git-backed save vault every other game
uses. See Save tracking below and the
canonical status table for how this lines up with the
other titles.
Status vocabulary follows the repository’s
docs/capability-matrix.tomland the parity audit.Donemeans shipped end to end and user-reachable;Partialmeans core logic exists but the surrounding workflow is not yet fully trustworthy.
How modde detects the install
Starfield is registered with a Steam App ID (1716740) and the Steam library
folder name Starfield. modde’s launcher detection uses these to locate the
install under your Steam libraries. No Heroic GOG or Epic IDs are registered for
Starfield, so launcher auto-detection is Steam-oriented; for any other source you
can always point a profile at the install directory explicitly.
Because Starfield runs through Proton on Linux, modde reads its Windows-side game
data — plugins.txt and the per-profile INI files — from inside the Proton
prefix rather than from a native Linux path. The relevant locations under the
Steam compatibility prefix are:
# Save games (auto-detected by the plugin)
~/.local/share/Steam/steamapps/compatdata/1716740/pfx/drive_c/Users/steamuser/Documents/My Games/Starfield/Saves
# Load order (read by the scanner)
~/.local/share/Steam/steamapps/compatdata/1716740/pfx/drive_c/users/steamuser/AppData/Local/Starfield/plugins.txt
The save directory is resolved relative to your detected Steam common/ folder,
so a non-default Steam library still resolves correctly as long as the
compatdata/1716740/pfx prefix exists. If the prefix has not been created yet
(you have never launched the game), save detection returns nothing rather than
guessing a path.
Mod directory & deploy strategy
Like every Bethesda title, Starfield’s mod directory is Data/ under the game’s
install root. modde deploys with the standard symlink-farm virtual filesystem:
mods are staged in an intermediate directory and symlinked into Data/, leaving
the original game files untouched and the whole deployment reversible.
The build/materialize/deploy pipeline, priority ordering (profile overrides win,
then later mods in the load order), rollback, and modde verify all behave
exactly as documented in Deployment & VFS. Nothing
about Starfield changes the deploy model — it is the same engine-wide path used
by Skyrim SE and Fallout 4.
When modde recognizes a bare (loose-files) mod layout during extraction, it
keys off the shared Bethesda layout policy: recognized root directories include
data, meshes, textures, scripts, interface, sound, music,
materials, seq, shadersfx, and strings (matched case-insensitively), plus
root-level files with .esp, .esm, .esl, .bsa, or .ba2 extensions. A mod
archive that unpacks into one of these is treated as already rooted at Data/.
What scanning finds
Starfield uses the data-driven BethesdaScanner, the same plugins.txt-aware
scanner as Skyrim SE and Fallout 4 (Fallout 76 is the exception — it uses the
loose-archive scanner). Scanning the Data/ directory does the following:
- Reads
plugins.txtfor authoritative load order. modde locatesplugins.txtinside the Proton prefix (path above) and parses it. Each line is a plugin; a leading*marks it enabled, an unprefixed line is disabled, and#/blank lines are ignored. - Emits plugins listed in
plugins.txtfirst, at high confidence (0.95). For each plugin it pairs companion archives that share the plugin’s stem — both<stem>.ba2and the texture-suffixed<stem> - Textures.ba2— into the same discovered mod. - Also scans for plugins not in
plugins.txt(disabled or unmanaged) by walkingData/for.esp,.esm, and.eslfiles, emitting them at lower confidence (0.8) so you can see plugins the load order does not yet track.
Each discovered mod’s footprint is recorded Data-relative, which is how modde
compares against MO2-style manifests when detecting stale duplicates. Plugin file
extensions recognized are esp, esm, esl; companion archive extensions are
bsa and ba2 (Starfield ships .ba2).
Conflict classification
Starfield uses the shared BethesdaCollisionClassifier. When two mods provide the
same relative path, modde classifies the severity of the overlap by file type.
It can also read inside .bsa/.ba2 archives (via the archive index) to classify
conflicts on archived contents, not just loose files.
| Severity | Extensions |
|---|---|
| Dangerous | esp, esm, esl, pex, dll, psc |
| Config | ini, cfg, json, toml, xml |
| Cosmetic | dds, png, tga, jpg, nif, hkx, fuz, wav, xwm, swf, btr, bto, btt, bsa, ba2 |
“Dangerous” overlaps (plugins, script bytecode/source, native DLLs) are the ones that change game logic and most often need a real resolution; cosmetic texture and mesh overlaps are usually a matter of which look wins. The same extension set feeds the save-breaking fingerprint described below. For the full conflict workflow — how to inspect, hide files, and reorder — see the Conflicts guide.
Save tracking
Save tracking for Starfield is Done. Unlike the older Bethesda titles, which
parse a binary .ess header for the character name and save number, Starfield’s
saves are .sfs files and are tracked with modde’s declarative pattern-based
tracker.
What the tracker captures:
-
File type:
.sfsfiles in the Saves directory (non-recursive). -
Slot category, derived from the filename prefix:
Prefix Category AutosaveautoQuicksavequickExitsaveexitSavemanualAny
.sfsthat does not match a known prefix still falls back tomanual, so no save is silently dropped. -
Label: the file stem (the save’s filename without the
.sfsextension). -
Capture summary: grouped by category, so a capture reads like
capture: 6 saves (2 auto, 1 exit, 3 manual)rather than a flat count.
These detected saves feed the same git-backed save vault as every other game:
a per-game git repository with per-profile branches, automatic capture on profile
switch and on game exit, history browsing, and restore. Each snapshot embeds a
SHA-256 fingerprint of the profile’s enabled save-breaking mods (the
Dangerous-classified extensions above), so restoring a save that was made under
a different mod set warns you about which save-breaking mods were added or removed.
The end-to-end save workflow — adopt, capture, watch, history, restore,
fingerprint semantics — is documented in the Save management guide.
Plugin & load-order handling
Starfield has a plugin system (has_plugin_system() is true). modde reads and
writes plugins.txt in the *-prefix enabled/disabled format. When modde writes
the file it prepends a generated header marker and emits one line per plugin,
prefixing enabled plugins with *:
# This file is generated by modde. Do not edit manually.
*Starfield.esm
*BlueprintShips-Starfield.esm
SomeDisabledMod.esp
The load order read from plugins.txt is authoritative for the scanner and is the
order modde uses when resolving conflicts (later wins).
Diagnostics
Starfield participates in the shared Bethesda diagnostics engine, which runs:
- Missing masters (
Error): flags an active plugin whose required master is not present in the load order — the game would crash on load. The suggested fix is to install and enable the mod that provides the missing master. - Orphaned overrides (
Info): notes when the profile’s overrides directory contains files, since those take top priority over every mod. - Empty / store-presence checks from the base diagnostics engine.
The Form 43 (“Oldrim” plugin format) diagnostic is Skyrim SE/AE only — it does not apply to Starfield. Do not expect a Form-43 warning here.
Installer specifics
- FOMOD: Starfield benefits from the same scripted-installer support as the
rest of the engine. modde’s FOMOD support is provided by the
fomod-oxideinstaller, so option-tree FOMOD packages (the common Nexus install format for Bethesda mods) are handled by the shared installer path. See the FOMOD guide for how the option tree is presented. - Loose archives / plugins: Mods that are just
.ba2archives and/or.esp/.esm/.eslplugins dropped intoData/are recognized directly by the scanner and bare-layout policy; no scripted installer is required. - There is no REDmod, SMAPI, or
.pakstep for Starfield — those belong to other engines (Cyberpunk, Stardew Valley, and Unreal/Larian titles respectively). Starfield is a plugin +.ba2Creation Engine title.
Gaming tools that matter
Starfield ships with no built-in OptiScaler profiles (optiscaler_profiles
is empty in its registration), so unlike Stellar Blade it has no preconfigured
OptiScaler/OptiPatcher setup. You can still manage the generic gaming tools
through modde’s tool system:
proton— per-game Proton version, Wine prefix, environment variables, and DLL overrides. This is the most relevant tool for Starfield on Linux; several script-extender and ASI-loader mods need forced DLL overrides (see below).mangohud,vkbasalt,gamemode— performance overlay, Vulkan post-processing, and the GameMode daemon.reshade/optiscaler— file-patching tools that place DLLs and configs into the game directory; modde records what it patched sotool revertcan cleanly remove them.
Named executables (script extenders, xEdit/SF1Edit-style tools, ASI managers)
are managed through modde’s Executables system with args, working directory,
environment, Wine DLL overrides, a configurable output mod, and overwrite capture.
See the Tools & overlays guide for modde tool and
modde exec usage.
Linux / Proton notes & known gotchas
These notes cover the game’s runtime on Linux and macOS, where Starfield runs
through Proton/Wine. On Windows the game runs natively — there is no Wine prefix,
and the Saves directory, plugins.txt, and INI files live at their native
Windows locations. modde itself runs natively on all three platforms.
-
Launch the game once before expecting save or load-order detection. Both the Saves directory and
plugins.txtlive insidecompatdata/1716740/pfx, which Proton only creates after the first launch. Until then, save detection returns empty and the scanner falls back to scanning looseData/plugins without an authoritative order. -
DLL-based mods (script extenders, ASI loaders) usually need DLL overrides. Configure them through the
protontool rather than hand-editing the prefix:modde tool configure proton --game starfield dll_override_mode=forced forced_dll_overrides=sfse_loaderUse whatever proxy/loader DLL name the specific mod requires.
-
StarfieldCustom.inimay need creating. Many loose-file and archive-invalidation workflows expect aStarfieldCustom.iniin the My Games folder; modde manages bothStarfieldPrefs.iniandStarfieldCustom.iniper profile, but the game itself does not always ship aCustomINI by default. -
Steam Cloud-aware saves. modde preserves Steam’s cloud marker and parks inactive root saves under its own
.modde/directory during profile switches, so cloud-synced Starfield saves are not clobbered. See the Save management guide for the Steam Cloud behavior.
Worked example
Home-Manager profile
Declare a Starfield profile with the home-manager module. You can declare it before the game is installed and let modde wait for the install:
{ inputs, ... }:
{
imports = [ inputs.modde.homeManagerModules.modde ];
programs.modde = {
enable = true;
profiles.my-starfield = {
game = "starfield";
# Wait for Steam/Proton to create the install before deploying.
installMode = "await-game";
# Once Steam has installed the game, set the Data/-bearing install root:
# gameDir = "/path/to/SteamLibrary/steamapps/common/Starfield";
};
};
}
After Steam has installed Starfield, set gameDir to the install root (the folder
containing Data/) and rebuild; modde deploys the profile via its activation
script.
CLI
# Deploy the Starfield profile, then launch through modde (captures saves on exit)
modde play --game starfield --profile my-starfield
# Or deploy without launching
modde deploy --profile my-starfield --game starfield
# Scan the install for plugins and loose .ba2 archives
modde scan --game starfield
# Adopt pre-existing saves into a profile (first capture in the vault)
modde save adopt --game starfield --profile my-starfield
# Manually capture .sfs saves with a message
modde save capture --game starfield --profile my-starfield -m "before the Unity"
# Watch for new saves when launching outside `modde play`
modde save watch --game starfield --interval 60
# Configure forced DLL overrides for a script-extender / ASI loader
modde tool configure proton --game starfield dll_override_mode=forced forced_dll_overrides=sfse_loader
# Run an external tool (e.g. a conflict cleaner) with overwrite capture
modde tool run /path/to/sf-tool --game starfield -- --some-flag
See also
- Supported games — the canonical status table and Wabbajack mapping
- Deployment & VFS — the symlink-farm deploy model used for
Data/ - Save management — the git-backed save vault and fingerprinting
- Conflicts — inspecting and resolving the conflicts modde classifies
- FOMOD installers — scripted-installer option trees
- Tools & overlays — Proton, OptiScaler, executables, and
modde tool - Scanning — how
modde scandiscovers installed mods - Parity audit — what is
DonevsPartial
Fallout: New Vegas
Fallout: New Vegas (game_id fallout-new-vegas) is a built-in modde title backed by
the shared, data-driven Gamebryo game plugin. The same plugin code drives
Oblivion; the per-game differences (Steam app id, save folder
name, INI set, Nexus domain) are configuration on a single GamebryoGame
record.
Overall status:
Partial. Deployment, filesystem scanning, BSA-aware conflict classification, save tracking, andplugins.txtload-order read/write all ship. What keeps New Vegas fromDoneis the absence of a bespoke, end-to-end script-extender (NVSE) bootstrap workflow and the deeper plugin-management UX (automatic master/ESM ordering, in-app conflict resolution) that the Bethesda Creation-Engine titles get. Treat New Vegas as a solid deploy-and-scan target, not a full Mod-Organizer replacement. The canonical status baseline isdocs/capability-matrix.tomland the supported-games table.
Engine and overall status
| Property | Value |
|---|---|
| Engine family | Gamebryo (EngineFamily::Gamebryo) |
modde game_id | fallout-new-vegas |
| Display name | Fallout: New Vegas |
| Mod directory | <install>/Data |
| Archive format | .bsa (Bethesda archives) |
| Plugin system | Yes — .esp / .esm, ordered via plugins.txt |
| Script extender | NVSE (.nvse) — treated as save-breaking binary content |
| INI files | Fallout.ini, FalloutPrefs.ini, FalloutCustom.ini |
| Save format | .ess (save), .fos (co-save) |
| Nexus domain | newvegas |
| OptiScaler profiles | None shipped for this title |
| Overall status | Partial |
The plugin advertises has_plugin_system() == true and
supports_save_profiles() == true, so New Vegas participates in modde’s
load-order and save-profile machinery.
How modde detects the install
New Vegas is registered with a Steam launcher binding and no Heroic GOG/Epic ids:
| Launcher key | Value |
|---|---|
steam_app_id | 22380 |
steam_dir | Fallout New Vegas |
heroic_gog_app_id | (none) |
heroic_epic_app_id | (none) |
modde locates the install by walking your Steam libraries for app 22380,
landing on steamapps/common/Fallout New Vegas. Because there is no Heroic
binding shipped, GOG/Epic copies are not auto-detected for this title — point
modde at the directory explicitly (--game-dir on the CLI, or gameDir in the
home-manager module) if you run a non-Steam copy.
The numeric Nexus game id is intentionally None for New Vegas: the
newvegas domain is enough for URL-based installs and update tracking, but
the in-app “Browse Nexus” picker (which needs the numeric id) will not list
this title until the id is confirmed. URL installs from
nexusmods.com/newvegas/mods/<id> resolve correctly via the domain.
Mod directory and deploy strategy
The mod directory is <install>/Data — the same Data/ folder New Vegas
itself reads plugins, meshes, textures, sound, and BSAs from.
modde never copies mods on top of your game files. It uses the standard
symlink-farm virtual filesystem described in
Deployment & VFS: mods are staged, the winning file
for each relative path is resolved by load order, and the result is symlinked
into Data/. This keeps the base install clean and every deploy reversible.
When modde unpacks a downloaded archive, the Gamebryo plugin’s
analyze_mod_archive looks for a top-level Data/ directory inside the
extracted mod. If present, it applies StripContentRoot { root: "Data" }, so a
mod packaged as Data/MyMod.esp deploys its contents directly into the game’s
Data/ rather than nesting a redundant Data/Data/. For archives that are not
wrapped in Data/, the plugin’s bare-layout recogniser accepts the mod
when it sees the expected loose roots — data, meshes, textures, sound,
music, menus, scripts, shaders (matched case-insensitively) — or
top-level .esp / .esm / .bsa files. This is what lets older “drop into
Data” mods install without manual repackaging.
What scanning finds
modde scan --game fallout-new-vegas runs the Gamebryo scanner over the single
Data directory. The scanner:
- reads the top level of
<install>/Data(non-recursive); - treats every
.espand.esmfile as a discovered mod; - pairs each plugin with a same-stem
.bsaarchive when one exists (e.g.MyMod.esp+MyMod.bsaare reported together as one mod’s files); - assigns each mod the id
plugin/<filename>with a fixed match confidence of0.85; - records each file’s install-relative path and on-disk size.
Loose meshes/textures dropped straight into Data/ without an accompanying
plugin are not surfaced as distinct mods by this scanner — discovery is
plugin-anchored. For matching an existing install against a Wabbajack manifest
(including New Vegas lists, see below), combine the scan with --manifest; the
scanner’s per-mod footprint is the lowercased plugin filename, which is what
manifest matching keys on. See Mod Scanning for
threshold, dry-run, and import flags.
Conflict classification
Two layers cooperate when New Vegas mods overlap.
Per-file collision severity (used when two deployed mods write the same relative path) comes from the BSA-aware collision policy:
| Severity | Extensions |
|---|---|
Dangerous | .esp, .esm, .dll (also .lua, .ws from the shared default set) |
Config | .ini, .json, .xml |
Cosmetic | .dds, .png, .jpg, .tga, .nif |
.bsa is registered as the archive extension for the New Vegas collision
classifier, so archive overlaps are reasoned about as packed content rather than
loose files.
Per-mod safety classification (classify_mod, used to flag whether
installing/toggling a mod can invalidate saves) uses the Gamebryo content
policy:
- Save-breaking extensions:
.esp,.esm,.pex,.dll,.obse,.nvse, plus anything under ascripts/directory. Adding or removing these changes the form-id / script footprint a save depends on. - Cosmetic extensions:
.nif,.bsa,.dds,.png,.tga,.jpg,.kf,.wav,.mp3,.ogg,.ini,.xml.
The same policy also classifies content into categories used across the UI and
reports — Plugin (esp/esm), Texture, Mesh, Sound, Script
(including .obse/.nvse), Archive (bsa), Config, and Binary (dll).
See Conflicts for how severities surface in the
conflict report.
Save tracking
New Vegas save tracking is shipped (Done). On Linux and macOS the game runs
through Proton/Wine, so the Gamebryo save tracker points at the compatibility
prefix:
<Steam>/steamapps/compatdata/22380/pfx/drive_c/users/steamuser/Documents/My Games/FalloutNV/Saves
i.e. modde derives the save folder from the Steam compatdata directory for app
22380, then descends into the Wine prefix’s
Documents/My Games/FalloutNV/Saves. (Note the on-disk folder name is
FalloutNV, not the modde id fallout-new-vegas.) On Windows the game runs
natively, so the same saves live at their native Windows location with no prefix
involved.
What is fingerprinted in that directory:
- files with the
.ess(save) and.fos(co-save) extensions; *.bakbackups are excluded;- detection is non-recursive (the top level of
Savesonly); - every match is filed under the
manualcategory, with the relative filename as its label, sorted newest-first by modification time.
Capture summaries use the by-category form, e.g. capture: 3 saves (3 manual),
so save-profile snapshots tell you how many saves were rolled into each
profile. Save profiles are enabled for this title. See
Save Management for snapshot, restore, and per-profile
isolation.
Plugin and load-order handling
New Vegas orders plugins through a plugins.txt-style file. modde ships
read/write helpers for that format:
- Reading strips comment lines (
#or;), ignores blanks, and removes a leading*enabled-marker from each plugin name, yielding the ordered list of active plugins. - Writing emits one plugin per line, each prefixed with
*to mark it enabled (creating the parent directory if needed).
This gives modde a faithful round-trip of the active-plugin list. What is not
yet shipped for New Vegas is the higher-level plugin UX the Creation-Engine
titles have — automatic master/ESM sorting, LOOT-style ordering, or in-app
conflict resolution. Curate complex load orders with an external sorter and let
modde persist the result; this is part of why New Vegas is Partial.
Installer specifics
- FOMOD is the primary scripted-installer path. New Vegas mods that ship a
fomod/ModuleConfig.xmlare driven through modde’s FOMOD installer just like the Bethesda titles — see the FOMOD Installers guide for the option/flag selection flow. Data/-rooted and bare archives install without FOMOD via theStripContentRoot/ bare-layout recognition described above.- BSA archives (
.bsa) deploy as opaque packed content; modde does not unpack them, and same-stem BSAs are tracked alongside their plugin. - NVSE / OBSE / script plugins (
.nvse,.obse,.dll) deploy as files intoData/and are flagged save-breaking, but modde does not currently bootstrap the NVSE loader executable for you — install the script extender itself manually into the game root, then manage its plugins through modde. - REDmod,
.pak, and SMAPI installers are not applicable to this engine.
Gaming tools and Proton
New Vegas ships no OptiScaler profiles (optiscaler_profiles is empty for
this title), so the upscaler presets that exist for Unreal titles like
Stellar Blade do not apply here. The engine-agnostic
overlays and the Proton tool still work: MangoHud, vkBasalt, ReShade, GameMode,
and the per-game proton tool (Proton build selection, Wine prefix, environment
variables, and DLL overrides) are all available through
Tools & Overlays. DLL overrides matter for script
extenders and ENB-style injectors that ship a d3d9.dll/dxgi.dll — set those
through the proton tool’s override settings so the Wine prefix loads them.
Linux / Proton notes and gotchas
These notes cover the game’s runtime on Linux and macOS, where New Vegas runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, and saves and INI files live at their native Windows locations. modde itself runs natively on all three platforms.
- Saves live in the Proton prefix, not your home directory. They are under
steamapps/compatdata/22380/pfx/..., which is why a fresh prefix (deleting compatdata, switching Proton builds) appears to “lose” saves. modde reads them from exactly that path, so launch the game once under Proton before expecting save detection to find anything. - No Heroic detection ships for New Vegas. GOG/Epic copies need an explicit game directory.
- Run the game once before deploying so Proton creates the prefix and the
My Games/FalloutNVtree exists; INI files (Fallout.ini,FalloutPrefs.ini,FalloutCustom.ini) are generated on first launch. - Script extenders are DLL injection. NVSE relies on Proton honouring the
loader; verify the override and that the extender’s own files sit in the game
root, not in
Data/. - The numeric Nexus id is unset, so use direct
nexusmods.com/newvegas/...URLs rather than the in-app browse picker.
Worked example
Home-Manager profile
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.viva-new-vegas = {
game = "fallout-new-vegas";
installMode = "auto";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Fallout New Vegas";
wabbajackList = {
url = "https://example.com/viva-new-vegas.wabbajack";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
tools = {
mangohud.enable = true;
gamemode.enable = true;
proton = {
enable = true;
# Honour a script-extender / injector DLL inside the prefix.
settings.dllOverrides = "d3d9=n,b";
};
};
};
};
If New Vegas is not installed yet, omit gameDir or set
installMode = "await-game"; activation prints the next step and continues
without failing, then deploys once you set gameDir and rebuild. See the
Home-Manager Module reference for every option.
CLI
# Discover what is already in Data/ and import it into a profile
modde scan --game fallout-new-vegas --import-to viva-new-vegas
# Match an existing install against a Wabbajack New Vegas list
modde scan --game fallout-new-vegas \
--manifest /path/to/viva-new-vegas.wabbajack \
--import-to viva-new-vegas \
--prune-duplicates
# Deploy the symlink farm into <install>/Data
modde deploy --profile viva-new-vegas --game fallout-new-vegas
# Snapshot saves from the Proton prefix into the profile
modde save snapshot --game fallout-new-vegas --profile viva-new-vegas
# Deploy then launch through Proton
modde play --game fallout-new-vegas
Wabbajack manifests that name the game FalloutNewVegas or FalloutNV
both map to the fallout-new-vegas id, so either spelling in a modlist resolves
to this title. See Wabbajack for the full modlist
workflow.
See also
- Supported Games — status table and Wabbajack mapping
- Oblivion — the other Gamebryo title sharing this plugin
- Deployment & VFS
- Mod Scanning
- Conflicts
- Save Management
- FOMOD Installers
- Tools & Overlays
- Wabbajack
- Home-Manager Module
- Parity & capability matrix
The Elder Scrolls IV: Oblivion
The Elder Scrolls IV: Oblivion (game_id oblivion) is one of modde’s two
Gamebryo-engine titles. It shares a single data-driven game plugin
(GamebryoGame in crates/modde-games/src/gamebryo/mod.rs) with Fallout: New
Vegas, so its mod-directory rules, conflict policy, and plugins.txt handling
match that engine generation.
Status at a glance
| Aspect | State | Notes |
|---|---|---|
| Overall | Partial | Deployment, scanning, conflicts, and save tracking work; there is no bespoke INI-merge or BSA-aware extraction UX. |
| Scanner | Yes | Reads Data/ for .esp/.esm plugins and matching .bsa archives. |
| Conflict detection | Yes | Gamebryo collision policy with a bsa-aware archive comparison. |
| Save tracking | Done | .ess/.fos saves under the Proton prefix, git-backed vault, fingerprinting. |
| Plugin system | Yes | plugins.txt-style load-order read/write. |
The canonical, test-coupled status row lives in the
supported games table. This page expands on the
mechanics behind that Partial rating.
Engine and registration
Oblivion is registered in crates/modde-games/src/registry.rs with the
EngineFamily::Gamebryo engine and the following identifiers:
| Field | Value |
|---|---|
game_id | oblivion |
| Display name | The Elder Scrolls IV: Oblivion |
| Steam App ID | 22330 |
| Steam install dir | Oblivion |
| Nexus domain | oblivion |
| Wabbajack name | Oblivion |
my_games_dir | Oblivion |
| INI file | Oblivion.ini |
| Archive extension | bsa |
The plugin reports has_plugin_system() == true and
supports_save_profiles() == true, and exposes the Nexus domain oblivion so a
nexusmods.com/oblivion/mods/<id> URL resolves straight to this game.
Not to be confused with the 2025 Unreal Engine remaster. That is a separate registration, Oblivion Remastered (
game_idoblivion-remastered, Steam App ID2623190), which uses the Unrealpak/ucas/utocpipeline — not the Gamebryo path described here.
Install detection
modde does not install the base game; it locates a launcher-managed install.
For Oblivion the registration carries only the Steam identifiers
(steam_app_id = "22330", steam_dir = "Oblivion"); the Heroic GOG/Epic app-id
fields are None, so detection is Steam-oriented. Under
launcher_games() Oblivion is matched by its Steam App ID, and the standard
Steam library scan finds the install at
.../steamapps/common/Oblivion/.
If you install through a non-default path (or via Heroic/GOG), set the game
directory explicitly — gameDir in the home-manager module, or --game-dir on
the CLI — and modde will use that instead of probing Steam.
Mod directory and deploy strategy
The Gamebryo plugin places mods under the game’s Data/ folder:
<install>/Data/
That is the value returned by mod_directory(install) —
install.join("Data"). Deployment uses modde’s standard symlink-farm VFS:
mods are staged, the winning file for each relative path is resolved by load
order, and the staging tree is symlinked into Data/. See
Deployment & VFS for the build → materialize →
deploy pipeline and rollback.
Archive recognition on install
When you install a downloaded archive, the plugin’s analyze_mod_archive
detects the common “the mod ships its own Data/ folder” layout and strips it,
mapping the archive’s Data/ contents to the game’s Data/
(InstallMethod::StripContentRoot { root: "Data" }). Archives that instead ship
a bare Gamebryo layout — loose meshes/, textures/, sound/, music/,
menus/, scripts/, shaders/ folders, or top-level .esp/.esm/.bsa
files — are recognized by recognizes_bare_layout (case-insensitively) and
deployed directly into Data/ without a wrapper directory. See the
FOMOD guide for installers that present optional
sub-packages.
What scanning finds
modde scan --game oblivion runs the Gamebryo scanner
(OBLIVION_SCANNER), which inspects only the Data/ directory and looks for
plugin files:
- It enumerates top-level files in
Data/and keeps those with an.espor.esmextension (case-insensitive). Sub-directories are skipped at this level. - For each plugin it also pulls in a sibling
.bsaarchive that shares the plugin’s stem (e.g.MyMod.esp+MyMod.bsa), grouping them into one discovered mod. - Each discovered mod is reported with
mod_id = "plugin/<filename>", the plugin filename as the display name, itsData/-relative file list with sizes, and a detection confidence of0.85.
The scanner’s footprint for matching against a Wabbajack manifest is the
lowercased plugin filename, so manifest archives that carry a known .esp/.esm
line up with on-disk plugins. See Mod Scanning for
--import-to, --manifest, and threshold options.
The scanner intentionally does not walk loose-asset trees or open BSA contents — it discovers plugins and their paired archives. Loose textures and meshes are still resolved and conflict-checked at deploy time; they are just not enumerated as standalone “mods” by
scan.
Conflict classification
Oblivion uses the shared Gamebryo collision classifier with a bsa-aware
archive comparison (gamebryo_collision_classifier, built over
BSA_ARCHIVE_EXTENSIONS = ["bsa"]). When two mods provide the same relative
path, the later mod in the load order wins, and the per-extension severity
comes from the default policy:
| Severity | Extensions |
|---|---|
| Dangerous | esp, esm, dll, lua, ws |
| Config | ini, json, xml |
| Cosmetic | dds, png, jpg, tga, nif |
Run modde collisions --profile <name> for the critical/major report, or add
--all to include cosmetic overlaps. See
Conflicts & Load Order for shadowed-mod and
redundant-file reporting and per-file hiding.
Content categories and save-breaking classification
Separately from collision severity, the Gamebryo content policy classifies
file types so save-tracking knows what is risky. Extensions are mapped to
categories — esp/esm → Plugin, dds/png/tga/jpg → Texture, nif/kf
→ Mesh, wav/mp3/ogg → Sound, pex/obse/nvse → Script, bsa →
Archive, ini/xml → Config, dll → Binary — and the following are treated as
save-breaking:
- Extensions:
esp,esm,pex,dll,obse,nvse - Any file under a
scripts/directory
Cosmetic file types (nif, bsa, dds, png, tga, jpg, kf, wav,
mp3, ogg, ini, xml) are not save-breaking. This set is what feeds the
save fingerprint described below.
Plugin / load-order handling
Gamebryo games carry a real plugin system. modde reads and writes
plugins.txt-style load-order files via read_plugin_order_file /
write_plugin_order_file:
- Reading strips blank lines,
#/;comments, and the leading*enabled-marker, returning a clean ordered list of plugin names. - Writing emits one plugin per line, each prefixed with
*to mark it enabled.
This is the same plugins.txt convention Bethesda’s Creation-Engine titles use,
so modde’s plugin-order backup/restore (modde backup plugins /
backup restore-plugins) applies to Oblivion as well. Note that Oblivion’s
classic engine does not use light plugins (.esl); load order is .esm
masters first, then .esp plugins. modde’s shared LOOT tooling is built for the
Bethesda masterlists; treat any cross-engine LOOT sorting for Oblivion as
best-effort rather than a guaranteed masterlist — fall back to a curated order
captured with backup plugins.
Save tracking
On Linux and macOS Oblivion runs through Proton/Wine, so save_directory()
resolves saves inside the compatibility prefix rather than at a native path:
<steam>/steamapps/compatdata/22330/pfx/drive_c/users/steamuser/Documents/My Games/Oblivion/Saves/
That path is assembled from the Steam common root, the compatdata/<app_id>
prefix, and the game’s my_games_dir (Oblivion). On Windows the game runs
natively, so the same saves live at their native Windows location with no prefix
involved. The Gamebryo save tracker
(GAMEBRYO_SAVE_TRACKER) then:
- Matches save files by extension —
.ess(Oblivion saves) and.fos(shared Gamebryo extension) — non-recursively in that directory. - Excludes
*.bakbackups. - Categorizes every match as
manualand labels it by its relative filename.
Saves flow into modde’s git-backed vault (one repo per game, one branch per
profile). Each snapshot embeds a SHA-256 fingerprint computed over the
sorted list of enabled save-breaking mods (the esp/esm/pex/dll/obse/
nvse + scripts/ set above). On restore, modde compares fingerprints and warns
when the save-breaking mod set has changed since the snapshot was taken. See
Save Management for capture, history, restore, and
save adopt for existing saves.
Installer specifics
- FOMOD — Oblivion mods are frequently packaged as FOMOD installers. modde’s FOMOD support (interactive wizard plus declarative TOML/JSON/Nix config) works for any game; pre-resolve choices declaratively where you can. See the FOMOD guide.
- Bare /
Data-rooted archives — handled automatically byanalyze_mod_archive/recognizes_bare_layoutas described above. - OBMM/OMOD — Oblivion’s legacy OMOD format is not a modde installer
type. Extract such mods to a plain
Data/layout (or convert) before installing. - Script extenders (OBSE) — the Oblivion Script Extender is an external
runtime, not a mod modde installs.
.obse/.dllplugins that ride along with OBSE are scanned/classified (as Script/Binary, and save-breaking), but you launch the game through the script-extender loader yourself — see Proton notes.
There is no REDmod, .pak, or SMAPI path here; those belong to other engines
(Cyberpunk 2077, Baldur’s Gate 3,
Stardew Valley respectively).
Tools that matter for Oblivion
Oblivion has no OptiScaler profiles registered (optiscaler_profiles: &[]),
which is expected for a 2006 DX9 title — DLSS/FSR upscaling frameworks are not a
fit. The tools that do matter are launch-and-config integrations:
- Proton — version selection, Wine prefix, environment, and DLL overrides.
The script extender needs
obse_loader.exe/obse_*.dllto be honored, which usually means a forced DLL override and launching via the loader (see below). - MangoHud / vkBasalt / GameMode — overlay, post-processing, and performance tuning; all apply to Oblivion as launch/environment integrations.
- xEdit / BodySlide-style tools — run external editors with overwrite
capture via
modde tool run, which snapshotsData/before and after and parks new/changed files into an__overwrite__mod.
See Tools & Overlays for the full tool surface, and Playing with modde for deploy-then-launch.
Linux / Proton notes and gotchas
These notes cover the game’s runtime on Linux and macOS, where Oblivion runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, and saves and INI files live at their native Windows locations. modde itself runs natively on all three platforms.
- Saves live in the Proton prefix, under
compatdata/22330/...— not in your Linux home directory. If you change Proton/compat tools or wipe the prefix, the save path moves with it; re-runmodde save adoptif needed. - Script extender launch — OBSE must be invoked through its loader inside the
prefix. Configure Proton DLL overrides for the OBSE DLLs (e.g. via
modde tool configure proton --game oblivion dll_override_mode=forced forced_dll_overrides=...) and set Steam’s launch command to the loader. Data/case-sensitivity — Windows is case-insensitive; Linux is not. modde recognizes bare layouts case-insensitively at install time, but mods that reference assets with a different case than the file on disk can still fail to load. Keep the on-disk casing matching what plugins expect.- INI is not auto-merged — modde detects
Oblivion.inias a config file and classifies INI collisions asConfigseverity, but it does not perform an MO2-style INI tweak/merge. Edit the INI under the prefix’sMy Games/Oblivion/directly when a mod requires it. Partialrating — the core path (install → scan → deploy → conflicts → saves) is solid, but there is no Oblivion-specific BSA browser, no INI editor, and no engine-validated LOOT masterlist. Plan load order withbackup plugins/restore-pluginssnapshots.
Worked example
CLI
# 1. Confirm modde resolves the install (Steam App ID 22330)
modde scan --game oblivion --dry-run
# 2. Create a profile and import what is already in Data/
modde scan --game oblivion --import-to my-oblivion
# 3. Install a Nexus mod (oblivion domain resolves automatically)
modde install mod "https://www.nexusmods.com/oblivion/mods/12345" \
--game oblivion --profile my-oblivion
# 4. Inspect conflicts before playing
modde collisions --profile my-oblivion --all
# 5. Back up the current plugin order, then deploy and play
modde backup plugins --profile my-oblivion --game oblivion
modde play --game oblivion --profile my-oblivion
# 6. Adopt pre-existing saves into the vault (one-time)
modde save adopt --game oblivion --profile my-oblivion
Home-Manager profile
programs.modde.profiles.my-oblivion = {
game = "oblivion";
# Set once Steam has installed the game; until then omit it or use await-game.
gameDir = "/home/me/.local/share/Steam/steamapps/common/Oblivion";
installMode = "auto";
tools = {
gamemode.enable = true;
mangohud.enable = true;
proton = {
enable = true;
settings = {
# Honor OBSE DLLs inside the Proton prefix.
dll_override_mode = "forced";
forced_dll_overrides = "dxgi";
};
};
};
};
Declare the profile before the game exists by leaving
gameDirunset (orinstallMode = "await-game"); activation prints the next step and continues. modde never installs the base game itself.
See also
- Supported games — the canonical status table
- Fallout: New Vegas — the sibling Gamebryo title
- Oblivion Remastered — the separate Unreal remaster
- Mod Scanning —
scan,--import-to, manifest matching - Conflicts & Load Order — severity, hiding, plugin order
- Deployment & VFS — symlink-farm deploy and rollback
- Save Management — vault, fingerprinting, restore
- FOMOD — interactive and declarative installers
- Tools & Overlays — Proton, MangoHud, overwrite capture
- MO2 parity & capability audit — what is
DonevsPartial
The Elder Scrolls IV: Oblivion Remastered
Oblivion Remastered is a hybrid: an Unreal Engine 5 presentation layer wrapped
around the original Gamebryo simulation. modde models that split directly — the
plugin recognises both the UE pak ~mods layout (.pak / .ucas / .utoc)
and Bethesda-style .esp / .esm plugins, and its scanner walks both the
UE pak directory and a Data/ plugin tree. The game id is oblivion-remastered.
Overall status:
Partial. Deployment, scanning, conflict classification, Wine DLL-override detection, and save tracking all work, and the title is wired into the install pipeline (Steam/Heroic detection, Nexus, Wabbajack). What keeps it short ofDoneis the same ceiling as the other UE titles: no bespoke load-order editor for the ESP/ESM side, no pak-internal asset conflict resolution (conflicts are classified by filename, not by archive contents), and the install-method coverage is narrower than the mature Creation Engine games. See Supported games for the status baseline and Parity for the MO2/Vortex/Wabbajack comparison.
Engine and registration
| Field | Value |
|---|---|
| Display name | The Elder Scrolls IV: Oblivion Remastered |
| Game id | oblivion-remastered |
| Engine family | Unreal4 (UE5 layout; shares modde’s UE pak handling) |
| Project folder | OblivionRemastered/ (under the install root) |
| Steam App ID | 2623190 |
| Steam install dir | Oblivion Remastered |
| Heroic (GOG/Epic) | not registered |
| Nexus domain | oblivionremastered |
| Wabbajack name | OblivionRemastered → oblivion-remastered |
| Save profiles | enabled |
| Plugin system | yes (ESP/ESM are recognised alongside paks) |
The plugin is its own GamePlugin implementation rather than a plain instance
of the shared UE4 game type — that is what lets it carry the Bethesda plugin
extensions, the Gamebryo-style save layout, and the blended content policy.
Install detection
modde finds the install through the launcher integration, keyed on the Steam
App ID 2623190 (Steam install dir Oblivion Remastered). There is no
registered Heroic/GOG/Epic mapping, so detection is Steam-first; a manually
specified install path also works for non-Steam copies.
On Linux and macOS the game runs through Proton/Wine, so modde resolves the prefix-rooted paths (saves, and the Wine DLL overrides described below) under the Steam compat data tree (on Windows these are native paths, no prefix involved):
<steam>/steamapps/compatdata/2623190/pfx/drive_c/...
If you have never launched the game, that prefix will not exist yet. modde’s prefix-dependent lookups return “not found” in that case — launch the game once through Steam/Proton so Proton creates the prefix, then re-run.
Mod directory and deploy strategy
The deploy root for pak-style mods is the UE ~mods directory under the
project’s Content/Paks:
<install>/OblivionRemastered/Content/Paks/~mods
The leading tilde is load-bearing: UE’s pak mounter sorts ~mods after the
shipping paks, so mod paks override base content. modde deploys mods here the
same way it deploys for every other game — a profile’s enabled mods are linked
into the live game tree, and switching profiles re-links the set. Plugin-style
content (.esp / .esm) belongs in the game’s Data/ tree, which the scanner
also walks (see below).
What scanning finds
The scanner inspects two locations:
Content/Paks/~mods # UE pak mods
Data # Bethesda-style plugins
- Pak mods are discovered by grouping files that share a stem across the
.pak/.ucas/.utocextensions into a single mod (so a.pakand its sidecar.ucas/.utoccount as one mod, not three). These come from thepaks-modssource location at confidence0.9, with mod ids prefixedpak/. - Plugins are discovered as one mod per top-level
.espfile directly inData/(source locationData, confidence0.8, mod ids prefixedplugin/). No prefixes are ignored, so master files are listed too.
Each discovered mod also has a footprint used for matching against a known mod:
| Mod id form | Footprint path |
|---|---|
pak/<stem> | oblivionremastered/content/paks/~mods/<stem>.pak |
plugin/<stem> | data/<stem>.esp |
Conflict classification
Oblivion Remastered uses modde’s pak collision classifier (archive
extensions pak / ucas / utoc, default severity table). When two enabled
mods write the same relative path, the severity is decided by file type:
| Extension(s) | Severity |
|---|---|
esp, esm, dll, lua, ws | Dangerous |
pak, ucas, utoc (as archive members) | Dangerous |
ini, json, xml | Config |
dds, png, jpg, tga, nif | Cosmetic |
Conflicts are detected by overlapping deployed paths, not by cracking open pak archives — two paks that touch different in-game assets but never collide on a file path are not flagged. Texture/mesh collisions are reported but treated as cosmetic (last-writer-wins is usually fine); plugin and pak collisions are flagged as dangerous because they can change game logic or load behaviour. See Conflict detection for how severities drive the UI and resolution.
Save-breaking classification
Separately from path collisions, modde classifies each mod’s content to decide whether enabling or disabling it should invalidate save compatibility. For Oblivion Remastered:
- Save-breaking extensions:
esp,esm,pak,ucas,utoc,dll,lua - Save-breaking directories:
plugins,logicmods - Cosmetic extensions:
dds,png,jpg,tga,nif
This classification feeds the save-fingerprint logic below: only save-breaking mods contribute to the fingerprint, so swapping a texture pack will not trigger a false “incompatible save” warning, while toggling a plugin or a logic pak will.
Save tracking
Saves live under the Proton prefix, in the remastered Documents tree:
<steam>/steamapps/compatdata/2623190/pfx/drive_c/users/steamuser/Documents/My Games/Oblivion Remastered/Saves
The save tracker is non-recursive over that directory and matches two
extensions, .ess and .sav, excluding *.bak. Every match is filed under
the manual category and labelled by its relative filename. What the tracker
records per save is its path, category, label, and last-modified time — it does
not hash individual save files.
Save fingerprinting happens at the vault layer, and it fingerprints your mod set, not the save bytes: modde computes a SHA-256 over the sorted list of enabled save-breaking mods (per the classification above) and embeds it as a git commit trailer in each snapshot:
Mod-Fingerprint: a1b2c3d4e5f6
Save-Breaking-Mods: pak/some-overhaul, plugin/somequest
On restore, modde compares the snapshot’s fingerprint to your current profile and warns if save-breaking mods were added or removed since that save was taken. Oblivion Remastered opts into per-profile save layering, so each profile gets its own branch in the git-backed vault and profile switches capture/restore automatically. See Save management for the full vault, fingerprint, and restore workflow.
Plugins and load order
modde recognises the ESP/ESM plugin system (it is reported as a plugin-capable
game) and scans Data/ for plugins, so plugin mods are discovered, classified
as save-breaking, and conflict-checked by path. There is no dedicated
load-order editor for the Bethesda side of this title in modde today — load
order for plugins is managed by the game/community tooling you already use, and
pak ordering is governed by UE’s ~mods mounting. Treat plugin ordering as
out-of-band for now.
Installer specifics
When you add a downloaded archive, modde’s analyzer picks the layout:
| Archive shape | Detected method |
|---|---|
.pak / .ucas / .utoc files at the archive root | SingleFileSet — files stage straight into the resolved mod root |
A top-level Data/ directory | StripContentRoot { root: "Data" } — the Data/ wrapper is stripped before staging |
modde also recognises a bare archive layout for this game: an archive whose
root contains any of the directories data, mods, content, paks, or
~mods (matched case-insensitively), or root files with the extensions esp,
esm, pak, ucas, or utoc. That recognition lets it accept the loosely
packed archives common on Nexus for this title without a wrapper directory.
FOMOD installers are handled by modde’s generic FOMOD pipeline when an archive
ships a fomod/ModuleConfig.xml; that path is engine-agnostic and not specific
to this game. There is no REDmod or SMAPI path here — those belong to Cyberpunk
2077 and Stardew Valley respectively. See FOMOD installers
and the installer pipeline for the staging and
uninstall model.
Gaming tools that matter here
Mod loaders, ReShade/ENB-style injectors, and upscalers for this title ship as proxy DLLs dropped next to the shipping executable in:
<install>/OblivionRemastered/Binaries/Win64
modde detects which proxy DLLs are present from this set and surfaces them as Wine DLL overrides (see below):
dwmapi xinput1_3 d3d11 dxgi version winmm
That covers the usual suspects — generic ASI/loader proxies (version,
winmm), UE-loader proxies (dwmapi, xinput1_3), and the graphics-swapper
slots (d3d11, dxgi) used by ReShade and DLSS/FSR swappers such as
OptiScaler.
This game does not ship a curated OptiScaler profile in modde (unlike Stellar
Blade), so OptiScaler is installed and tuned manually as a dxgi/d3d11 proxy.
The named-executable and proxy-DLL machinery is fully supported regardless — see
Tools and executables for adding a launcher executable
with arguments, working dir, env, and DLL overrides, and capturing its output.
Linux and Proton notes
These notes cover the game’s runtime on Linux and macOS, where Oblivion
Remastered runs through Proton/Wine. On Windows the game runs natively — there is
no Wine prefix or WINEDLLOVERRIDES, the save directory is its native Windows
location, and proxy DLLs load directly. modde itself runs natively on all three
platforms.
- Run the game once first. The save directory and the proxy-DLL overrides
are resolved relative to the Proton prefix at
compatdata/2623190/pfx. If you have not launched the game, that prefix does not exist and prefix-rooted lookups will report “not found”. - DLL overrides are automatic from what is present. modde builds
WINEDLLOVERRIDESfrom the proxy DLLs it finds inBinaries/Win64(or in a mod’s staging dir), so Wine loads the native (mod) DLL instead of its built-in stub. You do not hand-write the override string. ~modsordering is a UE convention, not a modde trick. If a pak mod is not taking effect, confirm it landed inContent/Paks/~modsand not loose inContent/Paks.- Texture/mesh collisions are last-writer-wins. They are reported as cosmetic; if a visual mod loses to another, adjust which one deploys, since modde does not merge pak contents.
Worked example
CLI
# Detect installs and confirm the game is found
modde game list
# Install a Nexus pak mod by URL (domain resolves to oblivion-remastered)
modde mod install "https://www.nexusmods.com/oblivionremastered/mods/1234"
# Scan the live install for already-present mods (paks + Data plugins)
modde mod scan --game oblivion-remastered
# Inspect conflicts for the active profile
modde conflicts --game oblivion-remastered
# Adopt existing saves into a profile, then capture
modde save adopt --game oblivion-remastered --profile main
modde save capture --game oblivion-remastered --profile main -m "fresh start"
# Add OptiScaler (or any injector) as a dxgi proxy executable launcher
modde tool add-executable \
--game oblivion-remastered \
--name "Oblivion Remastered (modded)" \
--working-dir "OblivionRemastered/Binaries/Win64"
modde exec --game oblivion-remastered "Oblivion Remastered (modded)"
home-manager profile snippet
A minimal modde profile for this title via the home-manager module:
{
programs.modde = {
enable = true;
profiles.oblivion-remastered = {
game = "oblivion-remastered";
mods = [
# Nexus URLs or local archive paths; paks land in
# OblivionRemastered/Content/Paks/~mods, plugins in Data/.
"https://www.nexusmods.com/oblivionremastered/mods/1234"
];
};
};
}
modde installs the same way for every game — through your platform’s native package manager, a direct download, Cargo, or, if you use Nix, the flake and its home-manager module. The home-manager module additionally lets you declare this profile as code. See Installation for the full set of install channels.
See also
- Supported games
- Oblivion (the Gamebryo original) and the sibling UE titles in the per-game list
- Conflict detection
- Save management
- Deployment and the installer pipeline
- FOMOD installers
- Tools and executables
- Scanning
- Parity reference
Cyberpunk 2077
Cyberpunk 2077 is one of the few non-Bethesda titles that modde supports end to
end. Its overall status is Done in docs/capability-matrix.toml and on
the supported games page: the scanner, conflict classifier,
and save tracker are all wired up, and deployment runs a real REDmod deploy pass
after staging.
The game id is cyberpunk2077. Use it verbatim with every CLI command and in
home-manager profiles.
Engine and status
| Property | Value |
|---|---|
| Engine family | REDengine 4 (EngineFamily::CyberpunkRedEngine) |
| Game id | cyberpunk2077 |
| Display name | Cyberpunk 2077 |
| Overall status | Done |
| Scanner | Yes |
| Conflict detection | Yes |
| Save tracking | Done |
| Steam App ID | 1091500 |
| Heroic (GOG) app id | 1423049311 |
| Heroic (Epic) app id | Ginger |
| Nexus domain | cyberpunk2077 |
| Nexus numeric game id | 3333 |
| Wabbajack name | Cyberpunk2077 |
The numeric Nexus id is present, so Cyberpunk 2077 appears in the UI’s Browse
Nexus picker (which needs the numeric id), and the Nexus side panel can fetch
mod metadata via REST v1 and GraphQL v2. The mod information dialog is still
Partial: only the Nexus-metadata side panel exists — there is no MO2-style
file-tree / INI / conflict dialog yet.
Install detection
modde locates the install through its launcher ids. For Steam it uses App ID
1091500 and the steamapps/common/Cyberpunk 2077 directory; for Heroic it
matches the GOG app id 1423049311 or the Epic app id Ginger. The same ids
drive Proton-prefix discovery for save tracking.
Because Cyberpunk runs through Proton/Wine on Linux, the game writes its saves inside a Wine prefix rather than a native Linux path. modde checks two prefixes, in order:
- Heroic (GOG / sideload):
~/Games/Heroic/Prefixes/default/Cyberpunk 2077/pfx/drive_c/users/steamuser/Saved Games/CD Projekt Red/Cyberpunk 2077 - Steam Proton: the Steam compat prefix under
steamapps/compatdata/1091500/pfx/drive_c/users/steamuser/Saved Games/CD Projekt Red/Cyberpunk 2077
The first prefix that exists on disk wins. If neither is present (for example, the game has never been launched under that launcher, so the prefix has not been created), save tracking reports no save directory.
Mod directory and deploy strategy
The canonical mod directory is <install>/mods/ — the REDmod loader’s directory.
Deployment is a two-step process:
- Per-mod symlink. For every entry in the profile’s staging directory, modde
symlinks the mod directory into
<install>/mods/<name>/. If a target name already exists (a real directory, a file, or a stale symlink), it is removed first so the new symlink is clean. This keeps the game directory pointing at modde-managed staging rather than copying bytes around. - Post-deploy REDmod deploy hook. After symlinking, modde runs a REDmod
deploy pass (see below). This is what turns the symlinked REDmod packages into
the game’s loadable
modarchives.
Symlink deployment means an uninstall or redeploy is cheap and non-destructive to your staged sources.
The REDmod deploy hook
modde looks for the redmod binary in two places:
<install>/tools/redmod/bin/redmod(orredmod.exeon Windows) — the copy CD Projekt Red ships alongside the game, and- anywhere on
PATH.
If a redmod binary is found and <install>/mods/ contains at least one
subdirectory, modde invokes:
redmod deploy -mod <mods/dir-1> -mod <mods/dir-2> ...
with the game directory as the working directory, passing one -mod flag per mod
subdirectory. If the binary is not found, modde logs a warning and skips the
step — it does not fail the deploy. That means loose-file content (redscript,
TweakXL, CET, .archive files) is still deployed by the symlink pass; only the
REDmod-packaged content needs the deploy hook to be registered. A non-zero exit
from redmod deploy does surface as a deploy error, with the tool’s stderr
attached.
Gotcha: REDmod is an optional component in the GOG/Steam installers. If you intend to use REDmod packages, install the REDmod DLC/tool so the
tools/redmod/bin/redmodbinary exists, or put aredmodbinary onPATH.
Bare-layout recognition
When you install an archive that is not a REDmod package, modde recognizes a
“bare” Cyberpunk layout if the extraction root contains any of these top-level
directories: r6, archive, archives, bin, engine, mods, red4ext.
A bare extract is treated as a single mod and symlinked into <install>/mods/<name>/,
preserving the loose-file tree the mod author shipped.
What scanning finds
modde scan --game cyberpunk2077 walks five known mod roots under the install
directory and reports a confidence score per discovered mod:
| Scan root | Mod kind | id prefix | Confidence |
|---|---|---|---|
bin/x64/plugins/cyber_engine_tweaks/mods/<name>/ | Cyber Engine Tweaks (CET) | cet/ | 0.70, or 0.95 if init.lua present |
r6/scripts/<name>/ | redscript | reds/ | 0.90 |
r6/tweaks/<name>/ | TweakXL | tweak/ | 0.90 |
archive/pc/mod/<file>.archive | loose .archive | archive/ | 0.85 |
mods/<name>/ | REDmod package | redmod/ | 0.95 with info.json, else 0.80 |
For REDmod packages, the scanner reads the info.json manifest and pulls the
mod’s name and version from it (falling back to the directory name when the
manifest is missing or unparseable). The manifest fields modde parses are name,
version, custom_sounds, and scripts.
Each discovered mod’s id footprint is stable and lowercased, so modde can detect
stale duplicates across redeploys — for example a CET mod maps to the directory
footprint bin/x64/plugins/cyber_engine_tweaks/mods/<name>/ and a loose archive
maps to the file footprint archive/pc/mod/<stem>.archive.
Conflict classification
modde classifies conflicts at the loose-file level. Cyberpunk’s .archive
container format is proprietary and not yet reverse-engineered in this codebase,
so the classifier does not index inside .archive files — two mods that both
ship archive/pc/mod/foo.archive collide on the file itself, but modde cannot
peek at the resources packed within.
Conflicts are graded by extension into three severities:
| Severity | Extensions |
|---|---|
| Dangerous | reds, lua, tweak, xl, yaml, yml, dll |
| Config | ini, cfg, json, toml |
| Cosmetic | archive, png, jpg, dds, tga |
A Dangerous collision means two mods are fighting over executable game logic (scripts, tweaks, or a native DLL) and the result is likely a crash or broken behavior. Config collisions usually need a manual merge. Cosmetic collisions are last-writer-wins texture/material overrides and are generally safe.
See the conflicts guide for how modde surfaces and resolves these.
Save tracking
Save tracking is Done. Once the save directory is resolved (see
Install detection), modde treats each top-level save folder
as a save and categorizes it by directory-name prefix:
| Prefix | Category |
|---|---|
ManualSave- | manual |
AutoSave- | auto |
QuickSave- | quick |
PointOfNoReturn- | point-of-no-return |
Anything that does not match a known prefix is bucketed as manual (the default
category). The scan is non-recursive (each save is one directory), and the
user.gls settings file is explicitly excluded so it is never mistaken for a save.
What is fingerprinted. For each save, modde tries to extract a human-readable
label. When a save uses CDPR’s NamedSaves, the directory contains a
metadata.9.json; modde reads it and uses the customName (or name) field as
the label. If that file is missing or has no usable name, it falls back to the
save’s directory name. The capture summary is reported by category, so a
snapshot tells you how many manual / auto / quick / point-of-no-return saves were
captured.
Cyberpunk 2077 also declares supports_save_profiles = true, so its saves
participate in save profiles.
Plugin and load-order handling
Cyberpunk 2077 has no ESP/ESM plugin list or loadorder.txt-style ordering
the way Bethesda titles do. There is therefore no plugin enable/disable or
load-order panel for this title. Ordering is whatever the underlying frameworks
impose:
- REDmod order is established by the
redmod deploypass over the mods in<install>/mods/. - redscript, TweakXL, and CET each resolve their own loading; modde’s job is to deploy the files into the correct roots, not to sequence them.
If two logic mods genuinely conflict, modde flags it as a Dangerous collision (see above) rather than trying to reorder them.
Installer specifics
modde inspects an extracted archive and picks an install method:
- REDmod packages are detected by a top-level
info.jsonplus anarchives/(orarchive/) subdirectory. modde records the install as a REDmod install whose manifest isinfo.json, and the post-deploy hook later registers it withredmod deploy. - Bare Cyberpunk extracts (loose redscript / TweakXL / CET /
.archivetrees) are recognized by the top-level directory names listed under Bare-layout recognition and symlinked wholesale intomods/. - FOMOD installers are handled by modde’s general FOMOD engine when a mod ships an XML installer; see the FOMOD guide. This is the same engine used across all supported titles.
There is no SMAPI or .pak/.ucas/.utoc handling for Cyberpunk — those belong
to Stardew Valley and the Unreal titles respectively.
Gaming tools that matter
modde’s tool integrations (modde tool ...) apply to Cyberpunk just as they do to
other titles — MangoHud, vkBasalt, GameMode, ReShade, OptiScaler, and per-game
Proton settings. See the Tools & Overlays guide for the
full workflow.
Cyberpunk does not ship a built-in OptiScaler profile in modde’s registry
(its optiscaler_profiles list is empty, unlike Stellar Blade). You can still
enable and apply OptiScaler manually with modde tool enable optiscaler /
modde tool apply optiscaler, but you choose the proxy DLL and release yourself —
modde does not auto-select a known-good proxy for this title. dxgi.dll is the
proxy most OptiScaler/upscaling setups use on Cyberpunk.
Wine DLL overrides
Many Cyberpunk frameworks ship as proxy DLLs that hijack a Windows system DLL
name in the executable directory bin/x64. Under Wine/Proton these need a
native,builtin override so Wine loads the mod’s DLL instead of its own built-in
stub. modde scans bin/x64 and emits overrides for any of these it finds:
| Proxy DLL | Typically used by |
|---|---|
version | CET (Cyber Engine Tweaks), ASI loaders |
winmm | ASI loaders, some mod frameworks |
dinput8 | various mod frameworks |
d3d11 | ReShade, ENB |
dxgi | OptiScaler, ReShade (often handled by fgmod) |
winhttp | some mod loaders |
xinput1_3 | controller-hook mods |
modde can compute these overrides both from the live game directory and from the
staging directory before deploy (it searches nested mods/.../bin/x64 layouts).
The detected overrides are surfaced through the Proton tool integration so they
end up in your launch environment automatically — no manual WINEDLLOVERRIDES
editing required for the proxies modde knows about. If you use a proxy DLL not in
the list above, set it explicitly, e.g.:
modde tool configure proton --game cyberpunk2077 \
dll_override_mode=forced forced_dll_overrides=dxgi,version
Linux / Proton notes and known gotchas
These notes cover the game’s runtime on Linux and macOS, where Cyberpunk runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, and saves and proxy DLLs live at their native Windows locations. modde itself runs natively on all three platforms.
- Launch the game once first. Saves live inside the Proton/Heroic prefix. Until the game has been run under your launcher, the prefix (and the save directory) does not exist, and save tracking has nothing to find.
- Install REDmod if you use REDmod packages. Without the
tools/redmod/bin/redmodbinary (or aredmodonPATH), the post-deploy hook is skipped with a warning. Loose-file mods still deploy. - Proxy DLLs need overrides. CET (
version.dll), ReShade (d3d11/dxgi), OptiScaler (dxgi), and friends will silently do nothing under Wine without thenative,builtinoverride. modde detects the common ones inbin/x64; verify withmodde tool status --game cyberpunk2077and the troubleshooting guide if a framework does not load. .archiveinternals are opaque. modde detects archive-vs-archive collisions by filename but cannot diff their contents, so two large.archivemods that touch overlapping resources may both report clean while still visually conflicting in-game.- Game runtime differs by platform. The Proton/Heroic prefix paths above are how Cyberpunk runs on Linux and macOS, where it runs through Proton/Wine. On Windows the game runs natively, so there is no Wine prefix — saves and proxy DLLs live at their native Windows locations. modde itself runs natively on all three platforms; see installation.
Worked example
Home-Manager profile
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles = {
cyberpunk-mods = {
game = "cyberpunk2077";
installMode = "auto";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Cyberpunk 2077";
nexusCollection = {
slug = "my-cyberpunk-collection";
version = "2.1.0";
};
tools = {
gamemode.enable = true;
proton = {
enable = true;
settings = {
# CET ships as version.dll; force the override so Wine loads it.
dllOverrideMode = "forced";
forcedDllOverrides = "version,dxgi";
};
};
};
};
};
};
See the home-manager module reference for every profile and tool option.
CLI
# Discover the install and inspect what is already there
modde scan --game cyberpunk2077
# Install a mod from a Nexus URL (REDmod, CET, redscript, TweakXL, or .archive)
modde install --game cyberpunk2077 https://www.nexusmods.com/cyberpunk2077/mods/107
# Deploy the active profile: symlinks into mods/, then runs `redmod deploy`
modde deploy --game cyberpunk2077
# Check for conflicts before playing
modde conflicts --game cyberpunk2077
# Capture and list saves from the resolved Proton/Heroic prefix
modde saves list --game cyberpunk2077
# Force a Wine DLL override if a proxy framework is not loading
modde tool configure proton --game cyberpunk2077 \
dll_override_mode=forced forced_dll_overrides=version,dxgi
See also
- Supported games
- The Witcher 3 — the other REDengine title
- Deployment
- Scanning
- Conflicts
- Saves
- FOMOD installers
- Tools & Overlays
- Nexus integration
- Home-Manager module
- Parity reference
The Witcher 3: Wild Hunt
The Witcher 3: Wild Hunt is the second REDengine title modde supports (alongside
Cyberpunk 2077). It ships as a built-in plugin under the
witcher3 game id.
Engine & overall status
| Property | Value |
|---|---|
| Engine family | REDengine (EngineFamily::Witcher) |
| game id | witcher3 |
| Display name | The Witcher 3: Wild Hunt |
| Overall status | Partial |
| Scanner | Yes |
| Conflict detection | Yes |
| Save tracking | Done |
The status is Partial, matching the supported games table.
Deployment, the filesystem scanner, .ws script-conflict classification, save
tracking, and Wine/Proton DLL-override detection are wired up and reachable, but
The Witcher 3 does not yet have the deeper engine integrations some Bethesda
titles enjoy (there is no LOOT-style load-order sorter and no plugin
validation — REDengine has no .esp/.esm plugin model). Treat it as solid for
loose-folder mods and script mods, with the usual REDengine caveat that
heavily-scripted modlists still benefit from manual review.
Install detection
modde locates the game through its launcher registration:
| Field | Value |
|---|---|
| Steam App ID | 292030 |
| Steam install dir | The Witcher 3 |
| Heroic (GOG) | not wired |
| Heroic (Epic) | not wired |
| Nexus domain | witcher3 |
On a typical Linux Steam install the game directory resolves to something like
~/.local/share/Steam/steamapps/common/The Witcher 3. Run modde detect to see
what modde found, and confirm the resolved path before deploying:
modde detect
Because no Heroic GOG/Epic app ids are registered for this title yet, GOG and Epic copies are not auto-detected. You can still point a profile at any install directory manually (see the worked example below) — detection only affects auto-discovery, not the ability to deploy into a known path.
Proton prefix
The Witcher 3 runs through Proton on Linux. modde does not install Proton or the
game; the proton tool stores per-game launch settings (runner version, prefix
path override, extra environment, DLL-override mode). The DLL proxies modde
detects for script-extender-style mods (below) are surfaced as Wine DLL
overrides so they take effect inside the Proton prefix. See
Tools & Overlays for the proton tool options.
Mod directory & deploy strategy
The mod directory is mods/ directly under the game install:
<install>/mods/
REDengine loads mods from a small set of well-known top-level roots. modde
recognises the multi-root REDkit/REDmod-style layout — mods/, dlc/, bin/,
and content/ — and chooses its deploy path accordingly:
- Multi-root overlay — If the staged content already contains any of
mods/,dlc/,bin/, orcontent/at its top level, modde symlinks those roots straight into the install (a symlink farm, leaving the original game files untouched). This is what most archive mods that bundle their ownmods/modFoo/directory will hit. - Single mod-root deploy — Otherwise modde treats the staged tree as the
contents of one mod and deploys it into the resolved mod root under
mods/.
Either way deployment is non-destructive and reversible — see
Deployment & VFS for the build → materialize → deploy
pipeline and modde rollback.
Archive analysis
When modde unpacks a downloaded archive it picks an install method:
- If the extracted tree contains any of
mods,dlc,bin, orcontent, it is installed as a multi-root overlay preserving those roots. - Otherwise, if there is a single top-level directory whose name begins with
mod(case-insensitive, e.g.modFoo), it is installed as a directory mod undermods/.
The same mod*-prefixed-directory heuristic is also used to recognise a “bare”
mod layout (a folder that is a single mod, with no wrapper directory).
What scanning finds
modde scan --game witcher3 walks the install and discovers already-present
mods in two directories:
| Directory | mod id prefix | Confidence |
|---|---|---|
mods/ | mod | 0.90 |
dlc/ | dlc | 0.75 |
Each immediate sub-directory of mods/ becomes a discovered mod (mod/<name>),
and each sub-directory of dlc/ becomes a discovered DLC-style mod (dlc/<name>).
The on-disk footprint modde records for a discovered mod is the directory itself,
lowercased — mods/<name>/ or dlc/<name>/ — so it can be matched and removed
cleanly later. Import the results into a profile with:
modde scan --game witcher3 --import-to my-witcher3
See Mod Scanning for --dry-run, threshold tuning, and
manifest matching.
Conflict classification
The Witcher 3 collision classifier treats .bundle and .cache as archive
extensions and otherwise uses modde’s default per-extension severities. The most
important entries for this title:
| Extension(s) | Category / severity |
|---|---|
ws | Script — Dangerous (overrides game scripts) |
dll | Binary — Dangerous |
xml, csv | Config |
bundle, cache | Archive |
dds, png, jpg, xbm | Texture / Cosmetic |
Run the standard conflict report:
modde collisions --profile my-witcher3
.ws script conflicts
The defining gotcha for Witcher 3 modding is script merging: many mods ship
gameplay scripts under content/scripts/, and REDengine compiles only one copy
of each script file. Two mods that both ship r4Player.ws will silently clobber
each other unless their changes are merged.
modde detects this specifically:
has_script_conflict(mod_dir)returnstrueif a mod directory contains any.wsfile anywhere in its tree (case-insensitive).script_conflict_paths(mods_root)walks every installed mod undermods/, records the relative.wspaths each one provides (lowercased, forward-slashed), and returns exactly the script paths provided by more than one mod — i.e. the scripts that actually collide.
Because .ws is classified Dangerous and content/scripts is flagged as a
save-breaking directory, script-shipping mods also contribute to the save
fingerprint (below). When two mods collide on the same .ws file you generally
need a manual or tool-assisted script merge — modde tells you which files
collide, but it does not merge REDengine scripts for you. See
Conflicts & Load Order for the general workflow and
per-file hiding.
Save tracking
| Property | Value |
|---|---|
| Save directory | ~/Documents/The Witcher 3/gamesaves |
| Tracked extension | .sav |
| Excluded | *.png (save thumbnails) |
| Recursive | No — top-level of the save dir only |
| Save profiles | Supported |
Saves live in a git-backed vault per the Save Management
guide, with one branch per profile. For The Witcher 3 modde tracks *.sav files
at the top level of the save directory and ignores the *.png save-thumbnail
images that sit alongside them.
What is fingerprinted
modde computes a SHA-256 mod fingerprint from the sorted list of enabled mods classified as save-breaking, and stores it as a commit trailer on each save snapshot. For The Witcher 3 the save-breaking surface is:
- Save-breaking extensions:
ws,xml,bundle,cache,csv,dll - Save-breaking directory:
content/scripts
Cosmetic-only mods (textures such as dds, png, jpg, xbm) do not move the
fingerprint, so swapping a texture pack will not trigger a save-compatibility
warning. Adding or removing a script/archive mod will. On restore, modde compares
fingerprints and warns when the snapshot’s save-breaking set differs from your
current profile.
Plugin / load-order handling
REDengine has no Bethesda-style plugin system, so there is no .esp/.esm
load order, no LOOT sorting, and no plugin validation for this title. Mod
precedence is the ordinary modde load-order priority (later mods win file
conflicts), resolved during the VFS build phase. The one ordering-sensitive area
is .ws script conflicts, which modde surfaces (see above) but does not
auto-resolve. There is no built-in mods.settings/modList priority writer.
Installer specifics
The Witcher 3 modding ecosystem does not use FOMOD, REDmod CLI packaging, UE
.pak chunks, or SMAPI. In practice mods are distributed as archives that either:
- contain the REDengine root layout (
mods/,dlc/,bin/,content/), or - contain a single
mod*-prefixed directory.
modde handles both via the archive-analysis heuristics described above — there is no separate scripted-installer step for this title. (For games that do use guided installers, see FOMOD Installers.)
bundle and cache are treated as packed archive formats for conflict
reporting; modde does not repack them.
Gaming tools that matter
modde does not ship any built-in OptiScaler presets for The Witcher 3 — the
title’s OptiScaler profile list is empty — so upscaling is best handled through
the generic optiscaler tool or your own pin if you want one. The tools that
matter most here:
proton— runner version, prefix override,extra_env, and DLL-override mode. This is where you control how the game launches under Proton.mangohud/vkbasalt/gamemode— overlay, post-processing, and the Feral GameMode daemon, all wired through modde’s launcher integration.- Script-extender-style DLLs — see the Wine DLL proxies below.
See Tools & Overlays for enabling and configuring each.
Wine DLL proxies
Several Witcher 3 mods inject through a proxy DLL placed next to the game
executable (in bin/x64). modde knows the proxy names this title uses and emits
the corresponding Wine DLL overrides so they load inside the Proton prefix:
dxgi, d3d11, version, winmm
When a staged mod (or the live executable directory) contains one of these DLLs, modde adds it to the game’s Wine DLL overrides automatically, so the proxy is picked up under Proton without hand-editing the prefix.
Linux / Proton notes & gotchas
These notes cover the game’s runtime on Linux and macOS, where The Witcher 3 runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, proxy DLLs load directly, and saves live at their native Windows location. modde itself runs natively on all three platforms.
- Documents path. The save directory is the Wine/Proton-mapped
~/Documents/The Witcher 3/gamesaves. If your distro or home setup relocatesDocuments, confirmmodde detect/save scanning is looking where the Proton prefix actually writes saves. - Script merging is on you. modde flags colliding
.wsfiles but does not merge REDengine scripts. For modlists that touch the same scripts, plan to do a manual or tool-assisted merge — and re-checkmodde collisionsafterward. - Proxy DLLs need the override. A mod that relies on a
dxgi/d3d11/version/winmmproxy will silently do nothing under Proton if the override is missing. modde sets these automatically when it sees the DLL; if you install such a mod outside modde, you may need to set the override yourself via theprotontool. - No Heroic auto-detect. GOG/Epic copies are not auto-discovered; point the
profile’s
gameDirat the install directory explicitly.
Worked example
Home-Manager profile
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.my-witcher3 = {
game = "witcher3";
installMode = "auto";
gameDir = "/home/me/.local/share/Steam/steamapps/common/The Witcher 3";
tools = {
gamemode.enable = true;
vkbasalt = {
enable = true;
settings = {
enableOnLaunch = true;
casSharpness = 0.4;
};
};
proton = {
enable = true;
settings = {
version_mode = "launcher_default";
};
};
};
};
};
Declare the profile before the game is installed by omitting gameDir or setting
installMode = "await-game"; activation prints the next step instead of failing.
After Steam finishes installing, set gameDir and rebuild. See the
Home-Manager Module reference for every option.
CLI workflow
# 1. Confirm modde found the install
modde detect
# 2. Import any mods already sitting in mods/ and dlc/
modde scan --game witcher3 --import-to my-witcher3
# 3. Check for colliding files (especially .ws scripts)
modde collisions --profile my-witcher3
# 4. Adopt your existing saves into the vault
modde save adopt --game witcher3 --profile my-witcher3
# 5. Deploy and launch (captures saves on exit)
modde play my-witcher3 --game witcher3
See also
- Supported Games
- Cyberpunk 2077 — the other REDengine title
- Deployment & VFS
- Mod Scanning
- Conflicts & Load Order
- Save Management
- Tools & Overlays
- Playing a Game
- Home-Manager Module
- Parity reference
Stellar Blade
Stellar Blade (game_id stellar-blade, Steam App ID 3489700) is one of
modde’s two data-driven Unreal Engine titles, sharing the Ue4Game plugin with
Subnautica 2. Its overall status is Partial: scanning, conflict
classification, save tracking, deployment, OptiScaler wiring, and Wine DLL-override
detection all ship and are user-reachable, but Stellar Blade does not get a bespoke
installer wizard or a curated load-order model beyond UE4’s ~mods mount order, and
the title is held back from the UI’s “Browse Nexus” picker (see the
Nexus Mods guide). Treat it as a solid pak-mod manager for
the game, not a full guided experience.
For the authoritative status row see
Supported games; the capability vocabulary (Done /
Partial / Not shipped) is defined there and in docs/capability-matrix.toml.
Engine and project layout
| Property | Value |
|---|---|
| Engine family | Unreal4 (the shared UE4/UE5 pak plugin) |
| Steam App ID | 3489700 |
| Steam library folder | Stellar Blade |
| UE project short name | SB |
| Nexus domain | stellarblade |
| Wabbajack name | StellarBlade |
| Save profiles | Enabled |
| OptiScaler profiles | community-dxgi |
Unreal titles share a near-identical on-disk shape: a project folder under the
install root (here SB/) that holds SB/Content/Paks/ for pak archives and
SB/Binaries/Win64/ for the shipping executable and any proxy DLLs. Because the
whole plugin is parameterised by that project short name, every path below is just
<install>/SB/....
How modde detects the install
modde resolves Stellar Blade through its registered launcher IDs. Only the Steam path is wired:
- Steam — App ID
3489700, library subfolderStellar Blade. modde walks your Steam library folders (including extra libraries fromlibraryfolders.vdf) to find the install root. - Heroic (GOG/Epic) — not configured for this title; there is no Heroic GOG or Epic app ID in the registry, so Heroic auto-detection does not apply.
You can always bypass detection by passing --game-dir <path> to commands that take
it (for example modde scan and modde install wabbajack), or by setting gameDir
in a home-manager profile.
Proton prefix
On Linux and macOS Stellar Blade runs through Proton/Wine, so both save tracking
and the UE Saved/Config deploy target read from the compatibility prefix that
Steam creates at (on Windows these are native paths, no prefix involved):
<steam_root>/steamapps/compatdata/3489700/pfx/
modde derives this from the Steam common directory’s parent, then compatdata/3489700/pfx.
If the prefix does not exist yet, the save directory and the ue4-saved-config
deploy target both resolve to None — the expected fix is to launch the game once
so Proton materialises the prefix, then retry.
Mod directory and deploy strategy
The deploy target is the standard UE pak drop folder:
<install>/SB/Content/Paks/~mods
The leading tilde in ~mods is load-bearing: UE’s pak mounter orders mount points
lexically, and ~mods sorts after the shipping paks so your mods override base
content. modde creates and deploys into this directory; it does not invent a custom
VFS for UE titles — deployment is a real on-disk layout under Content/Paks/~mods.
A mod archive is recognised as a single-file-set install when its extracted root
contains at least one .pak, .ucas, or .utoc file (InstallMethod::SingleFileSet).
That is the common case for Nexus pak mods, which extract a loose Name.pak
(optionally with Name.ucas / Name.utoc siblings) you drop straight into ~mods.
In addition to ~mods, the plugin exposes one named deploy target for user config:
| Target ID | Label | Kind | Resolves to |
|---|---|---|---|
ue4-saved-config | UE4 Saved/Config | UserConfig | <prefix>/drive_c/users/steamuser/AppData/Local/SB/Saved/Config/Windows |
This second target is how Engine.ini / GameUserSettings.ini style tweaks land in
the right place inside the Proton prefix. It resolves to None until the prefix exists.
What scanning finds
modde scan --game stellar-blade runs the UE4 scanner, which walks two
subdirectories of SB/Content/Paks/:
| Subdirectory | Source location tag |
|---|---|
~mods | paks-mods |
LogicMods | logic-mods |
Within each, files are grouped by file stem across the .pak / .ucas /
.utoc extensions. A Name.pak plus its Name.ucas and Name.utoc siblings collapse
into a single discovered mod with mod_id = "pak/Name" and a fixed scan confidence of
0.9. The scanner reads pak archives by directory and by their grouped stems only — it
does not crack open pak contents, parse asset trees, or read mod-author metadata
from inside the archives.
For change-tracking, a discovered mod’s footprint is its .pak file, recorded as
sb/content/paks/~mods/<stem>.pak (lower-cased). The .ucas/.utoc sidecars collapse
into that same row, which keeps the mod list one-entry-per-mod even though several files
back it.
# Discover already-installed Stellar Blade paks and import them into a profile
modde scan --game stellar-blade --import-to default
Conflict classification
Conflicts use the shared UE collision policy. Stellar Blade declares pak, ucas, and
utoc as archive extensions, so two mods shipping the same-named pak family are a
real overwrite conflict. Severities are assigned per file extension:
| Severity | Extensions |
|---|---|
Dangerous | pak, ucas, utoc, dll, lua |
Config | ini, cfg, json, toml, xml, yaml |
Cosmetic | dds, png, jpg, tga |
The same extension lists also drive mod safety classification (whether a mod is
considered save-affecting versus purely cosmetic): pak, ucas, utoc, dll, and
lua are treated as save-breaking/logic-altering, while png, jpg, dds, tga,
and ini are treated as cosmetic. So a texture-only retexture conflicting with another
texture is flagged Cosmetic, while two paks clobbering one another are Dangerous.
See Conflicts & load order for how modde presents and resolves these.
Save tracking
Save tracking is enabled for Stellar Blade (with_save_profiles(true)), and it is
wired into modde’s per-profile save layer.
Location. Saves are read from the Proton prefix:
<steam_root>/steamapps/compatdata/3489700/pfx/drive_c/users/steamuser/AppData/Local/SB/Saved/SaveGames
modde returns this directory only if it already exists on disk.
What is fingerprinted. The save tracker matches files by extension .sav,
recursively under the save directory, with no prefix rules and no exclusions. Every
matching file is captured under the default category manual, its label taken from the
file stem (falling back to the relative path). Captured saves are summarised by
category. Because matching is purely extension-and-recursion based, modde tracks the
.sav files themselves — it does not parse Stellar Blade’s save binary format or
extract in-game playtime, character, or chapter metadata.
See Save management for how captures attach to profiles.
Plugin and load-order handling
Stellar Blade has no plugin file (.esp/.esm) load order — it is not a Bethesda
title. Ordering is purely the UE pak mount convention: paks in ~mods mount after base
game paks, and within ~mods UE orders by name. If two mods need a deterministic order
relative to each other, the lever is the pak file name (the conventional zzz_-style
prefixes that sort late). modde does not synthesize or rewrite a load-order file for this
game; there is no curated load order beyond what the file names express.
A separate LogicMods folder is also scanned (tagged logic-mods) for setups that keep
Blueprint/logic paks distinct from content paks, but modde treats both folders uniformly
as grouped pak mods.
Installer specifics
- Pak / ucas / utoc — the native format. Archives whose root contains a pak-family
file install as a single file set straight into
~mods. This is the path most Nexus Stellar Blade mods take. - FOMOD — modde’s FOMOD installer is engine-agnostic, so a mod
that ships a
ModuleConfig.xmlcan be driven throughmodde fomod inspect/modde fomod apply(or a declarative config). FOMOD is not Stellar Blade-specific and is uncommon for this title, but it is available when an author uses it. - REDmod / SMAPI / BSA — not applicable. Those are Cyberpunk, Stardew, and Bethesda installer paths respectively; Stellar Blade uses none of them.
There is no bespoke Stellar Blade installer wizard — this is part of why the title is
Partial. Most installs are “extract pak, deploy to ~mods”.
Gaming tools that matter
OptiScaler — the community-dxgi profile
Stellar Blade ships exactly one curated OptiScaler profile, community-dxgi
(“Community tested dxgi.dll”). It is the recommended way to add modern upscaling/frame
generation. Its pinned settings, straight from the registry:
| Field | Value |
|---|---|
| Proxy DLL | dxgi.dll |
| Source mode | github_release, release tag official:v0.9.1 |
| Tested OptiScaler version | 0.9 |
| FSR4 variant | latest_fp8 |
emulate_fp8 | true (FP8 emulation, for RDNA3 and other cards without native FP8) |
| OptiPatcher | enabled |
spoof_dlss | false |
| Companion files | copied |
The profile uses OptiPatcher to unlock the game’s DLSS and DLSS-FG inputs without
spoofing a DLSS-capable GPU — so you get the DLSS/DLSS-FG code paths re-routed through
OptiScaler/FSR rather than faking vendor detection. The latest_fp8 FSR4 variant with
emulate_fp8 = true is what lets RDNA3-class cards run the FP8 model via emulation.
Known OptiScaler gotchas for this title (from the bundled profile notes):
- The game may crash on first boot with the proxy in place but work on the next launch — if the first launch dies, just relaunch before assuming a bad install.
- If DLSSG/frame-gen HUD elements look wrong (interpolation artifacts on the HUD), set the in-game sharpness slider to 0 as a workaround.
Enable and apply it through the tools system:
# Enable OptiScaler for Stellar Blade and select the curated profile
modde tool enable optiscaler --game stellar-blade
modde tool configure optiscaler --game stellar-blade optiscaler_profile=community-dxgi
modde tool apply optiscaler --game stellar-blade
modde tool apply writes the dxgi.dll proxy and companion files into the game’s
Binaries/Win64. See Tools & overlays and the OptiScaler source
selector (official releases versus GOverlay builds) documented there.
Proton, overlays, and Wine DLL overrides
The other tool IDs apply normally for this Proton game: proton (version mode, extra env,
DLL-override mode), mangohud, vkbasalt, gamemode, and reshade. Because Stellar
Blade runs through Wine/Proton, any native proxy DLLs you stage must be loaded native
first. modde scans SB/Binaries/Win64 (and mod staging) for the common UE proxy DLL
names and surfaces them as WINEDLLOVERRIDES:
dwmapi, xinput1_3, d3d11, dxgi, version, winmm, dinput8
So a UE4SS install (dwmapi), a ReShade/DLSS swapper (dxgi/d3d11), or a generic ASI
loader (version/winmm) is detected and overridden automatically — the OptiScaler
profile’s own dxgi.dll is exactly one of these.
Linux / Proton notes and gotchas
These notes cover the game’s runtime on Linux and macOS, where Stellar Blade runs
through Proton/Wine. On Windows the game runs natively — there is no Wine prefix or
WINEDLLOVERRIDES, and the save directory and config live at their native Windows
locations. modde itself runs natively on all three platforms.
- Launch the game once before first deploy. The save directory and the
ue4-saved-configtarget both live inside the Proton prefix and resolve toNoneuntil Proton has createdcompatdata/3489700/pfx. - OptiScaler first-boot crash. Expected with this profile; relaunch. (See the OptiScaler section above.)
- HUD interpolation under DLSSG. Set the in-game sharpness slider to 0.
- DLL override ordering. If a proxy DLL mod is staged but not taking effect, confirm
the matching
WINEDLLOVERRIDESentry is present — modde derives it from the proxy name inBinaries/Win64, so the file must be the recognised name. - Pak mount order is name-driven. There is no load-order editor; rename paks (e.g. a late-sorting prefix) when one mod must win over another.
Worked example
CLI
# 1. Discover the install (Steam App ID 3489700 / "Stellar Blade") and scan it,
# importing what is already in ~mods into the default profile.
modde scan --game stellar-blade --import-to default
# 2. Install a pak mod from a Nexus URL into a profile.
modde install mod https://www.nexusmods.com/stellarblade/mods/<id> --profile default
# 3. Deploy the active profile into SB/Content/Paks/~mods.
modde deploy --game stellar-blade --profile default
# 4. Add upscaling/frame-gen via the curated OptiScaler profile.
modde tool enable optiscaler --game stellar-blade
modde tool configure optiscaler --game stellar-blade optiscaler_profile=community-dxgi
modde tool apply optiscaler --game stellar-blade
# 5. (Optional) Register a named external tool/executable for this game so it runs
# with overwrite capture and the right Wine DLL overrides.
modde tool add-executable --game stellar-blade --name my-tool /path/to/tool.exe
modde exec my-tool --game stellar-blade
Home-Manager profile
The home-manager module exposes Stellar Blade as a declarative profile. The
tools.optiscaler.profile option is type-checked against the registered profile list,
so community-dxgi is the valid value for this game.
{
programs.modde = {
enable = true;
profiles.stellar-blade-main = {
game = "stellar-blade";
# Auto-detected from Steam; set explicitly if your library is non-standard.
# gameDir = "/path/to/SteamLibrary/steamapps/common/Stellar Blade";
installMode = "auto";
tools.optiscaler = {
enable = true;
profile = "community-dxgi";
};
};
};
}
On activation modde enables, configures, and applies the OptiScaler profile for the
stellar-blade game, writing the dxgi.dll proxy into SB/Binaries/Win64. Remember to
launch the game once first so the Proton prefix exists.
See also
- Supported games
- Subnautica 2 — the other data-driven UE title
- Oblivion Remastered — another UE pak-based game
- Mod scanning
- Conflicts & load order
- Deployment & VFS
- Save management
- Tools & overlays
- FOMOD installer
- MO2 parity & capability audit
Subnautica 2
Subnautica 2 (game_id = subnautica2) is one of modde’s Unreal Engine titles. It
shares the data-driven UE4 plugin with Stellar Blade and Oblivion Remastered: a
single Ue4Game definition parameterised per title, with the trait
implementation reused across every Unreal game. Its overall status is Partial —
the engine path (detection, deployment, conflict classification, save tracking,
Wine/Proton DLL overrides) is wired and shipped, but there is no bespoke
Subnautica-specific scanner, save format parser, or mod-loader integration beyond
the generic pak layout. Treat it as “the UE4 pipeline pointed at Subnautica 2”,
not a hand-tuned per-game experience.
Status baseline. The authoritative per-game status lives in Supported games and in
docs/capability-matrix.toml. This page expands on the engine mechanics; it does not change the status.
Engine and overall status
| Aspect | Value |
|---|---|
game_id | subnautica2 |
| Engine family | Unreal4 (covers UE4 and UE5 pak/IoStore titles) |
| UE project name | Subnautica2 |
| Steam App ID | 1962700 |
| Nexus domain | subnautica2 |
| Overall status | Partial |
| Scanner | Yes (shared UE4 pak scanner) |
| Conflict detection | Yes (shared UE4 collision policy) |
| Save tracking | Done (Proton-prefix .sav capture) |
| Save profiles | Enabled (with_save_profiles(true)) |
| OptiScaler profile | None shipped |
The Unreal4 engine family is modde’s umbrella for Unreal pak/IoStore games
regardless of whether the title is technically UE4 or UE5 — the on-disk layout
(<Project>/Content/Paks/, <Project>/Binaries/Win64/, ~mods mount ordering,
pak/ucas/utoc chunk triples) is the same, so the same code path drives both.
How modde detects the install
Subnautica 2 is registered with Steam launcher IDs only:
| Launcher field | Value |
|---|---|
steam_app_id | 1962700 |
steam_dir | Subnautica2 |
heroic_gog_app_id | none |
heroic_epic_app_id | none |
Detection walks your Steam libraries (parsed from libraryfolders.vdf, so extra
drives are covered) and looks for steamapps/common/Subnautica2. There is no
Heroic (GOG/Epic) mapping for this title today, so a non-Steam copy must be
pointed at explicitly with --game-dir (CLI) or gameDir (home-manager). Run
modde detect to see what modde resolves before you commit a profile to it.
The install root is the directory that contains the UE project folder
Subnautica2/. modde derives everything else from there:
- Paks root:
<install>/Subnautica2/Content/Paks - Executable / proxy-DLL dir:
<install>/Subnautica2/Binaries/Win64
Proton prefix
Saves and per-user config live inside the Proton prefix Steam creates for App ID
1962700, not in the install directory. modde resolves the prefix as
<steam_library>/steamapps/compatdata/1962700/pfx. If you have never launched the
game, that prefix does not exist yet and modde’s save/config resolution returns
nothing — the fix is to launch Subnautica 2 once through Steam (Proton) so the
prefix is created, then re-run modde.
Mod directory and deploy strategy
modde deploys mods into the tilde-prefixed ~mods folder under the paks root:
<install>/Subnautica2/Content/Paks/~mods/
The leading tilde is deliberate: Unreal’s pak mounter loads ~mods after the
shipping paks, so modded content overrides base game content rather than the other
way around. modde creates this directory if it does not exist.
Deployment uses modde’s standard staging-and-link model (see
Deployment): mods live in per-mod staging directories,
and the active profile is materialised into ~mods by linking the enabled mods’
files. A pak/ucas/utoc triple that shares a file stem is treated as one
logical mod — the IoStore container (.ucas/.utoc) and its .pak header travel
together.
When modde analyses a downloaded archive, it recognises the Subnautica 2 layout
when the archive root contains at least one .pak, .ucas, or .utoc file and
classifies it as a single file set (InstallMethod::SingleFileSet) — i.e. “drop
these chunk files into ~mods”. Archives that do not present pak files at their
root fall through to the generic installer flow.
What scanning finds
modde scan --game subnautica2 walks two directories under the paks root and
groups .pak/.ucas/.utoc files by file stem into discovered mods:
| Scanned subdirectory | Source location label |
|---|---|
Subnautica2/Content/Paks/~mods | paks-mods |
Subnautica2/Content/Paks/LogicMods | logic-mods |
LogicMods is included because UE4SS-style Blueprint/logic mods conventionally
ship there; modde reports anything it finds in that folder even though it deploys
its own managed mods into ~mods. Each discovered mod gets an ID of the form
pak/<stem> and is recorded at a confidence of 0.9. Sibling .ucas/.utoc
files collapse into the same row as their .pak (the footprint is represented by
subnautica2/content/paks/~mods/<stem>.pak), so a multi-chunk mod shows up once
rather than three times.
Scanning is filesystem discovery only — it tells you which pak sets are physically present. It does not parse pak contents, read UE asset trees, or resolve load-order semantics. See Scanning for how discovered rows reconcile with manifest-driven installs.
Conflict classification
Conflicts are classified by the shared UE4 collision policy. Two mods “collide” when they ship a file at the same relative path; the severity of that overlap is decided by file extension:
| Extension | Severity | Meaning |
|---|---|---|
pak, ucas, utoc | Dangerous | Overlapping game-content chunks — last-writer wins and the loser’s assets are masked |
dll, lua | Dangerous | Code/script overlap (proxy DLLs, UE4SS Lua) |
ini, cfg, json, toml, xml, yaml | Config | Configuration overlap — usually mergeable/tunable by hand |
dds, png, jpg, tga | Cosmetic | Texture overlap — visual only |
The archive extensions that participate in pak-aware collision handling are
pak, ucas, and utoc. Because Subnautica 2 mods are predominantly pak sets,
most real conflicts you will see are Dangerous same-path pak overlaps: two mods
both shipping, say, ~mods/zMyOverhaul.pak cannot coexist without one shadowing
the other. modde surfaces these so you can choose a winner via load order /
filename prefixing rather than discovering it in-game. See
Conflicts for the full classification model and how to
resolve overlaps.
Save tracking
Save tracking is shipped (Done) and Subnautica 2 opts into modde’s per-profile
save layer (supports_save_profiles = true).
Location
modde resolves the save directory inside the Proton prefix:
<steam_library>/steamapps/compatdata/1962700/pfx/drive_c/users/steamuser/
AppData/Local/Subnautica2/Saved/SaveGames
If the prefix does not exist yet (game never launched), save resolution returns nothing — launch once through Proton first.
What is captured and fingerprinted
The UE4 save tracker is a pattern tracker that recursively matches *.sav files
under the save directory, labels each by its file stem, and files them under the
manual category. “Recursive” matters for Subnautica 2 because UE titles often
nest per-slot save folders; modde descends into them.
Captured saves are committed into a per-game git vault (one repository per
game_id, one branch per profile), which gives history, branching, and rollback
for free. Each capture commit also records a mod fingerprint:
- The fingerprint is a SHA-256 over the sorted, de-duplicated list of enabled, save-breaking mod IDs in the active profile.
- A mod is “save-breaking” if it ships any of the save-affecting extensions
pak,ucas,utoc,dll, orlua— i.e. exactly the content types that can change game logic. Purely cosmetic mods (png,jpg,dds,tga,ini) do not move the fingerprint, so two profiles that differ only in textures stay compatible. - The fingerprint and a human-readable mod list are stored as
Mod-Fingerprint:/Save-Breaking-Mods:git trailers on the capture commit.
When you later restore a snapshot, modde compares the stored fingerprint against the current profile and warns if the save-breaking mod set has changed (which mods were added/removed), so you do not silently load a save into an incompatible mod configuration. See Saves for the capture/restore/rollback workflow.
Note: the file content itself is not hashed — the fingerprint identifies the mod configuration that produced the save, not the bytes of the save file.
Per-user config deploy target
Beyond saves, the UE4 plugin exposes one deploy target for user-editable config:
| Target ID | Label | Resolves to |
|---|---|---|
ue4-saved-config | UE4 Saved/Config | compatdata/1962700/pfx/drive_c/users/steamuser/AppData/Local/Subnautica2/Saved/Config/Windows |
This lets modde place INI tweaks into the prefix’s Saved/Config/Windows
directory. As with saves, the target resolves to nothing until the Proton prefix
exists.
Plugin and load-order handling
Unreal pak games have no plugins.txt-style master/plugin list the way Bethesda
titles do. Load order is filename/mount order within ~mods: paks mount in a
deterministic order, and ~mods mounts after the base paks. There is no Subnautica
2 load-order editor in modde; if two pak sets conflict, you resolve it by choosing
which mod wins (renaming with a sorting prefix, or disabling one). modde’s job here
is to make the conflict visible and the deployment reproducible — not to arbitrate
UE pak precedence automatically.
Installer specifics
Subnautica 2 mods are pak-based, so there is no FOMOD/REDmod/SMAPI flow specific to this title:
- Pak sets are the native format. modde’s archive analysis detects a root-level
.pak/.ucas/.utocand deploys the set into~mods. - FOMOD installers are still supported generically (the FOMOD engine is engine-agnostic), but most Subnautica 2 mods are plain pak archives that need no scripted installer. See FOMOD installers if you do hit one.
- No REDmod / no SMAPI / no Bethesda plugin tooling applies here — those belong to Cyberpunk and Stardew/Creation-Engine titles respectively.
Gaming tools that matter
modde manages external gaming tools per game (see Tools). For Subnautica 2:
-
Proton is the runtime. modde’s
protontool selects the Proton build and can apply DLL overrides; everything below the executable runs inside the App ID1962700prefix. -
Wine DLL overrides for proxy DLLs. UE mod loaders (UE4SS) and overlays ship as proxy DLLs that must be loaded as native instead of Wine’s built-in stub. modde scans
Subnautica2/Binaries/Win64/(or a mod’s staging dir) for known proxies and emits the rightWINEDLLOVERRIDES. The recognised proxy DLLs are:Proxy DLL Typical use dwmapiUE4SS default proxy xinput1_3UE4SS alternate / some trainers d3d11ReShade / ENB-style dxgiReShade / DLSS swappers versionGeneric ASI loader winmmGeneric ASI loader dinput8Generic hook -
No OptiScaler profile ships for Subnautica 2. Unlike Stellar Blade — which has a community-tested
community-dxgiOptiScaler preset —subnautica2registers an empty OptiScaler profile list. You can still enable the genericoptiscalertool if you know what you are doing, but modde provides no curated, game-specific preset for this title, so there is noprofile = "..."value to select. Overlays like MangoHud, VkBasalt, and GameMode are engine-agnostic and work as for any Proton title.
Linux / Proton notes and gotchas
These notes cover the game’s runtime on Linux and macOS, where Subnautica 2 runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, and saves and per-user config live at their native Windows locations. modde itself runs natively on all three platforms.
- Launch once before modding state resolves. Saves, the
ue4-saved-configtarget, and prefix-based config all require the Proton prefix to exist. A fresh install has nocompatdata/1962700until you run the game once. ~modslives in the install, saves live in the prefix. These are two different trees on disk. Deploy/scan operate on<install>/Subnautica2/Content/Paks; save capture operates on the prefix. Do not look for saves under the install directory.- IoStore triples must stay together. Deploying or removing a
.pakwithout its.ucas/.utoc(or vice versa) produces a broken mount. modde groups them by stem, so let modde manage the set rather than hand-copying single files. - Browse Nexus picker. Subnautica 2 has a Nexus domain (
subnautica2) but no numeric Nexus game ID recorded, so URL-based installs and update tracking work while the UI’s “Browse Nexus” game picker may hide the title until the numeric ID is confirmed. You can still install from anexusmods.com/subnautica2/mods/<id>URL ornxm://link. - Verify your runtime against the game’s current state. Subnautica 2 is an evolving title; pak mods can break across game patches. Capture a save before a big mod change so the fingerprint lets you roll back cleanly.
Worked example
Home-Manager profile
A minimal declarative Subnautica 2 profile. Point gameDir at the Steam install
(the directory containing Subnautica2/), and let activation manage the rest.
programs.modde = {
enable = true;
profiles.subnautica2-modded = {
game = "subnautica2";
installMode = "auto";
gameDir =
"/home/me/.local/share/Steam/steamapps/common/Subnautica2";
tools = {
# Engine-agnostic overlays work fine on this Proton title.
mangohud.enable = true;
gamemode.enable = true;
# No OptiScaler preset ships for subnautica2, so there is no
# `profile = "..."` to set here.
};
};
};
If the game is not installed yet, omit gameDir or set
installMode = "await-game"; activation prints the next step instead of failing.
See the Home-Manager module reference for every
option.
CLI
# See what modde resolves for the install and Proton prefix.
modde detect
# Create a profile and scan the pak directories for existing mods.
modde profile create modded --game subnautica2
modde scan --game subnautica2 --import-to modded
# Install a pak mod from a Nexus URL into the profile.
modde install mod "https://www.nexusmods.com/subnautica2/mods/123" \
--profile modded
# Deploy the active profile's mods into Content/Paks/~mods.
modde deploy --game subnautica2 --profile modded
# Switch profile, deploy, capture the outgoing profile's saves, and launch.
modde play modded --game subnautica2
If your copy is not a default Steam install, add --game-dir <path> to the
commands that take it (scan, deploy), pointing at the directory that contains
Subnautica2/.
Named executables (modding tools)
modde can manage named launch targets — UE4SS-driven tools, trainers, or any helper
executable — with overwrite capture, custom working dir, environment, and Wine DLL
overrides. The exec commands are a thin alias for the executable subset of tool;
the two share storage.
# Register a tool as a named launch target for subnautica2.
modde exec add my-tool "/path/to/Tool.exe" --game subnautica2 \
--wine-dll-overrides "dinput8=n,b" \
--env "SOME_VAR=1" \
-- --tool-arg value
# Equivalent via the tool surface:
modde tool add-executable my-tool "/path/to/Tool.exe" --game subnautica2
# Run it, capturing any files it writes into the overwrite mod.
modde exec run my-tool --game subnautica2 --profile modded
Captured output defaults to the __overwrite__ mod; pass --output-mod <name> to
route writes into a named mod instead. See Tools and
Playing for the full launch model.
See also
- Supported games — the canonical status table and per-game guide index
- Stellar Blade — sibling Unreal title (has an OptiScaler preset)
- Oblivion Remastered — another
Unreal4pak title - Deployment — staging-and-link deployment model
- Scanning — filesystem discovery and manifest reconciliation
- Conflicts — collision classification and resolution
- Saves — capture, fingerprints, restore, and rollback
- Tools — Proton, overlays, and named executables
- Playing — switch-deploy-capture-launch flow
- Home-Manager module — declarative profile options
- Parity reference — how modde’s coverage maps to MO2/Vortex
Baldur’s Gate 3
Baldur’s Gate 3 (baldurs-gate3) runs on Larian Studios’ Divinity engine and is
modelled in modde as the Larian engine family. Mods are distributed as .pak
archives, the enabled module list lives in modsettings.lsx, and the writable
game data lives inside the Steam Proton prefix rather than the install
directory. modde ships built-in support for all of these.
Overall status
Baldur’s Gate 3 is Partial. Deployment, scanning, conflict
classification, save tracking, and modsettings.lsx load-order generation are
wired and reachable, but several BG3-specific workflows are deliberately
conservative and there is no FOMOD-grade installer UI for the game’s
.pak-with-info.json ecosystem. Treat it as solid for .pak-based content
and load order, and verify by hand for anything that touches the Script
Extender or non-standard archive layouts.
| Capability | Status | Notes |
|---|---|---|
| Install detection | Done | Steam app id 1086940, Steam dir Baldurs Gate 3 |
| Mod directory resolution | Done | Proton-prefix Mods/ with install-relative fallback |
| Scanner | Partial | Discovers root .pak mods under Mods/ |
| Conflict detection | Partial | pak-aware collision classifier |
| Save tracking | Done | .lsv saves, git-backed vault |
| Load order | Partial | Generates modsettings.lsx from deployed .pak files |
The canonical status baseline for every game lives in
docs/capability-matrix.toml; this page describes how the BG3 plugin behaves,
not a promise beyond that matrix.
Install detection
modde locates the install through its launcher IDs:
| Launcher | Identifier |
|---|---|
| Steam app id | 1086940 |
| Steam library dir | steamapps/common/Baldurs Gate 3 |
| Heroic (GOG/Epic) | not registered |
Only Steam is registered for BG3 today. The GOG and Epic Heroic app ids are not
populated, so if you run a non-Steam copy you must point modde at the install
yourself with --game-dir (CLI) or gameDir (home-manager).
Mod directory and path resolution
The writable BG3 data root is not in the install directory. Under Proton it
lives inside the prefix that Steam creates for app 1086940. modde resolves the
data root by walking up from the install path to the steamapps/common
directory and then descending into the matching compatdata prefix:
steamapps/compatdata/1086940/pfx/drive_c/users/steamuser/AppData/Local/Larian Studios/Baldur's Gate 3/
From that data root, modde derives:
| Purpose | Path (relative to the data root above) |
|---|---|
| Mod directory | Mods/ |
| Load order file | PlayerProfiles/Public/modsettings.lsx |
| Save directory | PlayerProfiles/Public/Savegames/ |
If modde cannot find a steamapps/common ancestor (for example a manually
specified non-Steam path), it falls back to an install-relative
Larian Studios/Baldur's Gate 3 data root and the same sub-paths under it.
Directory matching for the bare-layout heuristics is case-insensitive, so
Mods / mods and PlayerProfiles / playerprofiles are both recognised.
The deploy strategy is modde’s standard symlink farm: .pak files are staged
and symlinked into the resolved Mods/ directory, leaving the original install
untouched and fully reversible. See Deployment & VFS
for the build → materialize → deploy pipeline.
What scanning finds
The BG3 scanner discovers root .pak files inside Mods/. It prefers the
Proton-prefix data root, but if that prefix has no Mods/ directory yet it
falls back to scanning the install directory’s Mods/ instead, so a fresh
prefix does not silently produce an empty scan.
- It looks only at the
Modsdirectory (single-file.pakrule). - Each discovered mod is reported with a
pak/mod-id prefix and a confidence of0.95. - A mod’s on-disk footprint is the lower-cased
mods/<stem>.pakfile, which is what conflict detection compares between mods.
Scanning is shallow by design: it enumerates the .pak archives that BG3 itself
loads, not the contents inside each archive. There is no MO2-style file-tree or
per-archive INI inspection for BG3, which is part of why the title is Partial.
See Scanning for the general model.
Conflict classification
BG3 uses modde’s pak-aware collision classifier. Two mods are flagged when
they ship the same relative file path, and the severity is decided by file
extension:
| Severity | Extensions | Meaning |
|---|---|---|
| Cosmetic | dds, png, jpg, tga, nif | Visual overlap; last writer wins |
| Config | ini, json, xml | Settings overlap; review which wins |
| Dangerous | esp, esm, dll, lua, ws | Code/plugin overlap; resolve before playing |
pak, ucas, and utoc are treated as archive extensions for collision
purposes, so two mods packing identically named loose files are compared rather
than two opaque archives being declared incompatible outright. Resolution
follows the load order: the later mod wins each conflicting path. See
Conflicts for how to inspect and resolve them.
Save-breaking content
Separately from collision severity, the BG3 plugin classifies individual mods
for save safety. A mod is treated as save-breaking when it contains
.pak, .dll, .json, or .lsx files, or when it ships a Script Extender
directory (Script Extender / ScriptExtender). Purely cosmetic mods (.png,
.jpg, .dds, .tga) are treated as safe. This classification is what feeds
the save fingerprint described below, so adding or removing a Script Extender
mod is correctly surfaced as a save-compatibility change.
Save tracking
Save tracking is Done. BG3 stores saves as .lsv files under:
PlayerProfiles/Public/Savegames/
modde’s BG3 save tracker:
- Matches
**/*.lsvrecursively under the save directory (each campaign save is its own folder containing an.lsv, so recursion is required). - Categorises every detected save as
manualand labels it by its path relative to the save directory. - Sorts captures newest-first by modification time.
Saves are stored in a per-game git-backed vault at
~/.local/share/modde/saves/baldurs-gate3/, with one branch per profile. Each
snapshot embeds a SHA-256 fingerprint of the save-breaking mods (the .pak
/ .dll / .json / .lsx / Script-Extender content described above), so
restoring a save into a profile whose dangerous mods differ raises a
compatibility warning instead of silently loading an incompatible save. See
Save Management for vault mechanics, adoption, and
restore.
Load order and modsettings.lsx
BG3 reads its enabled module list from an LSX (Larian XML) file at
PlayerProfiles/Public/modsettings.lsx. modde manages this file for you:
- After every deploy, the plugin’s post-deploy step reads the deployed
Mods/directory, collects the file stem of each.pak, sorts the list, and writes a freshmodsettings.lsxenabling those modules in order. If no.pakfiles are present, the existing file is left untouched. - The generated XML uses the standard
ModuleSettingsregion with oneModuleShortDescnode per module, keyed on theFolderattribute. - modde can also read an existing
modsettings.lsx, parsing the ordered list of enabled module folders back out, which is how it reconciles against what the game currently has enabled.
The important limitation: the generated order is the sorted .pak stem
order, not a dependency-aware load order. For modlists whose .pak stems do
not encode the intended order (BG3 mods frequently need a specific sequence and
carry UUID/dependency metadata in their meta.lsx), you should review and
adjust modsettings.lsx — or use BG3 Mod Manager in the prefix — after
deploying. modde does not parse per-mod meta.lsx UUIDs or dependency
declarations.
Installer specifics
BG3 mod packaging is varied, and modde handles the two layouts that map cleanly to a symlink farm:
| Archive shape | What modde does |
|---|---|
One or more .pak files at the archive root | Installs as a single file set (the .paks land directly in Mods/) |
A top-level Mods/ directory in the archive | Strips the Mods/ content root so its contents map to the game’s Mods/ |
A bare extracted layout is recognised as a BG3 mod when it contains mods or
playerprofiles directories, or root .pak / .lsx files (case-insensitive).
There is no FOMOD installer path for BG3 here — FOMOD is an XML installer
format used by Bethesda-style mods, and BG3’s .pak-with-info.json packaging
is not FOMOD. There is likewise no REDmod (Cyberpunk) or SMAPI (Stardew)
handling, because those belong to other engines. Archives that bundle a
loose-file overlay alongside .paks, or that expect a manual info.json
merge, are not auto-resolved; install those by hand and re-run a deploy.
Gaming tools and Proton
The BG3 registration ships no OptiScaler profile, so there is no curated
DLSS/FSR/XeSS preset for this title the way there is for, say, Stellar Blade. You
can still enable the generic tools per profile — mangohud, vkbasalt,
gamemode, reshade, and especially proton — from the
Tools & Overlays guide and the
home-manager module.
The proton tool is the relevant one for BG3: it manages the per-game Proton
runtime selection, Wine prefix, environment, and DLL overrides. Because BG3’s
mod directory and load-order file live inside the Proton prefix, the prefix
that proton targets must be the same one your Mods/ and modsettings.lsx
resolve into.
Linux and Proton notes (gotchas)
These notes cover the game’s runtime on Linux and macOS, where Baldur’s Gate 3
runs through Proton/Wine and its writable data lives inside the compatibility
prefix. On Windows the game runs natively — there is no Wine prefix, and Mods/,
modsettings.lsx, and saves live at their native Windows locations. modde itself
runs natively on all three platforms.
- The mod directory is inside the prefix, not the install. This is the most
common surprise: editing files under
steamapps/common/Baldurs Gate 3does not affect the modded game. modde resolves the prefix path for you, but if you ever inspect things manually, look undercompatdata/1086940/pfx/.../AppData/Local/Larian Studios/Baldur's Gate 3/. - The prefix must exist before scanning the prefix data root. If you have
never launched BG3 through Proton, the
compatdata/1086940prefix may not have aMods/directory yet; the scanner falls back to the install directory, but load-order generation and saves only become meaningful once the prefix is populated. Launch the game once through Steam first. - Script Extender is save-breaking. BG3’s Script Extender lands as a
Script Extenderdirectory and is classified as dangerous/save-breaking. Toggling it changes the save fingerprint, so expect a compatibility warning if you restore older saves after enabling or disabling it. Script Extender itself is injected via the prefix and is not something modde installs for you. modsettings.lsxordering is heuristic. As noted above, modde writes a sorted-stem order; if your list needs a specific sequence, adjust the file (or use a BG3-specific load-order tool inside the prefix) after deploying.- Steam-only detection. Non-Steam copies need an explicit game directory.
Worked example
Home-manager profile
programs.modde = {
enable = true;
# Optional: needed for Nexus downloads / update tracking.
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.bg3-mods = {
game = "baldurs-gate3";
installMode = "auto";
# Point at the Steam install. modde derives the in-prefix Mods/ and
# modsettings.lsx from this path automatically.
gameDir = "/home/me/.local/share/Steam/steamapps/common/Baldurs Gate 3";
tools = {
gamemode.enable = true;
# Keep modde's prefix targeting aligned with the game's actual prefix.
proton.enable = true;
};
};
};
If BG3 is not installed yet, omit gameDir or set
installMode = "await-game"; activation prints the next step and continues
without failing. Set gameDir and rebuild once Steam has the game.
CLI
# Scan the resolved in-prefix Mods/ directory for installed .pak mods.
modde scan --game baldurs-gate3
# Install a mod archive (root .pak files or a top-level Mods/ folder).
modde install ./SomeBg3Mod.zip --game baldurs-gate3
# Deploy the profile. This symlinks .pak files into the prefix Mods/ and
# regenerates PlayerProfiles/Public/modsettings.lsx from what was deployed.
modde deploy --game baldurs-gate3
# Inspect conflicts between deployed mods.
modde conflicts --game baldurs-gate3
# Adopt existing .lsv saves into a profile vault.
modde save adopt --game baldurs-gate3 --profile bg3-mods
After deploying, review PlayerProfiles/Public/modsettings.lsx if your modlist
relies on a specific load order, since modde generates a sorted-stem order
rather than a dependency-resolved one.
See also
- Supported Games — the status matrix and the full per-game guide index
- Deployment & VFS — the symlink-farm pipeline
- Scanning — how installed mods are discovered
- Conflicts — inspecting and resolving collisions
- Save Management — git-backed vaults, fingerprinting, restore
- Tools & Overlays — Proton, OptiScaler, and overlays
- Home-Manager Module — declarative profile options
- Feature parity — how modde compares to MO2/Vortex
Stardew Valley
Stardew Valley is modde’s SMAPI-engine title. It is the only game in the registry
on the Smapi engine family, and unlike the Bethesda, REDengine, and Unreal games
it is a managed-content loader: mods are self-describing directories under Mods/
rather than archives, plugins, or .pak overlays. This page documents exactly
what modde knows about the title, sourced from
crates/modde-games/src/stardew/.
Engine and overall status
| Property | Value |
|---|---|
game_id | stardew-valley |
| Display name | Stardew Valley |
| Engine family | Smapi |
| Loader | SMAPI (Stardew Modding API) |
| Steam App ID | 413150 |
| Nexus domain | stardewvalley |
| Overall status | Partial |
| Scanner | Yes |
| Conflict detection | Yes (generic policy classifier) |
| Save tracking | Done |
The Partial rating matches the supported-games matrix:
the core path — detection, the SMAPI mod layout, scanning, conflict
classification, and save tracking — is implemented, but it has not been promoted
to Done the way the Bethesda Creation Engine titles and Cyberpunk 2077 have.
Treat installer coverage and end-to-end UX as work in progress, not as a turnkey
SMAPI manager. The canonical, test-coupled status baseline lives in
docs/capability-matrix.toml.
How modde detects the install
modde resolves the Stardew Valley install through its launcher IDs. The
registration carries a Steam App ID of 413150 and a Steam library directory name
of Stardew Valley, so a Steam install is located by walking your Steam libraries
for steamapps/common/Stardew Valley. No Heroic GOG or Epic IDs are registered for
this title, so Heroic auto-detection is not wired up — point modde at the install
directory directly if you run it from GOG or another launcher.
Stardew Valley runs natively on Linux. The default Steam Linux build is a native Linux binary, so there is no Proton prefix involved for the base game, and modde does not need to resolve a Wine prefix to find the install or the saves. (You can still choose to run the Windows build under Proton — see Linux/Proton notes — but that is not the default path.)
You can always override detection and pass the resolved game directory explicitly
when a command needs it; the game directory is the folder that contains the
Stardew Valley executable and the Mods/ directory.
Mod directory and deploy strategy
modde’s plugin resolves the mod directory to Mods/ directly under the install
root (<install>/Mods). This is SMAPI’s standard mod folder: SMAPI loads every
subdirectory of Mods/ that contains a manifest.json.
Deployment uses modde’s standard symlink-farm VFS — the same pipeline as every
other game. Mods are staged in ~/.local/share/modde/staging/<profile>/, the
winning file for each relative path is resolved by load-order priority, and the
staging tree is symlinked into Mods/. Your real Stardew install is never mutated
in place, so deactivating modde or switching profiles is fully reversible. The
plugin’s executable_dir is the install root itself, which is where overlay-style
tools (proxy DLLs) land when applicable.
See Deployment & VFS for the full pipeline and rollback behaviour.
What scanning finds
modde scan --game stardew-valley discovers SMAPI mods already on disk. The
scanner walks the Mods/ directory and applies a directory-mod rule:
- Each immediate subdirectory of
Mods/is a candidate mod. - The presence of a
manifest.jsonmarker file inside a candidate raises detection confidence to0.98— that file is SMAPI’s own mod manifest and is the authoritative signal that a directory is a SMAPI mod. - Directories without the marker are still reported at a lower base confidence
(
0.75), so loose content packs are not silently dropped. - Discovered mods are namespaced under a
smapi/mod-id prefix, and a mod’s on-disk footprint is recorded asmods/<name>/(lower-cased), reflecting that SMAPI directory names are matched case-insensitively.
Import discovered mods straight into a profile:
modde scan --game stardew-valley --import-to my-stardew
This is the path for adopting an existing hand-managed Mods/ folder into modde.
See Mod Scanning for dry-run mode and threshold filtering.
Conflict classification specifics
Stardew Valley uses modde’s generic policy collision classifier (it has no bespoke per-game collision classifier the way Bethesda and Cyberpunk do). The generic classifier registers no archive extensions — SMAPI mods are loose directory content, not packed archives — and applies the default severity table to overlapping files:
| Extension | Severity |
|---|---|
dll, lua | Dangerous (critical) |
json, xml, ini | Config |
png, jpg, dds, tga, nif | Cosmetic |
In practice that means two content packs that both ship assets/foo.png collide
as a cosmetic conflict (last in load order wins), while two mods that both
ship a SMAPI .dll or a Content Patcher-style content.json are flagged at the
more serious end. Resolve conflicts by load-order priority exactly as for any other
game:
modde collisions --profile my-stardew
modde collisions --profile my-stardew --all # include cosmetic conflicts
Separately from collision severity, the plugin also classifies each mod for save safety, which feeds save fingerprinting (next section):
| Class | Extensions |
|---|---|
| Save-breaking | dll, json, xnb, tmx, tbin |
| Cosmetic (save-safe) | png, jpg, xnb |
A mod containing any save-breaking extension is classified SaveBreaking; a mod
that contains only cosmetic files is SaveSafe; an empty or unrecognised mod is
Unknown and treated conservatively as save-breaking for fingerprinting. Note that
xnb appears in both lists — it is content that can be either gameplay data
(save-breaking) or a recolour (cosmetic), and modde errs toward the save-breaking
reading because any save-breaking extension in the mod wins. For per-file content
categorisation, extensions map as: dll → binary, json/tmx/tbin → config,
xnb → archive, png/jpg → texture.
See Conflicts & Load Order for severity meanings and the hide workflow.
Save tracking
Save tracking is Done for Stardew Valley. The save directory is the native Linux XDG config location:
~/.config/StardewValley/Saves
modde resolves this through its platform-aware config-directory helper, so on Linux
it honours $XDG_CONFIG_HOME (defaulting to ~/.config). Each farm is its own
subdirectory named <FarmName>_<id> (e.g. Pelican_123456789).
The save tracker enumerates directories under the saves folder whose name
contains an underscore — that is the <name>_<id> farm convention — and reports
each as a save in the farm category, labelled by its directory name. The
startup_preferences entry is excluded. Results are sorted newest-first by
modification time.
supports_save_profiles is true, so Stardew participates in modde’s full
git-backed save vault: each profile is a branch, switching profiles captures and
restores the appropriate farm snapshots, and every snapshot embeds a SHA-256
fingerprint computed from the sorted list of enabled save-breaking mods (the
classification above). On restore, if the snapshot’s fingerprint does not match
your current profile, modde warns you which save-breaking mods were added or
removed — so you do not load a farm into a mod set it was never saved against.
# Adopt an existing farm into a profile
modde save adopt --game stardew-valley --profile my-stardew
# Capture, browse history, and restore
modde save capture --game stardew-valley --profile my-stardew -m "year 2 spring"
modde save history --game stardew-valley --profile my-stardew
modde save restore abc12345 --game stardew-valley --profile my-stardew
See Save Management for the vault model, auto-capture, and the fingerprint trailer format.
Plugin and load-order handling
Stardew Valley has no Bethesda-style plugin (.esp/.esm) load order. SMAPI
loads mods by their manifest.json and resolves inter-mod dependencies itself at
runtime. modde therefore does not manage an external plugin/load-order file for
this title; ordering is handled entirely through modde’s standard mod load-order
priority (later mods win file conflicts during deployment), which is what the
collision classifier and the VFS builder consume. There is no separate plugin
table to edit.
Installer specifics
There are no FOMOD, REDmod, BAIN, or .pak flows for Stardew Valley — those are
Bethesda/Cyberpunk/Unreal concepts. The plugin recognises two SMAPI-native archive
layouts when you install a downloaded mod:
- SMAPI directory mod — if the extracted archive root contains a
manifest.json, the archive is one SMAPI mod. It installs as aDirectoryMod(the whole directory is staged under a stable mod directory). This is the common case for a single SMAPI mod packaged at the archive root. Mods/-rooted archive — if the extracted root instead contains aMods/directory, modde strips that content root (StripContentRoot { root: "Mods" }) so the inner mod folders stage directly into the game’sMods/. This handles archives that bundle one or more mods already nested under aMods/folder.
For bare-layout recognition (used when an archive has no obvious nesting), the
plugin accepts a root manifest.json, or a mods/ directory, or root files with a
json/dll extension; directory matching is case-insensitive. Anything modde
cannot classify falls through to the generic installer, which writes a dossier so
the layout can be handled later — it is not silently discarded. See the
FOMOD Installer guide for the interactive/declarative
installer machinery that the Bethesda titles use; Stardew does not need it.
Gaming tools that matter
Stardew Valley registers no OptiScaler profiles — it is a 2D pixel-art game, so DLSS/FSR upscaling is not relevant, and there is no Unreal-style upscaler swap to manage. The tools that matter here are modde’s general overlay/launcher tools:
- MangoHud / GameMode — useful as always for an FPS overlay and CPU governor tuning, even on a lightweight title.
- Proton — only relevant if you deliberately run the Windows build under Proton
(see below). The
protontool stores launch/compat settings; it does not install Proton or the game.
See Tools & Overlays for enabling and configuring these.
Linux and Proton notes
- Native Linux is the default and the happy path. The Steam Linux depot ships a
native build, SMAPI has a first-class Linux installer, and the save directory is
the native
~/.config/StardewValley/Saves. No Wine prefix is involved, and modde does not look for one. - SMAPI itself must be installed and set as the launch command. modde manages
your
Mods/content; it does not install or bootstrap SMAPI. After running SMAPI’s installer, set Stardew’s Steam launch options to run SMAPI (the standardStardewModdingAPI %command%pattern) so the loader actually starts. - If you run the Windows build under Proton on Linux instead, the save directory
moves into the Proton prefix’s emulated
%APPDATA%(steamapps/compatdata/413150/pfx/drive_c/users/steamuser/AppData/Roaming/StardewValley/Saves) rather than~/.config/StardewValley/Saves. On Linux modde’s save tracker targets the native Linux path, so prefer the native Linux build to keep save tracking accurate; running the Windows build under Proton on Linux is a known gotcha for the save vault. (On Windows itself, where the native build is the only option, the tracker targets the native Windows save path, and on macOS the native build’s config path.) modde itself runs natively on Linux, macOS, and Windows. - Case sensitivity. SMAPI mod directory names are matched case-insensitively by modde’s scanner, which is the right behaviour for mods authored on Windows but deployed onto a case-sensitive Linux filesystem.
Worked example
Home-Manager profile
Declare a Stardew profile in your home-manager configuration. You can declare it before the game is installed and let modde wait for the Steam install:
{ inputs, ... }:
{
imports = [ inputs.modde.homeManagerModules.modde ];
programs.modde = {
enable = true;
profiles.my-stardew = {
game = "stardew-valley";
# Wait for Steam to install the game, then set gameDir and rebuild.
installMode = "await-game";
};
};
}
Once Steam has installed the game, set gameDir to the resolved install path and
rebuild; modde deploys the profile through its activation script. See the
Home-Manager module reference for the full option
set.
CLI
# 1. Adopt an already-managed Mods/ folder into a profile
modde scan --game stardew-valley --import-to my-stardew
# 2. Inspect conflicts (cosmetic png overlaps are expected and harmless)
modde collisions --profile my-stardew --all
# 3. Deploy the symlink farm into <install>/Mods
modde deploy --profile my-stardew --game stardew-valley
# 4. Adopt an existing farm and capture a baseline snapshot
modde save adopt --game stardew-valley --profile my-stardew
modde save capture --game stardew-valley --profile my-stardew -m "baseline"
# 5. Play (SMAPI must already be set as the launch command); saves capture on exit
modde play --game stardew-valley
See also
- Supported Games
- Baldur’s Gate 3 — the other non-Bethesda/non-Creation-Engine title
- Deployment & VFS
- Mod Scanning
- Conflicts & Load Order
- Save Management
- Tools & Overlays
- Playing a Game
- Feature Parity
Mount & Blade II: Bannerlord
Mount & Blade II: Bannerlord (bannerlord) is TaleWorlds’ medieval sandbox built on its in-house engine. modde models it as its own engine family (EngineFamily::Bannerlord) because its mod layout — a flat Modules/ directory where each module carries a SubModule.xml manifest — does not match any of the Bethesda, Unreal, REDengine, or SMAPI families.
Overall status
Bannerlord ships as a Partial title in the supported-games matrix. What that means in practice:
- Scanner — Yes. modde discovers installed modules under
Modules/and parses eachSubModule.xmlfor identity and dependencies. - Conflict detection — Yes, via the generic policy classifier plus Bannerlord-specific save-breaking and cosmetic classification.
- Save tracking —
Done. The git-backed save vault, fingerprinting, and capture/restore all work against the Bannerlord save directory.
The Partial label reflects what is not yet built rather than broken behavior: there is no Bannerlord-aware load-order manager (the game’s own Launcher / Mod Options screen still owns load order), and the mod information dialog only surfaces a Nexus-metadata side panel, not a file-tree / dependency-graph inspector. Deployment, scanning, conflict classification, save management, and launcher integration are all functional.
| Capability | Status | Notes |
|---|---|---|
| Install detection (Steam) | Yes | Steam app id 261550, install dir Mount & Blade II Bannerlord |
| Mod directory | Yes | Modules/ under the install root |
| Filesystem scanner | Yes | Walks Modules/, parses SubModule.xml |
| Dependency checking | Yes | Reports <DependedModule> ids missing from the module set |
| Conflict classification | Yes | Generic classifier + Bannerlord save-breaking / cosmetic policy |
| Save tracking | Done | Documents/Mount and Blade II Bannerlord/Game Saves/Native, *.sav |
| Load-order management | Not shipped | Use the in-game Launcher’s Mod Options screen |
| OptiScaler profiles | Not shipped | No bundled upscaling profile (see tools) |
How modde detects the install
Bannerlord is registered with a Steam app id of 261550 and a default Steam library directory name of Mount & Blade II Bannerlord. When modde walks your Steam libraries, it matches by app id first and falls back to the directory name. No Heroic (GOG/Epic) ids are registered, because Bannerlord is a Steam title; if you run it from a non-Steam source you can still point modde at the install explicitly with --game-dir (see the worked example).
On Linux, Steam runs Bannerlord through Proton. The mod directory and save directory modde uses are host-side paths — modde reads and writes Modules/ directly in the real game install and the save directory under your real $HOME/Documents, so you do not generally need to reach inside the Proton prefix to manage mods. The game’s Windows-side view of those paths is provided by Proton’s drive mapping. The Windows executable lives under bin/Win64_Shipping_Client/ relative to the install root; modde records that as the game’s executable directory so tools and launch integrations can find it.
Mod directory and deploy strategy
The mod directory is Modules/ under the game install root. Each mod is a self-contained subdirectory (for example Modules/MyAwesomeMod/) that contains, at minimum, a SubModule.xml and usually a bin/, ModuleData/, GUI/, or AssetPackages/ tree.
modde deploys mods with its standard symlink-farm VFS: mods are staged in ~/.local/share/modde/staging/<profile>/ and then linked into Modules/, leaving the original game files untouched and the deployment fully reversible. Conflict resolution (later mods in the load order win) happens during the build phase. See Deployment & VFS for the full pipeline.
Archive analysis (what modde does when you install a mod)
When you install a Bannerlord mod archive, modde inspects the extracted contents and picks a layout strategy:
- Module-root archives — if the archive extracts to a directory that already contains a
SubModule.xmlat its top level, modde treats the whole archive as a single module. It uses the module’s ownId.valueattribute (fromSubModule.xml) as the mod id when one is present. Modules/-prefixed archives — if the archive instead contains a top-levelModules/directory, modde strips that prefix and deploys its contents into the game’sModules/directory, so a mod packaged asModules/CoolMod/...lands atModules/CoolMod/....
The same two shapes are recognized as a “bare layout” (a loose, already-extracted folder you point modde at): a directory holding a SubModule.xml, or a directory whose root contains a modules/ folder and .xml files (the directory match is case-insensitive). This is what lets you adopt manually downloaded mods without a wrapping archive.
What scanning finds
modde scan --game bannerlord walks the Modules/ directory and reports one discovered mod per immediate subdirectory that contains a SubModule.xml. For each such module it:
- Parses
SubModule.xmlfor the module id (<Id value="...">) and human-readable name (<Name value="...">). If<Id>is missing it falls back to the directory name; if<Name>is missing it falls back to the id. - Records every file under the module directory, relative to the install root, as the module’s footprint.
- Emits a discovered mod with id
module/<id>, the parsed display name, and a high confidence score (0.95). Modules with no files are skipped.
A directory without a SubModule.xml is not reported — modde only counts a subfolder as a Bannerlord module when that manifest is present. See Mod Scanning for importing scan results into a profile and for Wabbajack-manifest matching.
Dependency checking
Beyond identity, SubModule.xml parsing collects every <DependedModule Id="..."> entry. modde can then cross-reference the declared dependencies of all discovered modules against the set of module ids that are actually present and report missing dependencies as (module_id, missing_dependency_id) pairs. This catches the classic Bannerlord failure mode where a mod silently refuses to load because a required module (for example Bannerlord.Harmony or Bannerlord.UIExtenderEx) is not installed. modde does not reorder modules to satisfy dependencies — load order remains the in-game Launcher’s job — it only surfaces what is missing.
Conflict classification
Bannerlord uses the generic policy collision classifier for file-level overwrite severity, layered on top of a Bannerlord content policy that maps file types to save-breaking, cosmetic, and content categories:
| Extension | Content category | Save-breaking? |
|---|---|---|
dll | Binary | Yes |
xml | Config | Yes |
xslt | Config | Yes |
xsl | Config | Yes |
pak | Archive | Yes |
dds, png, tga, jpg | Texture | No (cosmetic) |
The save-breaking extension set is dll, xml, xslt, xsl, pak; the cosmetic set is png, jpg, dds, tga. In addition, anything under a module’s bin/ directory or a submodule.xml file is treated as save-breaking, because those carry the compiled assemblies and the module manifest that actually change how a campaign behaves. Texture overwrites are classified as cosmetic — visible, but they will not invalidate a save.
Run modde collisions --profile <name> to see conflicting module pairs with severity and file counts, or add --all to include cosmetic overlaps. The conflict model (load-order priority, shadowed mods, redundant files, per-file hiding) is the same one described in Conflicts & Load Order; the LOOT plugin-sorting parts of that guide are Bethesda-only and do not apply to Bannerlord.
Save tracking
Bannerlord saves are tracked from:
~/Documents/Mount and Blade II Bannerlord/Game Saves/Native/
The tracker matches *.sav files (non-recursively) in that directory and files them all under the campaign category. Each file’s relative name is used as its label, and the capture summary is reported by category.
These saves flow into modde’s standard git-backed save vault: a per-game repository under ~/.local/share/modde/saves/bannerlord/, with one branch per profile. Switching profiles captures the outgoing saves and restores the incoming profile’s. Every snapshot embeds a SHA-256 fingerprint of the profile’s enabled save-breaking mods (the dll/xml/xslt/xsl/pak + bin/ + submodule.xml set above), so that on restore modde can warn you when a snapshot was taken with a different set of save-breaking modules than your current profile:
Mod-Fingerprint: a1b2c3d4e5f6
Save-Breaking-Mods: module/Bannerlord.Harmony, module/CoolMod
Adopt pre-existing saves with modde save adopt --game bannerlord --profile <name>, then capture/restore/history as in Save Management. Because Bannerlord saves carry the active module list inside the .sav itself, the fingerprint warning is a strong signal: loading a campaign save against a mismatched module set is the most common cause of corrupted Bannerlord saves.
Plugin / load-order handling
Bannerlord has no separate plugin format (no .esp/.esl equivalent); a module either loads or it does not, and order is resolved by the game’s own Launcher → Mods screen and the LauncherData.xml it writes. modde does not ship a Bannerlord load-order manager and does not write LauncherData.xml. What modde provides instead is:
- Dependency visibility — missing-
<DependedModule>reporting (above), so you know which modules must be present and roughly in what dependency relationship. - Deployment ordering — modde’s load-order priority decides which mod wins a file-level conflict during deployment, which is independent from the runtime module activation order TaleWorlds’ Launcher controls.
After deploying with modde, enable and order your modules in the in-game Launcher (or a third-party launcher such as the Bannerlord Mod Launcher / BUTR tools) as you normally would.
Installer specifics
Bannerlord does not use FOMOD, REDmod, .pak mod managers, or SMAPI. Its packaging is simpler and modde handles it directly:
- No FOMOD wizard — most Bannerlord mods are plain module folders. modde’s two archive shapes (a top-level
SubModule.xml, or a top-levelModules/prefix to strip) cover the vast majority of Nexus downloads. The FOMOD installer described in FOMOD installer is used by Bethesda-style mods and generally does not apply here. .pakfiles are Bannerlord asset packages, classified as save-breaking archives — they are content, not an installer format, and are deployed like any other module file.- Harmony / UIExtenderEx / ButterLib / MCM are themselves ordinary modules: install them like any other mod and let dependency checking confirm they are present.
Install a single mod from Nexus with modde install mod <url> --profile <name>; the Nexus domain for Bannerlord is mountandblade2bannerlord, which modde uses to resolve Nexus URLs to this game.
Gaming tools, Proton, and overlays
Bannerlord registers no OptiScaler profile, so the bundled DLSS/FSR upscaling workflow that ships for titles like Stellar Blade is not pre-configured here. The general tool integrations still apply: you can enable MangoHud, vkBasalt, GameMode, ReShade, and per-game Proton settings through modde tool ..., exactly as documented in Tools & Overlays.
The most relevant tool for a Linux Bannerlord install is the proton integration, which stores per-game launch settings (Proton version mode, prefix override, extra environment variables, and DLL overrides) without installing Steam or the game itself:
# Keep Steam's default Proton runner for Bannerlord
modde tool configure proton --game bannerlord version_mode=launcher_default
# Force a DLL override if a native-loader mod needs it
modde tool configure proton --game bannerlord dll_override_mode=forced forced_dll_overrides=dxgi
modde’s executable management is fully shipped and is useful for Bannerlord’s out-of-game tools: register a named executable (with arguments, working directory, environment, Wine DLL overrides, and an output-capture mod) via modde tool add-executable ... and run it with modde exec ..., capturing any files the tool writes into an overwrite mod. This is handy for running modding utilities against the deployed Modules/ tree.
Linux / Proton notes and known gotchas
These notes cover the game’s runtime on Linux and macOS, where Bannerlord runs through Proton/Wine. On Windows the game runs natively — there is no Wine prefix, and Modules/ and the save directory live at their native Windows locations. modde itself runs natively on all three platforms.
- Run the game once before modding. Let Steam/Proton create the prefix and let Bannerlord generate
~/Documents/Mount and Blade II Bannerlord/(Proton maps this from the prefix’sdrive_c/users/steamuser/Documents, but on a typical setup it surfaces at your real$HOME/Documents). modde’s save tracker reads theGame Saves/Native/directory there. - Modules live host-side. Because modde links into the real
Modules/directory under the install root, you usually do not touch the Proton prefix to add or remove mods — but the game still needs to be launched through the same Proton runner you configured. - Enable modules in the Launcher after deploying. Deployment puts the files in place; it does not toggle modules on. A freshly deployed module that is not enabled in the in-game Launcher will appear “missing” even though its files are present.
.NETmodule DLLs are save-breaking. Adding or removing adll-bearing module changes the save fingerprint. Expect (and heed) modde’s mismatch warning when restoring an older campaign save.- Case sensitivity. modde’s bare-layout recognition treats the
Modules/modulesdirectory case-insensitively, which matters on Linux’s case-sensitive filesystem when a mod ships an oddly-cased folder; the on-diskModules/directory itself is whatever the game created.
Worked example
Declarative (home-manager)
Define a Bannerlord profile and wait for the game to be installed before modde deploys into it:
programs.modde.profiles.my-bannerlord = {
game = "bannerlord";
# Point at the real Steam install once it exists; until then modde
# prints an awaiting message instead of failing activation.
installMode = "await-game";
# gameDir = "/home/you/.steam/steam/steamapps/common/Mount & Blade II Bannerlord";
tools.proton.settings = {
version_mode = "launcher_default";
};
};
After Steam has installed the game, set gameDir to the install path and switch installMode = "auto", then rebuild your home-manager configuration; modde deploys the profile during activation.
Imperative (CLI)
# 1. Create a Bannerlord profile
modde profile create my-bannerlord --game bannerlord
# 2. Install a mod from Nexus into it
modde install mod \
https://www.nexusmods.com/mountandblade2bannerlord/mods/2018 \
--profile my-bannerlord
# 3. Import any modules already sitting in Modules/
modde scan --game bannerlord --import-to my-bannerlord
# 4. Inspect conflicts (add --all for cosmetic overlaps)
modde collisions --profile my-bannerlord
# 5. Deploy the symlink farm into the game's Modules/ directory
modde deploy --profile my-bannerlord --game bannerlord \
--game-dir "$HOME/.steam/steam/steamapps/common/Mount & Blade II Bannerlord"
# 6. Adopt existing campaign saves into the git-backed vault
modde save adopt --game bannerlord --profile my-bannerlord
Then launch through Steam (Proton), enable your modules in the in-game Launcher, and order them there.
See also
- Supported games
- Deployment & VFS
- Mod Scanning
- Conflicts & Load Order
- Save Management
- Tools & Overlays
- Feature parity reference
Generic & user-defined games
modde ships 15 built-in games with bespoke scanners, save trackers, and conflict logic. For everything else, there is the generic game path: a configurable plugin driven by a small TOML file (a GameSpec) that you write — or generate — and drop into modde’s data directory. This lets you point modde at an arbitrary title and get the deployment engine, conflict detection, launcher integration, and executable management without waiting for a hand-coded profile.
Generic game support is
Partial. A user-defined game gives you the engine-agnostic parts of modde — virtual-filesystem deployment, conflict classification, launcher detection, and executable management — but it does not give you a bespoke filesystem scanner or a save tracker. There is no plugin author writing game-specific heuristics behind it; you are describing the game with a handful of paths and IDs. Treat it as “modde can deploy and launch mods for this game”, not “modde fully understands this game”. See the supported-games matrix for the canonical status vocabulary (the baseline lives indocs/capability-matrix.toml).
What a user-defined game can and cannot do
A generic game is registered with EngineFamily::Generic. modde treats it like
any other game for the parts of the pipeline that do not need game-specific
knowledge, and explicitly opts out of the parts that do.
| Capability | Generic game | Notes |
|---|---|---|
| Mod deployment (virtual filesystem / overlay) | Yes | Mods deploy into the configured mod_dir (defaults to the install root). |
| Conflict detection & classification | Yes | Uses the built-in generic_collision_classifier. |
| Profiles | Yes | Standard per-game profiles work. |
| Install detection | Yes | Via install_path_override, Steam install_dir_name, or Steam/Heroic launcher data. |
| Launcher integration | Yes | steam_app_id lets modde resolve the Steam launch target. |
Executable management (modde exec / modde tool add-executable) | Yes | Named launch targets with args, working dir, env, Wine DLL overrides, and overwrite capture work the same as for built-in games. See the CLI reference. |
| Wine / Proxy DLL overrides | Yes | proxy_dlls are emitted as Wine DLL overrides, but only for DLLs that actually exist in the executable directory. |
| Nexus metadata side panel | Partial | Set nexus_domain to enable Nexus browse/search and the metadata side panel; the richer mod-information dialog is not implemented for any game. |
| Bespoke filesystem scanner | No | scanner: None. modde scan has no game-specific heuristics to discover already-installed mods for a generic game. |
| Save tracking / save profiles | No | save_tracker: None and supports_save_profiles: false. modde save save-profile features are unavailable. |
| Wabbajack list matching | No | Generic games carry no wabbajack_names. |
| OptiScaler profiles | Opt-in | None ship by default, but you can import them — see import-profile. |
The short version: deployment, conflict, and launcher work; there is no
bespoke scanner and no save tracker. Those two gaps are the defining limits of
the Partial status.
Where specs live on disk
User-defined game specs are plain TOML files in modde’s data directory:
<data dir>/modde/games/<id>.toml
<data dir> resolves per-platform:
- Linux:
$XDG_DATA_HOME/modde/games/or~/.local/share/modde/games/ - Overridable by the active instance data directory.
Each game is one file named after its id (for example factorio.toml). The
loader reads this directory at startup:
- only files ending in
.tomlare considered; - files ending in
.optiscaler.tomlare skipped (those are OptiScaler profile sidecars, not game specs); - specs are loaded in sorted filename order;
- each spec is validated, and any spec whose
idcollides with a built-in game or a previously-loaded user game is skipped with a warning; - invalid or unparseable specs are skipped (with a warning) rather than aborting the load.
You can edit these files by hand, but the recommended path is the
modde game CLI, which validates before writing.
The GameSpec TOML schema
A GameSpec is a flat TOML table. Only three fields are required.
# games/<id>.toml
id = "factorio" # required
display_name = "Factorio" # required
executable_dir = "." # required (relative to install root)
# All of the following are optional:
steam_app_id = "427520" # Steam AppID, as a string
install_dir_name = "Factorio" # steamapps/common/<name>
install_path_override = "/games/factorio" # absolute path; bypasses detection
mod_dir = "mods" # where mods deploy, relative to install root
nexus_domain = "factorio" # Nexus game domain for browse/search
proxy_dlls = ["dxgi", "winmm"] # candidate Wine DLL overrides
| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | yes | Stable game ID. Must match ^[a-z0-9][a-z0-9-]*$ and not collide with a built-in game ID. This is also the filename stem. |
display_name | string | yes | Human-readable name. Must be non-empty (after trimming). |
executable_dir | path | yes | Directory containing the game executable(s), relative to the install root. Use "." if the executable sits at the root. Must be relative and must not contain .. or escape the root. |
steam_app_id | string | no | Steam AppID (stored as a string; parsed to u32 when a launch target is resolved). Enables Steam launcher integration. |
install_dir_name | string | no | Folder name under steamapps/common/. modde scans every known Steam library for this folder when detecting the install. |
install_path_override | path | no | Absolute path to the install. When set and the directory exists, it short-circuits all other detection. |
mod_dir | path | no | Directory mods deploy into, relative to the install root. Must be relative and must not escape the root. Defaults to the install root itself when omitted. |
nexus_domain | string | no | Nexus game domain (the slug in a Nexus URL). Enables Nexus browse/search and the metadata side panel for this game. |
proxy_dlls | array of strings | no | DLL base names (without .dll) that modde may register as Wine DLL overrides. At launch, modde only emits an override for a name if <executable_dir>/<name>.dll actually exists, so listing a DLL that is not present is harmless. Defaults to empty. |
Validation rules
Both the CLI and the on-disk loader run the same validation before a spec is accepted:
idmust match^[a-z0-9][a-z0-9-]*$(lowercase ASCII letters/digits and hyphens, starting with a letter or digit).idmust not collide with a built-in game ID.executable_dirandmod_dir(if present) must be relative and must not contain.., a root component, or a Windows drive prefix — they cannot escape the install root.display_namemust not be empty after trimming whitespace.
A spec that fails validation is rejected by modde game add / modde game import
and silently skipped (with a warning) by the loader.
Install detection order
When modde needs to find the install directory for a generic game, it tries, in order:
install_path_override, if set and the directory exists.install_dir_nameunder any detectedsteamapps/common/library.- modde’s generic launcher detection (Steam/Heroic) keyed on the game ID.
The first hit wins.
The modde game CLI surface
All user-defined-game management lives under modde game. The full subcommand
set:
| Subcommand | Purpose |
|---|---|
add | Create or overwrite a user-defined game spec. |
list | List configured user-defined games. |
remove | Delete a user-defined game spec (with confirmation). |
show | Show a resolved game registration (user-defined or built-in). |
detect | Find executable-bearing directories under an install path. |
export | Serialize a game registration (any game) to a GameSpec TOML. |
import | Install a GameSpec TOML from a file into the games directory. |
import-profile | Install OptiScaler profiles from a TOML file for a game. |
modde game add
modde game add <id> \
--display-name <name> \
--executable-dir <relative-path> \
[--steam-app-id <id>] \
[--install-dir-name <name>] \
[--mod-dir <relative-path>] \
[--nexus-domain <domain>] \
[--proxy-dll <name>]... \
[--force]
| Flag / arg | Required | Maps to GameSpec field |
|---|---|---|
<id> (positional) | yes | id |
--display-name <name> | yes | display_name |
--executable-dir <path> | yes | executable_dir |
--steam-app-id <id> | no | steam_app_id |
--install-dir-name <name> | no | install_dir_name |
--mod-dir <path> | no | mod_dir |
--nexus-domain <domain> | no | nexus_domain |
--proxy-dll <name> | no | one entry in proxy_dlls; repeat the flag to add more |
--force | no | overwrite an existing <id>.toml instead of erroring |
add writes (or overwrites, with --force) <data dir>/modde/games/<id>.toml.
install_path_override is not settable from add — it is always written as
unset; use install_dir_name/steam_app_id for detection, or edit the TOML by
hand (or import a spec that contains it) if you need an absolute override.
Re-running add for an existing id without --force fails and tells you to
re-run with --force. On success it prints whether it Saved or Overwrote
the spec and the path it wrote.
modde game list
modde game list
Prints the user-defined games as a table: id | display name | executable_dir | source, where source is the on-disk path of each spec. Prints
No user-defined games configured. when the games directory is empty or absent.
Built-in games are not listed here.
modde game show
modde game show <id>
Shows a resolved registration. If <id> is a user-defined game it prints the
full spec (display name, executable_dir, mod_dir, steam_app_id,
install_dir_name, nexus_domain, proxy_dlls, and the source path). If <id> is a
built-in game it prints the built-in summary (display name, steam_app_id,
nexus_domain, and source: built-in registry). Unknown IDs error with the list
of supported game IDs.
modde game remove
modde game remove <id> [--yes]
Deletes <data dir>/modde/games/<id>.toml. By default it prompts
Remove user-defined game '<id>'? [y/N]; pass --yes to skip the prompt.
Attempting to remove a built-in game errors with
game '<id>' is built in and cannot be removed; removing an unknown id errors
with no user-defined game named '<id>' exists.
modde game detect
modde game detect <install-path>
Walks <install-path> looking for directories that contain .exe files, to
help you choose an executable_dir (and confirm the install layout). For each
directory containing at least one executable it reports the relative path, the
sorted list of .exe names, and the combined size of those executables in
bytes. Behavior:
- the search recurses up to 4 directory levels deep below the install root;
.exematching is case-insensitive;- results are sorted by total executable size descending, then by relative path — so the largest-executable directory (usually the real game binary) is listed first;
- a non-existent install path errors;
- when nothing is found it prints
No executable-bearing directories found under <path>.
This is purely advisory — detect never writes a spec. Use its top result as
your --executable-dir.
modde game export
modde game export <id> [--with-optiscaler] [--output <path>]
Serializes any resolvable game (built-in or user-defined) to a GameSpec TOML.
Exporting a built-in game gives you a starting point you can rename and tweak: it
carries the built-in display_name, steam_app_id, install_dir_name, and
nexus_domain, with executable_dir = "." and no mod_dir/proxy_dlls. With
--with-optiscaler the exported TOML also includes the game’s resolved
OptiScaler profiles (appended after the spec). With --output <path> it writes
to that file; otherwise it prints to stdout.
modde game import
modde game import <path> [--force]
Reads a GameSpec TOML from <path>, parses and validates it, then copies it to
<data dir>/modde/games/<spec-id>.toml. The destination filename comes from the
spec’s id, not from the source filename. Without --force, an existing
destination errors; with --force it overwrites. This is how you install a spec
someone shared with you.
modde game import-profile
modde game import-profile <path> --for <game-id> [--force]
Imports OptiScaler profiles (not a game spec) from <path> and writes them to
the sidecar <data dir>/modde/games/<game-id>.optiscaler.toml. The --for
flag names the game the profiles belong to. Importing for an unknown game id
warns but proceeds. Without --force, an existing sidecar errors; with --force
it overwrites. On success it reports how many profiles were imported and where.
Detecting executable-bearing directories
Before registering a game you usually do not know where the real binary lives —
some titles ship a tiny launcher at the root and the actual game several folders
deep. modde game detect answers that:
modde game detect "/home/you/Games/factorio"
Executable-bearing directories under /home/you/Games/factorio:
1. bin/x64
executables: factorio.exe
total exe size: 41203712 bytes
2. .
executables: launcher.exe
total exe size: 524288 bytes
The first entry has the largest executables, so bin/x64 is almost certainly
the executable_dir you want. Feed it straight into modde game add --executable-dir bin/x64.
Sharing a spec
GameSpecs are designed to be portable text files:
# On the authoring machine: write the spec to a file you can commit/share.
modde game export factorio --output factorio.toml
# Or hand-author games/factorio.toml and copy it out of the games directory.
# On another machine: install it (filename is taken from the spec id).
modde game import factorio.toml
Because the destination filename is derived from the spec’s id, the source file
can be named anything. import re-validates the spec before installing it, so a
malformed or id-colliding spec is rejected up front rather than being silently
dropped at load time. Keep specs in version control or share them in issues — they
are small and contain no secrets.
Worked example: registering a new title
Suppose you want to manage mods for a Steam game called Voidrunner that modde
does not ship a profile for. Its Steam AppID is 998877, it installs to
steamapps/common/Voidrunner, the real binary lives in Binaries/Win64, and
mods drop into a top-level Mods/ folder. It uses a dxgi.dll proxy for an
upscaler, and there is a Nexus page under the voidrunner domain.
1. Find the executable directory.
modde game detect "$HOME/.local/share/Steam/steamapps/common/Voidrunner"
Executable-bearing directories under .../Voidrunner:
1. Binaries/Win64
executables: Voidrunner-Win64-Shipping.exe
total exe size: 78905344 bytes
2. .
executables: VoidrunnerLauncher.exe
total exe size: 1048576 bytes
Binaries/Win64 is the winner.
2. Register the game.
modde game add voidrunner \
--display-name "Voidrunner" \
--executable-dir "Binaries/Win64" \
--steam-app-id 998877 \
--install-dir-name "Voidrunner" \
--mod-dir "Mods" \
--nexus-domain voidrunner \
--proxy-dll dxgi
Saved user-defined game 'voidrunner' at /home/you/.local/share/modde/games/voidrunner.toml
This writes:
# /home/you/.local/share/modde/games/voidrunner.toml
id = "voidrunner"
display_name = "Voidrunner"
steam_app_id = "998877"
install_dir_name = "Voidrunner"
mod_dir = "Mods"
executable_dir = "Binaries/Win64"
nexus_domain = "voidrunner"
proxy_dlls = ["dxgi"]
3. Confirm it.
modde game list
modde game show voidrunner
4. Use it like any other game. From here, --game voidrunner works across
the CLI: create a profile, install mods, deploy, and register a launch target.
modde profile create main --game voidrunner
# install mods into the profile (nexus / mediafire / local files) ...
modde deploy --game voidrunner
# Register a named executable launch target (see the CLI reference):
modde exec add voidrunner-game "Binaries/Win64/Voidrunner-Win64-Shipping.exe" \
--game voidrunner
modde play main --game voidrunner
modde will detect the install (via the override-free detection chain:
install_dir_name under Steam, then launcher data), deploy mods into
Mods/, classify conflicts, and — because dxgi.dll exists in
Binaries/Win64 after you deploy the upscaler — register the dxgi Wine DLL
override at launch.
What you do not get: modde scan --game voidrunner has no Voidrunner-aware
heuristics to discover pre-existing mods, and modde save save-profile features
are unavailable, because generic games ship neither a scanner nor a save tracker.
That is the boundary of the Partial status.
See also
Nexus Mods
Overview
modde integrates with Nexus Mods for authentication, browsing,
searching, downloading, installing, and update-checking mods. The integration uses both the
v1 REST API (https://api.nexusmods.com/v1) and the v2 GraphQL API
(https://api.nexusmods.com/v2/graphql). A Nexus Mods account with an API key is required for
every authenticated call, and automated CDN downloads additionally require a Premium
subscription — see Premium and what free accounts get.
This page covers the full surface: the six-source key-resolution chain, browse/search/trending
feeds, rate-limit behaviour, single-mod installs, Nexus Collections, the nxm:// handler, and
update checking with its safety guardrails.
Authentication
The six-source resolution chain
When modde needs a Nexus credential, it walks an ordered chain and uses the first source that yields a non-empty key. Higher entries win over lower ones — a configured config file beats an environment variable, which beats the keyring, and so on.
| Priority | Source | How it is set | Best for |
|---|---|---|---|
| 1 | OAuth token | OAuth2 PKCE flow, stored in the keyring (modde/nexus-oauth-token); used only while unexpired (60 s skew buffer) | Future interactive desktop login |
| 2 | modde config file | modde nexus auth writes ~/.config/modde/nexus_api_key (mode 0600) | Single-user workstations |
| 3 | NEXUS_API_KEY | Environment variable | CI, containers, one-off shells |
| 4 | System keyring | secret-service over D-Bus, service modde, key nexus-api-key | Desktop sessions with an unlocked keyring |
| 5 | NEXUS_API_KEY_FILE | Environment variable pointing at a file whose contents are the key | sops-nix / agenix / systemd credentials |
| 6 | Legacy settings.toml | nexus_api_key field in modde’s settings file | Backward compatibility only |
If none of the six yields a key, modde fails with:
No Nexus API key found. Set NEXUS_API_KEY env var or run `modde nexus auth`.
Every key is trimmed of surrounding whitespace before use, and empty/whitespace-only sources are skipped (they do not short-circuit the chain). The OAuth entry at priority 1 is checked first but falls through to the API-key chain when no token is stored or the stored token has expired.
Note on the keyring vs. the config file. Priority 2 (the
modde nexus authconfig file) and priority 4 (the system keyring) are distinct. The currentmodde nexus authflow writes the mode-0600config file at~/.config/modde/nexus_api_key; the keyring slot is also read if present (e.g. written by another tool or a future flow). Because the config file sits above the keyring in the chain, a key written bymodde nexus authalways takes precedence.
Recommendation per environment
| Environment | Recommended source | Why |
|---|---|---|
| NixOS / home-manager | NEXUS_API_KEY_FILE via programs.modde.nexus.apiKeyFile | Declarative, secret stays out of the Nix store, sops-nix/agenix compatible |
| Single-user desktop | modde nexus auth (config file) | One command, restrictive permissions, survives reboots |
| CI / ephemeral containers | NEXUS_API_KEY env var | No persistent state, easy to inject as a secret |
| Shared / multi-user host | NEXUS_API_KEY_FILE with per-user file permissions | Avoids leaking the key into a shared keyring or shell history |
Saving your API key (modde nexus auth)
modde nexus auth
This stores your personal API key so that subsequent commands can authenticate non-interactively. Get your key from Nexus Mods → My Account → API Keys.
Using sops-nix (declarative, recommended on NixOS)
For NixOS/home-manager setups, point modde at a secret file. The home-manager module wires
apiKeyFile to NEXUS_API_KEY_FILE, which is priority 5 in the chain:
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
};
The file contents are the raw key; modde trims trailing newlines. Because the path is read at runtime, the secret never enters the Nix store. This is compatible with sops-nix, agenix, and systemd credentials. See the Home-Manager module reference for the full option.
Checking status (modde nexus status)
modde nexus status
This calls the v1 users/validate.json endpoint and reports the resolved account name and whether
it has Premium. Use it to confirm both that your key is valid and that automated downloads
will work.
Premium and what free accounts get
Automated CDN downloads — the download_link.json endpoint that returns a time-limited CDN URL —
are Premium-only. Before generating any download link, modde validates the account and refuses
non-Premium keys with:
Nexus Premium is required for automated downloads.
Please upgrade at https://next.nexusmods.com/premium
| Capability | Free account | Premium account |
|---|---|---|
modde nexus status / account validation | Yes | Yes |
| Browse / search / trending / updated feeds (REST + GraphQL) | Yes | Yes |
| Mod metadata, file listings, collection manifests | Yes | Yes |
Update checking (modde update check) | Yes | Yes |
| Endorse / track / untrack mods | Yes | Yes |
Automated CDN download (install mod, update apply, nxm handle, collection installs) | No | Yes |
Free accounts can still drive a nxm:// “Download with modde” link when the link carries its own
short-lived key/expires pair issued by the website (see the nxm:// handler),
because that path does not require the Premium download_link.json call. Everything that resolves a
CDN URL through the API requires Premium.
Browsing and searching
modde queries the v2 GraphQL endpoint first for browse/search, because it returns richer per-tile data (thumbnails, summaries, endorsement and download counts) in a single round-trip. If the GraphQL schema shifts or the endpoint errors, modde transparently falls back to the v1 REST equivalent so the UI keeps rendering.
| Feed | GraphQL (preferred) | REST fallback (v1) |
|---|---|---|
| Trending | browse_feed_gql(domain, Trending) | GET /games/{domain}/mods/trending.json |
| Monthly top | browse_feed_gql(domain, MonthlyTop) | GET /games/{domain}/mods/updated.json?period=1m |
| Full-text search | search_mods_gql(domain, term, page) | GET /games/{domain}/mods/search.json?search=…&page=… |
| Collections feed/search | collections_feed_gql(domain, term) | GET /games/{domain}/collections.json?search=… |
Additional v1 REST endpoints back the rest of the integration:
| Purpose | Endpoint |
|---|---|
| Mod details | GET /games/{domain}/mods/{id}.json |
| File listing | GET /games/{domain}/mods/{id}/files.json |
| Recently updated | GET /games/{domain}/mods/updated.json?period={1d|1w|1m} |
| Collection by slug | GET /collections/{slug}.json (game domain auto-discovered) |
| Collection revision | GET /games/{domain}/collections/{slug}/revisions/{rev}.json |
| Endorse / abstain | POST …/mods/{id}/endorse.json · …/abstain.json |
| Tracked mods | GET/POST/DELETE /user/tracked_mods.json |
The mod-image gallery is fetched via a small dedicated GraphQL query (modImages), falling back to
the single picture_url from the v1 mod response when GraphQL is unavailable.
The base URLs honour
MODDE_NEXUS_BASE_URLandMODDE_NEXUS_GRAPHQL_URLfor pointing the client at a mock server in tests. Production never sets them.
Rate-limit awareness
Nexus enforces hourly and daily request limits. modde reads the response headers on every v1 call and reacts:
x-rl-hourly-remaining— when fewer than 10 requests remain in the hour, modde logs a warning so you can slow down before hitting the wall.- HTTP
429— a hard rate-limit error. modde surfaces it asNexus API rate limit exceeded. Please wait before retrying.Respect theRetry-Afterheader the server returns and wait before re-running.
Update checking is designed to be frugal here: mods are grouped by game domain and modde issues
exactly one updated_mods call per domain regardless of how many mods you track in that game
(see Checking for updates).
Installing a single mod
modde install mod https://www.nexusmods.com/skyrimspecialedition/mods/12345 --profile my-skyrim
The pipeline:
- Resolve the mod, pick the latest MAIN file, and generate a CDN download link (Premium-gated).
- Download the archive into the store, then extract into a temporary staging tree (not the store) for analysis.
- Hash the archive (
xxhash64) and record it on the install plan. - Analyze the staged tree to detect the install method (simple copy, FOMOD, BAIN, …).
- Move files into the canonical store layout
store/{domain}_{mod}_{file}/, or, when more input is needed, keep the staged copy and route you to a wizard.
The pipeline is idempotent: if store/{domain}_{mod}_{file}/ already exists it returns
AlreadyStaged and skips the download. The same code path backs both the CLI and the UI
Browse Nexus → Install button.
Non-interactive FOMOD installs
If the mod uses a FOMOD installer, supply a declarative config to answer its choices without a wizard:
modde install mod <url> --profile my-skyrim --fomod-config my-choices.toml
The config can be TOML or JSON. Without it, a FOMOD mod is staged and marked
pending_user_input so a wizard can run later without re-downloading. See the
FOMOD guide for authoring --fomod-config files.
When detection fails
If modde cannot classify the archive, it writes a dossier (mod name, author, version, Nexus URL,
and a probe trace) next to the staged files and marks the install unknown. This is the same
dossier the skill tooling consumes to add bespoke handling.
Nexus Collections
A Nexus Collection is a curated, version-pinned set of mods. modde resolves them in two steps, which lets you install by slug alone without knowing the game domain up front:
- Discover —
GET /collections/{slug}.jsonreturns the collection’s game domain and its latest published revision number. - Fetch the manifest —
GET /games/{domain}/collections/{slug}/revisions/{rev}.jsonreturns the full, pinned manifest.
From the CLI
# Install the latest published revision
modde install nexus-collection <slug> --profile my-collection
# Pin a specific revision/version
modde install nexus-collection <slug> --version 7 --profile my-collection
When --version is omitted, modde uses the latest published revision discovered in step 1. When it
is supplied, step 1 is still used to learn the game domain, but the given revision is fetched
directly.
Declaratively (home-manager)
programs.modde.profiles.my-collection = {
game = "skyrim-se";
nexusCollection = {
slug = "collection-slug";
version = "1.0.0";
};
};
nexusCollection is mutually exclusive with wabbajackList. See the
Home-Manager module reference.
Version pinning and the profile lock
Installing a collection locks the profile with a NexusCollection lock reason. The lock
preserves the curator’s intended load order and prevents modde update apply from silently drifting
the profile away from the pinned revision. To update a locked profile you must opt in explicitly —
see the update guardrails.
nxm:// protocol handler
Nexus’s “Download with [manager]” buttons emit nxm:// URIs. To make modde the system handler:
modde nxm install
This registers an XDG desktop entry for the nxm:// scheme. Clicking a download link on the Nexus
website then hands the URI to modde, which fetches the file. You can also handle a URI by hand:
modde nxm handle "nxm://skyrimspecialedition/mods/12345/files/67890?key=abc&expires=123" \
--profile my-skyrim
The URI carries the game domain, mod ID, file ID, and a short-lived key/expires pair the website
mints for the download. --profile routes the result into a specific profile.
Checking for updates
modde update check
# Check for updates in the last week (default period)
modde update check --profile my-skyrim
# Check updates from the last day or month
modde update check --profile my-skyrim --period 1d
modde update check --profile my-skyrim --period 1m
--period accepts 1d, 1w (default), or 1m. Only enabled mods that carry Nexus metadata
(nexus_mod_id + nexus_game_domain + installed_timestamp) are considered — mods installed via
modde install mod and Nexus Collections record this automatically. modde groups the tracked mods by
game domain, makes one updated_mods call per domain, and reports a mod as updatable when its
latest_file_update timestamp is newer than your local installed_timestamp.
modde update check --modschecks your Nexus-tracked profile mods. Without--mods(and with no profile mods to check) the same command surface also covers modde’s own product update check.
modde update apply
The companion command downloads and installs the latest MAIN file for each updatable mod and rewrites
the profile entries to point at the fresh (mod_id, file_id) pair. Pair check and apply in a
scheduled task to keep a profile current.
# Preview only — list what would change, download nothing
modde update apply --profile my-skyrim --dry-run
# Apply non-breaking updates
modde update apply --profile my-skyrim
Safety guardrails
update apply is deliberately conservative because auto-updating can corrupt a curated setup:
- Locked-profile guard. Wabbajack, Nexus Collection, and TOML-import profiles ship a pinned,
authoritative load order.
applyrefuses to run on a locked profile and tells you to either pass--confirm-locked(acknowledging the drift) or runmodde profile unlock <name>first. With--confirm-lockedit proceeds but warns that the locked source will not be re-verified afterward. - Breaking-semver guard. Updates that look like a major version bump (e.g.
1.x → 2.0) are flagged breaking. They are skipped unless you pass--accept-breaking, and even then each breaking mod prompts for an interactivey/Nconfirmation.--yesassumes “yes” to those per-mod prompts but still refuses any breaking update unless--accept-breakingis also set. Version strings that can’t be parsed fall through as “not breaking”.
# Update a locked Collection profile, accepting major bumps non-interactively
modde update apply --profile my-collection --confirm-locked --accept-breaking --yes
See also
- Download backends — every transport modde can fetch from, and the resume/queue model
- Wabbajack modlists — the other major Nexus-backed install surface
- FOMOD installers — authoring
--fomod-configfiles - Home-Manager module reference —
nexus.apiKeyFile,nexusCollection - Parity audit — what is
DonevsPartial
Wabbajack Modlists
Overview
Wabbajack is a modlist installer originally built
for Windows. A .wabbajack file is a recipe — not a bundle of mods — that
records exactly which archives to download from the internet and exactly how to
reconstruct a curated mod setup from them. modde reads that recipe and replays
it natively on Linux: no Windows VM, no Wine-hosted Wabbajack client, no
.NET runtime. modde parses the manifest, resolves and verifies every download,
applies binary patches, repacks Bethesda archives, and stages the result, all in
Rust.
This means a .wabbajack list authored on Windows installs the same way a
NixOS user expects any other package to install: content-addressed, hash-verified,
resumable, and reproducible.
How it works
A .wabbajack file is a ZIP archive. Inside it is a single JSON manifest
(named modlist or *.json) plus inline patch/data blobs. modde reads the
manifest into a WabbajackManifest
with two top-level lists:
Archives— every external file the list needs, each identified by its Wabbajack hash (an xxHash64 stored as little-endian base64), its size, and a sourceStatedescribing where to fetch it.Directives— the ordered list of operations that turn downloaded archives plus inline data into the final mod layout.
modde converts the raw archives into typed download directives and the raw directives into typed install directives, then drives them through a pipeline: download/verify → resumable staging → apply directives → optional BSA/BA2 repack → validate → deploy.
modde does not install Steam or Heroic games; install the game with its launcher
first, then point gameDir at the installed game directory.
Archive sources (download directives)
Each Archive carries a State whose $type selects a downloader. modde maps
them to typed download directives:
Wabbajack State | modde directive | Notes |
|---|---|---|
NexusDownloader | Nexus | Resolved via the Nexus API (game domain + mod id + file id). Requires an API key. |
WabbajackCDNDownloader | WabbajackCdn | Authored-files / generated-output archives hosted on Wabbajack’s CDN. |
GitHubDownloader | GitHub | Release asset by user/repo, tag, and asset name. |
GoogleDriveDownloader | GoogleDrive | By Drive file id. |
MegaDownloader | Mega | By MEGA URL. |
MediaFireDownloader | MediaFire | By MediaFire URL. The MediaFire download backend resolves the real file. |
HttpDownloader | DirectURL | Plain HTTP(S) with optional headers. |
ModDBDownloader | DirectURL | Direct URL plus a ModDB HTML mirror resolver that follows the /downloads/start/<id>/all mirror page. |
ManualDownloader | Manual | A site (workupload, sharemods, LoversLab, …) that needs a human to click through. modde cannot fetch these unattended. |
GameFileSourceDownloader | (not a download) | Points at a file inside the local game install, not the internet. See game-file sources. |
Every download is verified against its manifest hash before it is trusted. modde never substitutes a similarly named file — the hash is the only safe identity for an archive.
Install directives
modde supports the full set of directives a typical Bethesda/MO2 modlist uses.
Each directive writes into the MO2-style staging layout (mods/<Mod Name>/…)
before deployment.
| Directive | What it does |
|---|---|
FromArchive | Extract one file out of a source archive (identified by ArchiveHashPath = [hash, inner/path]) and place it at the To path. The bread-and-butter directive — most files in a list are FromArchive. |
PatchedFromArchive | Extract a basis file from an archive, then apply an OctoDiff binary delta patch (PatchID) to reconstruct a modified file. Used when a list ships a small edit to a large vanilla/mod file rather than redistributing it. modde applies the OCTODELTA patch in pure Rust with bounds and output-size checks, and verifies the patched output against the directive’s expected Hash. |
InlineFile | Write a file whose bytes are stored inline in the .wabbajack (by SourceDataID). Used for small generated/config files. |
RemappedInlineFile | Like InlineFile, but with path-remapping applied by the author. modde treats it identically to InlineFile for placement and verifies its expected Hash. |
CreateBSA / BA2 repack | Reconstruct a Bethesda archive from a set of FileStates. modde repacks the staged loose files into a real .bsa (Skyrim, magic BSA\0, SSE version 105) or .ba2 (Fallout 4, magic BTDX/GNRL), computing the Bethesda/BA2 name hashes the games expect. The output format is chosen from the To extension. |
Manual (ManualDownloader archive) | Not a directive type, but the corresponding archive must be human-fetched (see missing archives). |
GameFileSource | Archives sourced from vanilla game files (see below). The directives that consume them are ordinary FromArchive/PatchedFromArchive. |
Directives that read from a single source archive are grouped per archive so each downloaded archive is opened once, all of its outputs (extractions and patches) are produced together, and the archive can then be released or pruned. This keeps memory and I/O bounded even for lists with thousands of directives.
Unknown / unsupported directive $types parse as Unknown and are surfaced as
hard blockers by modde wabbajack assess rather than
being silently skipped.
GameFileSource: vanilla game files
Some lists reference files that ship with the base game instead of downloading
them. Wabbajack records these as GameFileSourceDownloader archives that name a
game-relative path (e.g. Data\Skyrim.esm). These entries are not downloads —
modde reads and verifies them straight out of your installed game directory.
For example, several Skyrim SE lists (including Legends of the Frost) carry
GameFileSourceDownloader entries. Pass --game-dir (CLI) or set gameDir
(Home Manager) so modde can locate and hash those files. If a list has
game-file sources and no game directory is provided, the install is blocked
until you point it at the install.
Prerequisites
- A Nexus Mods API key — required for every
NexusDownloaderarchive (the bulk of most lists). See Nexus integration. - The base game installed — install it via Steam/Heroic first; modde does not install games.
- The game install path for lists that reference vanilla files
(
GameFileSourceDownloader) — supply--game-dir/gameDir. - Sufficient disk space — you need room for the downloaded archives and the staged/deployed output simultaneously. Large Bethesda lists routinely need hundreds of gigabytes. Staging compresses eligible files (see resumable zstd staging) to soften this, but plan for the full footprint.
Declarative installation (Home Manager)
programs.modde = {
enable = true;
nexus.apiKeyFile = "/run/secrets/nexus-api-key";
profiles.my-modlist = {
game = "skyrim-se";
installMode = "auto";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition";
wabbajackList = {
url = "https://example.com/modlist.wabbajack";
hash = "sha256-...";
};
};
};
If the modlist is already available locally, for example through
pkgs.requireFile or a custom fetcher, use path instead of url and hash:
programs.modde.profiles.my-modlist = {
game = "skyrim-se";
gameDir = "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition";
wabbajackList = {
path = /nix/store/...-Legends-of-the-Frost.wabbajack;
};
};
Use installMode = "await-game" while the game is not installed yet. Activation
will skip install/deploy and print the next step instead of failing.
CLI installation
modde install wabbajack /path/to/modlist.wabbajack \
--profile my-modlist \
--game-dir "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition"
Wabbajack registry pages usually link through to an authored-files URL for the
actual .wabbajack archive. Some authored-files CDN links now resolve through
Wabbajack’s chunked download page instead of a plain file response, so
pkgs.fetchurl may 404 even when modde’s chunk-aware downloader works. In that
case, download the file with modde wabbajack download and use
wabbajackList.path, or use a dedicated fetcher that reconstructs the chunks.
The full modde install wabbajack flag set
modde install wabbajack <PATH> [flags]
| Flag | Default | Effect |
|---|---|---|
--profile <name> | derived from list | Target profile to create/update. |
--game-dir <path> | auto-detect | Game install to deploy into and to read GameFileSource files from. |
--force | off | Force a full reinstall, skipping the preflight short-circuit (the cheap existence check that lets an already-staged list skip straight to deploy). |
--no-deploy | off | Stage into modde’s data directory but skip the final copy into --game-dir. Ideal for Stock-Game lists that must not write into the live install, and for staging a list before the game is even present. |
--continue-on-error | off | Log per-archive failures (download errors, missing files, broken upstream links) and keep going instead of aborting. Lets you drop manually-fetched archives in and re-run to fill the gaps. |
--reset-staging | off | Explicitly discard existing staging before installing. By default modde resumes compatible staging and adopts incompatible-but-present staging without deleting it (see below). |
--skip-validate | off | Skip post-staging hash validation before deploy. Faster, but you forfeit the safety net. |
--missing-archive-policy <p> | fail | What to do when downloadable manual/Nexus archives are missing. One of fail, omit-files, omit-mods (see below). |
--archive-retention <p> | keep | Source-archive retention after a batch is integrated: keep, prune-applied, or auto. |
--diagnostics-dir <path> | none | Write apply diagnostics (heartbeat + per-batch JSONL) here for later analysis. |
--diagnostics-interval <s> | 30 | Diagnostics heartbeat interval in seconds. |
--stall-warn-seconds <s> | 600 | Warn when apply makes no batch/sentinel progress for this long. |
--stall-abort-seconds <s> | 1800 | Abort when stalled this long and cgroup memory/swap are saturated. |
--acquire-missing | off | Frontload assisted acquisition of missing manual archives before applying. |
--no-acquire-missing | off | Disable the automatic frontloaded acquisition pass entirely. |
--acquire-download-dir <path> | data dir | Browser download directory to watch during assisted acquisition. |
--acquire-include-nexus | off | Also attempt to acquire missing Nexus archives during the frontload pass. |
--acquire-browser-controller | off | Drive controlled Chromium tabs (with a managed profile and auto-download prefs) for acquisition instead of just opening the system browser. |
--acquire-timeout <s> | 900 | Per-archive assisted-acquisition timeout in seconds. |
--missing-archive-policy
When optional manual/Nexus archives cannot be obtained, this controls how far the install proceeds:
fail(default) — abort before bulk work; print every missing archive with a validation command. Safest: you get a complete list back, not a partial install.omit-files— skip only the directives that depend on a missing archive (plus any downstreamCreateBSAthat consumed those files). Everything else installs.omit-mods— skip the entire MO2 mod root (mods/<Mod Name>/…) that any missing archive feeds, so you never end up with a half-populated mod folder.
Use modde wabbajack missing-impact first to see exactly
which mods each policy would drop.
--archive-retention
After modde finishes integrating an archive’s batch of directives it can free the downloaded source file:
keep(default) — keep all source archives (fastest re-runs; most disk).prune-applied— delete each source archive once its batch is fully applied.GameFileSource-backed archives are never pruned (they live in your game install, not the store).auto— prune small single-use archives but keep large or multi-directive archives that are likely to be re-read, balancing disk against re-extraction cost.
Acquiring missing archives during install
Unless --no-acquire-missing is set, modde install wabbajack runs an
assisted-acquisition frontload pass before applying directives. For each missing
manual archive it first tries a direct download; if the source needs a human
(login, captcha, mirror selection) it opens the page in your browser (or, with
--acquire-browser-controller, in managed Chromium tabs configured to
auto-download into the watched directory) and waits for a matching file to land,
importing it only when its hash matches. With --acquire-include-nexus it also
resolves Nexus archives via the API. If acquisition leaves archives unresolved
and the policy is fail, the install stops with the exact list of what is still
missing.
Assessing readiness
For large lists, do not start blind. The modde wabbajack subcommands inspect a
.wabbajack (and your store) without mutating anything so you can fix
problems before committing hours of downloading.
| Command | Purpose |
|---|---|
modde wabbajack assess <list> | Full readiness report: archive/directive type histograms, downloadable vs store-present counts, missing archives with remediation, manual-download list, game-file-source presence (--game-dir), staging layout action (create/resume/adopt), RAR support, and hard blockers vs warnings. Exits non-zero if any hard blocker exists. Supports --profile, --game-dir, --json. |
modde wabbajack missing-impact <list> | Which downloadable archives are missing and what they block: directly blocked directives + output bytes, affected CreateBSA outputs, and the omit-mods blast radius. --json for machines; --nix-snippet prints a Home Manager manualArchives block to fill in. |
modde wabbajack manual-links <list> | Just the manual-download URLs (workupload / sharemods / LoversLab) that require an operator visit, with the store path each file must end up at. |
modde wabbajack acquire-missing <list> | Run the assisted-acquisition flow standalone (direct → browser → controlled Chromium), importing only hash-matching downloads. Flags: --download-dir, --include-nexus, --browser-controller, --timeout, --json. |
modde wabbajack import-archive <list> <files…> | Import already-downloaded archives into the store by hashing them and matching against the manifest (see below). |
modde wabbajack analyze-diagnostics <dir> | Summarize a previous run’s --diagnostics-dir JSONL: heartbeats, last phase, max idle, peak RSS/swap/cgroup memory, and the ten slowest archive batches with per-phase timings. The tool you reach for when a run stalled or thrashed. |
A typical pre-flight is:
modde wabbajack assess /path/to/list.wabbajack \
--game-dir "/path/to/game" --profile my-modlist
modde wabbajack missing-impact /path/to/list.wabbajack
If assess reports only warnings (no hard blockers) you are ready for a
validated staged deploy. If it reports missing archives but no hard blockers, it
will tell you a partial staging is possible with
install wabbajack --no-deploy --continue-on-error --skip-validate.
Resumable zstd staging
Wabbajack installs are long. modde stages into a versioned, resumable layout so an interrupted run picks up where it left off instead of starting over.
- Layout versioning — staging records a
staging-layout.jsondescribing the layout version and compression policy. On the next run modde compares it:- matching → resume (reuse staged work);
- present but incompatible → adopt in place (keep files, rewrite the layout marker) — it does not delete your work;
- absent → create fresh.
--reset-staging(orassess’sadoptnotice) is the only thing that deletes staging.
- Per-archive and per-BSA sentinels — completed archive batches and
CreateBSAoutputs drop sentinel files so a resumed run skips finished work.assessreportsarchive sentinels: X/YandBSA sentinels: X/Yso you can see how far a previous run got. - Transparent zstd compression — large, compressible staged files (textures,
meshes, audio blobs) are stored zstd-compressed with a
.modde-zstsuffix. modde resolves, hashes, and materializes logical paths regardless of whether a file is plain or compressed, so validation and deploy are oblivious to it. Files that compress poorly or must stay byte-exact are never compressed: plugins (.esp/.esm/.esl), executables and DLLs, config formats (.ini/.json/.toml/.yaml/.xml/…), MO2 metadata (meta.ini/meta.json), BSA temp-build files, and anything under the staging_statedirectory. The size threshold and compression level are tunable with theMODDE_ZSTD_MIN_BYTESandMODDE_ZSTD_LEVELenvironment variables (level is clamped to 1–22). - Post-staging validation — before deploy (unless
--skip-validate), modde re-hashes every expected file against the manifest.InlineFile,RemappedInlineFile, andPatchedFromArchivecarry expected output hashes and are fully verified;FromArchiveoutputs are existence-checked (the manifest does not carry a post-extraction hash for them). Validation accepts both xxHash64 and xxh3 digests and reads compressed staged files transparently.
Missing authored and manual archives
Some Wabbajack modlists reference generated authored-files archives hosted by
Wabbajack, or manual-download archives behind a site that needs a human. If those
upstream entries disappear, modde fails before bulk downloads (under the default
fail policy) and prints every missing archive plus a curl -fI validation
command. modde will not substitute similarly named files because the manifest
hash is the only safe identity for an archive.
If you already have the exact missing archives in an old Wabbajack cache or a backup, import them into the modde store:
modde wabbajack import-archive /path/to/list.wabbajack \
/path/to/missing-archive-1.7z \
/path/to/missing-archive-2.7z
The import command hashes each file and imports only archives whose Wabbajack hash matches an archive referenced by the manifest. It classifies each input as:
- imported — hash matched a referenced archive; hard-linked (or copied) into the store and re-verified;
- already-present — the store already holds a verified copy;
- mismatched — the filename matches a manifest archive but the bytes do not (refused; this is the dangerous case the hash check catches);
- unused — the file’s hash is not referenced by this manifest at all.
Mismatched and unused inputs are reported and not imported, and the command exits non-zero if it refused anything.
Troubleshooting
Hash mismatches (and the nix prefetch trick)
Every archive and every patched/inline output is hash-verified. A mismatch means the bytes you have are not the bytes the list was built against — never force it through, because a single wrong file can corrupt the whole load order silently.
- For a missing/wrong
.wabbajackfile itself fetched declaratively, recompute the Nix hash withnix store prefetch-file <url>(ornix-prefetch-url) and paste the reportedsha256-…intowabbajackList.hash. Ahash mismatchduring the Home Manager build almost always means the URL now serves a different (or chunk-wrapped) body — see the chunked-download note above. - For a missing/wrong archive inside the list, use
modde wabbajack assess/missing-impactto identify it by Wabbajack hash, obtain the exact file (correct mod/file id on Nexus, or the original manual source), andimport-archiveit. The import refuses anything whose hash does not match, so you cannot accidentally poison the store. - A post-staging validation mismatch points at a bad source archive or a
failed patch. Re-run with
--reset-stagingfor the affected list after fixing the offending input; a corrupt compressed staged file (.modde-zst) is caught by validation and re-extracted on the next run.
Missing authored / manual archives
If assess lists manual archives, run modde wabbajack manual-links to get the
exact URLs and target store paths, fetch them by hand (or with
acquire-missing --browser-controller), and import-archive them. If an
authored-files archive has truly vanished upstream and you have no backup, you
cannot reproduce that exact file — switch the install to --missing-archive-policy omit-mods to drop just the affected mods, after reviewing the
missing-impact blast radius.
Stalls and thrash
Very large lists can appear to hang during archive verification/extraction.
- Run with
--diagnostics-dir <dir>; afterward (or on another terminal against a prior run) usemodde wabbajack analyze-diagnostics <dir>to see the last phase, max idle time, peak memory/swap, and the slowest batches. Abottleneck: archive trust/download verification wall before extractionline means the time is going into hashing, not a true hang. --stall-warn-secondssurfaces a warning when no batch/sentinel progress happens for a while;--stall-abort-secondsaborts only when stalled and cgroup memory/swap are saturated (genuine thrash) so a slow-but-progressing run is never killed prematurely.- If memory is the constraint, set
--archive-retention auto(orprune-applied) to free integrated archives, and lowerMODDE_ZSTD_LEVELto reduce compression CPU. - An interrupted run is safe to resume — just re-run the same command. modde resumes compatible staging and skips finished archive/BSA sentinels.
Migrating an existing on-disk install
If you already deployed a Wabbajack list (on Windows, or a prior run) and want
modde to track it without reinstalling, scan the game directory and match it
against the manifest instead. See Mod scanning for
modde scan --manifest …, threshold tuning, and duplicate pruning.
Supported games
Not all Wabbajack games are supported yet. See the Wabbajack game mapping for the current list.
See also
- Mod scanning — track an already-installed list via manifest matching
- Nexus integration — API key setup and download resolution
- Deployment — how staged mods reach the game directory
- Profiles — load-order locks applied by Wabbajack installs
- Troubleshooting — general install/runtime issues
- Supported games — game coverage and Wabbajack mapping
- CLI reference — every flag, generated from the binary
Download backends
Overview
modde resolves and fetches mod archives through a set of pluggable download backends. Every
backend implements one trait (DownloadSource) with two phases:
resolve— turn a download directive into a concreteDownloadHandle(a final URL, optional mirror candidates, headers, an expected hash, and a size hint).download_with_progress— stream the bytes to disk, report progress, and verify the hash.
At runtime the backends are wrapped in an AnySource enum so a heterogeneous list can dispatch to
the right one by directive type. Every backend verifies the downloaded file against a
Wabbajack-compatible hash: it computes both xxhash64 (xxh64, seed 0) and xxh3-64
(xxh3) while streaming, and accepts the file if either digest matches the expected value. On a
mismatch the partial file is deleted and the download fails with a typed HashMismatch error.
First-class vs. directive-driven. In normal end-to-end user flows, Nexus is the first-class download surface (the Browse/Install UI and
modde install mod). The other backends exist for completeness and are primarily exercised by Wabbajack and collection directive flows, where a manifest names exactly which transport and hash to use. That broader backend surface is Partial: solid transport code, but not all of it is wired into a polished standalone UX. See the parity audit.
Backend reference
| Backend | Directive | Auth | Resume | Notable behaviour |
|---|---|---|---|---|
| Nexus | Nexus { game, mod, file, hash } | API key + Premium | No | CDN link via download_link.json |
| GitHub | GitHub { user, repo, tag, asset, hash } | optional GITHUB_TOKEN | No (retry) | Resolves a release asset by name |
| Direct | DirectURL { url, headers, mirror_resolver, hash } | per-directive headers | Yes (Range) | Mirror fallback + range resume |
| Google Drive | GoogleDrive { id, hash } | none | No | Handles the virus-scan interstitial |
| MEGA | Mega { url, hash } | none | No | Client-side AES-128-CTR decrypt |
| MediaFire | MediaFire { url, hash } | none | Yes (via Direct) | HTML-scrapes the direct link |
| Manual | Manual { url, prompt, … } | n/a | n/a | Fails fast with guidance |
| Wabbajack CDN | WabbajackCdn { url, hash } | none | No | Wabbajack-hosted mirror archives |
Nexus
The Nexus backend asks the v1 API for a time-limited CDN URL
(…/files/{file_id}/download_link.json) and streams it to disk. Generating that link is
Premium-gated: modde validates the account first and refuses non-Premium keys. Authentication,
the six-source key chain, browse/search, collections, and update checking are all documented in the
Nexus Mods guide. This page covers Nexus only as a transport.
GitHub Releases
GitHub { user, repo, tag, asset, hash }
Resolves to a release asset by exact name within a tag (GET /repos/{user}/{repo}/releases/tags/{tag}),
then downloads browser_download_url. If a GITHUB_TOKEN environment variable is set, modde
sends it as a Bearer token — this lifts the unauthenticated rate limit and allows private-repo
assets. All requests send a User-Agent: modde header (GitHub rejects requests without one).
Transfers are wrapped in the shared retry/backoff helper (up to 3 attempts with exponential backoff
and jitter). The backend can also list releases and fetch a release summary by tag for tooling.
Direct HTTPS
DirectURL { url, headers, mirror_resolver, hash }
The most capable transport:
- Range resume. If a partial file already exists at the destination, modde sends a
Range: bytes=N-header. On a206 Partial Contentit appends to the existing file and seeds the running hash with the bytes already on disk, so resumed downloads still verify end to end. If the server ignores the range (returns200), modde restarts cleanly from scratch. - Mirror fallback. When the directive carries a
mirror_resolver, modde scrapes the resolver page for candidate URLs and tries each in order, deleting the partial file between failed candidates. A single-candidate download surfaces its own error; a multi-candidate download reports an aggregated list of every URL that failed. A resolver-suppliedUser-Agentis injected unless the directive already set one. - Custom headers. Arbitrary per-directive request headers are forwarded verbatim.
Direct is also the engine behind MediaFire (below).
Google Drive
GoogleDrive { id, hash }
Google interrupts large-file downloads with an HTML “can’t scan this file for viruses” interstitial instead of serving the bytes. modde handles both paths:
- It first hits the modern
drive.usercontent.google.com/download?id=…&confirm=thost, which usually skips the interstitial entirely. - If Google still returns
text/html, modde parses the page for the confirm token (it tries three patterns: an&confirm=TOKENquery parameter, aname="confirm" value="TOKEN"hidden input, and theid="uc-download-link"anchor) and re-requests with&confirm=<token>to fetch the real bytes.
MEGA
Mega { url, hash }
MEGA encrypts files client-side, so modde decrypts them locally:
- Both URL formats are parsed: the modern
https://mega.nz/file/HANDLE#KEYand the legacyhttps://mega.nz/#!HANDLE!KEY. - It calls the MEGA API (
https://g.api.mega.co.nz/cs) with the file handle to obtain the encrypted download URL and the file size. - The 32-byte base64url key is decoded; the AES-128 key is the XOR of its two 16-byte halves, and the IV is bytes 16–24 zero-padded (the low 8 bytes are the CTR counter, starting at 0).
- As the encrypted stream arrives, modde applies an AES-128-CTR keystream chunk by chunk and writes the plaintext to disk, then verifies the hash.
MediaFire
MediaFire { url, hash }
MediaFire serves a file page, not a direct link. modde fetches the page with a browser-like
User-Agent, scrapes the aria-label="Download file" anchor to extract its href (the real
download…mediafire.com/… URL), and then delegates the transfer to the Direct backend — so
MediaFire downloads inherit Direct’s range-resume and retry behaviour for free.
Manual
Manual { url, prompt, expected_name, … }
Some Wabbajack archives are gated behind interactive pages that cannot be fetched automatically. modde mirrors Wabbajack’s behaviour: it fails fast at resolve time with a clear, actionable message naming the expected file, the upstream URL to visit, and any prompt the list author left:
manual download required for <expected_name>: visit <url> and place the downloaded
file in the modde downloads directory. Prompt from list author: <prompt>
This is intentional — a Manual directive is a signal that you must download the file by hand and drop it into the downloads directory, after which the install can continue.
Resumable .meta sidecars
modde models each download as a JSON .meta sidecar written next to the destination file
(mod_file.zip → mod_file.zip.meta). The sidecar records:
url,expected_hash,bytes_downloaded,total_bytes, and astatusstring (queued/downloading/paused/complete/failed: …)- Nexus provenance:
nexus_mod_id,nexus_file_id,game_domain,mod_name,version
Sidecars are the persistence layer that lets a queue be rebuilt across process restarts: on load,
complete entries are restored only if the file still exists, and any downloading/paused entry is
restored as paused (because no transport is running after startup). This pairs with the Direct
backend’s range resume to continue a partial transfer rather than restart it.
Status: Partial. The sidecar data model and round-trip are implemented and tested, but writing and reloading sidecars is not yet a shipped end-to-end workflow in the GUI. Treat durable cross-restart resume as in-progress; see the parity audit.
The download queue
DownloadQueue is a pure, synchronous state machine that tracks tasks and enforces a concurrency
limit — it performs no I/O itself; the caller spawns the actual async transfers. Each task moves
through Queued → Active → {Complete | Paused | Failed}:
enqueue(url, dest, expected_hash, meta)adds a task and returns its ID.take_next()promotes the nextQueuedtask toActive, but only while the active count is below the limit — this is how concurrency is bounded.pause(id)/resume(id)move a task toPausedand back toQueued;cancel(id)removes it.save_sidecars()/load_from_sidecars(dir, limit)persist and rebuild the queue from.metafiles (see above).
The GUI currently constructs the queue with a concurrency of 2 and renders real queue state in its Downloads view.
Status: Partial. The queue’s UI state (queued/active/paused/failed/complete) is shipped, but transport-level pause/resume is not yet wired to the network layer, and there is no speed/ETA calculation yet (progress callbacks report bytes, not rate). The Wabbajack engine has its own concurrent download path (default parallelism 4) used during modlist installs.
Tuning environment variables
These environment variables tune the download and staging machinery. The byte cache, zstd staging compression, and archive-retention knobs are read by the Wabbajack install engine and are mainly relevant to large modlist installs (Partial); they are not part of the everyday single-mod download path.
| Variable | Default | Effect | Where it applies |
|---|---|---|---|
MODDE_BYTE_CACHE_MIB | 512 | Size (MiB) of the in-memory LRU cache of extracted archive entries | Wabbajack extraction (Partial) |
MODDE_ZSTD_MIN_BYTES | 1048576 (1 MiB) | Minimum file size before a staged file is zstd-compressed | Wabbajack staging (Partial) |
MODDE_ZSTD_LEVEL | 9 | zstd compression level for staged files (clamped to 1–22) | Wabbajack staging (Partial) |
MODDE_ARCHIVE_RETENTION | keep | What to do with source archives after a batch is applied: keep, prune-applied (aliases prune/delete), or auto | Wabbajack install (Partial) |
Related authentication / transport variables documented elsewhere:
| Variable | Purpose | See |
|---|---|---|
GITHUB_TOKEN | Authenticated GitHub release downloads | GitHub Releases |
NEXUS_API_KEY / NEXUS_API_KEY_FILE | Nexus credentials | Nexus Mods guide |
MODDE_ARCHIVE_RETENTION also has a CLI equivalent on the Wabbajack installer
(--archive-retention keep|prune-applied|auto); the flag wins when both are set.
See also
- Nexus Mods — the first-class download surface: auth, browse, install, updates
- Wabbajack modlists — the directive-driven install flow that exercises every backend
- Troubleshooting — download failures, hash mismatches, rate limits
- Parity audit — what is
DonevsPartial
How mod installation works
Overview
Mod archives are not uniform. A Skyrim mod might be loose files that drop into
Data/; a Cyberpunk mod might be a REDmod package keyed off info.json; a
texture pack might ship a FOMOD installer; a Wrye Bash mod might use numbered
option folders. modde turns all of these into the same end state — files staged
in a per-mod store directory and recorded so uninstall is exact — by running
every install through one pipeline that first classifies the archive, then
stages it accordingly.
This guide explains that pipeline, the full set of layouts modde recognizes, when it auto-detects versus needs your input, and what happens when it cannot figure an archive out at all.
The three-stage pipeline
Every install is analyze → execute → record.
1. Analyze
installer::analyze inspects an extracted archive and decides how to stage it,
producing an InstallPlan. The plan carries:
- a
method— one of theInstallMethodvariants below; - an optional
strip_prefix— set when the archive is wrapped in a single directory (e.g.ModName-1.0/<real contents>), so everything downstream is resolved relative to the inner directory; - the
source_archive_hash— the xxh64 digest of the original download, computed during download (it cannot be derived from the extracted tree); - an empty
staged_fileslist, filled in by the next stage.
Detection is ordered, and the order is deliberate so that authoritative, game-specific rules win before generic guesses:
- Normalize — if the extracted directory is a single wrapper directory with
no files, recurse into it and record
strip_prefix. Unwrapping stops at a directory whose only child is recognized mod content (Data/,r6/,fomod/, …) so real content roots are never mistaken for wrappers. - Game plugin —
GamePlugin::analyze_mod_archivegets first crack. A game can claim a layout it knows (Cyberpunk’s REDmod, an ENB pack for Bethesda) before any generic probe runs. - FOMOD — presence of
fomod/ModuleConfig.xml. - BAIN — numbered option subdirectories (
00 Core,01 Option, …). - DLL overlay — a top-level
.dllwith no nested asset directories. - Bare extract — the game plugin’s
recognizes_bare_layoutrecognizer. - User-config overlay — the plugin advertised a
UserConfigdeploy target and every file in the tree looks like a config file. - Unknown — fall through; the caller dumps a dossier.
2. Execute
installer::execute stages files from the temporary extraction directory into
the mod’s store directory according to the plan, and returns a concrete
StagedFile manifest. Execution is deliberately dumb: it copies (or renames,
when the source and destination share a filesystem) files, records each one’s
store-relative path, its original archive path, and its size — and does not
touch the database.
Two variants can stop here and ask for input: a Fomod with no config and a
Bain with no selection both return RequiresUserInput, which the GUI routes to
a wizard. An Unknown method returns UnknownMethod.
3. Record
The caller wires the returned Vec<StagedFile> into the database
(ModdeDb::record_install), persisting the method and the file manifest. Because
the manifest is exact, modde mod remove unlinks
precisely the files this mod staged — no orphans, no collateral damage.
The mod’s row also carries an InstallStatus:
| Status | Meaning |
|---|---|
installed | Files are staged and tracked. |
pending_user_input | Detection succeeded but execution needs answers (FOMOD wizard, BAIN picker). The archive sits extracted in a staging dir. |
unknown | Detection failed; a dossier was written. Retry is blocked until the installer is extended. |
failed | Execution started but failed partway; the store dir may hold partial files. |
Install methods
InstallMethod is the heart of detection — and the extensibility point. Variants
are ordered by specificity, so game-specific layouts take precedence over generic
ones (a Cyberpunk archive with both info.json and a Data/ folder is REDmod,
not a Bethesda bare-extract).
| Method | What triggers it |
|---|---|
BareExtract | Game plugin’s recognizes_bare_layout returns true — the archive maps 1:1 onto the game’s mod dir (e.g. loose files that drop straight into Skyrim’s Data/). |
StripContentRoot { root } | Archive nests its payload under a content root that must be peeled before staging (e.g. Data/Foo.esp → Foo.esp when the deploy root is already Data/). Emitted by a game plugin’s analyze hook. |
DirectoryMod { directory_name } | Archive root is one self-contained directory-style mod; files stage under a stable subdirectory (defaults to the store dir name). |
DirectoryModFromXml { marker, id_attr, fallback_name } | Same as DirectoryMod, but the stable directory name is read from an XML marker attribute — Bannerlord’s SubModule.xml is the canonical case. |
MultiRootOverlay { roots } | Archive carries several game-root overlay directories at once (mods/, dlc/, bin/, …); each is staged under its own root. |
SingleFileSet | One or more loose files that stage directly into the resolved mod root. |
Fomod { module_config, config_toml } | fomod/ModuleConfig.xml is present. config_toml holds the chosen options; None means the wizard must run before execution. See the FOMOD guide. |
REDmod { manifest } | A Cyberpunk REDmod package — detected by a game plugin via its info.json manifest (plus archives/). Staged under mods/<name>/. |
Bain { selected_subdirs } | Wrye Bash layout: ≥2 numbered option subdirs like 00 Core, 01 Option. selected_subdirs is empty until the user picks options. |
DllOverlay { target_dir_hint } | A top-level .dll with no nested asset directories (dxvk, ENB, proxy hooks). Files route to the game’s executable directory at deploy time. |
ScriptMerge { merge_group, base } | Reserved for script-merge support. Today it stages per base and tags every file with merge_group; actual merging is not yet wired in. |
UserConfigOverlay { target_id } | The game plugin advertised a UserConfig deploy target and every file in the archive is a config file (.ini, .cfg, .json, .xml, …). Staged like a bare extract; the alternate routing only applies at deploy time. |
Unknown { reason } | Nothing matched. A dossier is dumped so the installer can be extended. |
Auto-detected vs. needs input
Most methods are fully automatic — analysis classifies the archive and execution stages it with no further interaction. The exception is the two interactive formats:
- FOMOD — auto-detected (the
fomod/directory is unmistakable), but execution needs the user’s option choices. Supply them interactively via the GUI wizard, or non-interactively with a declarative config (--fomod-config, ormodde fomod apply). Until a config is present the mod sitspending_user_input. See the FOMOD guide. - BAIN — auto-detected by the numbered-subdir convention, but modde cannot
know which options you want;
selected_subdirsstarts empty and the GUI prompts you to pick. Without a selection, execution returnsRequiresUserInput.
Everything else (BareExtract, StripContentRoot, DirectoryMod,
DirectoryModFromXml, MultiRootOverlay, SingleFileSet, REDmod,
DllOverlay, UserConfigOverlay) is is_ready() the moment analysis finishes.
A note on wrapper stripping: any of the above can be wrapped in a versioned
directory. The analyzer’s normalize step records a strip_prefix so execution
recurses into the inner directory automatically — you never have to repack an
archive just because the author zipped it with a ModName-1.0/ top folder.
Unknown archives and the dossier system
When detection falls through to Unknown (or execution surfaces
UnknownMethod), modde does not silently fail or guess. It writes a dossier —
a self-contained bundle that captures everything a developer (or a Claude Code
skill) needs to teach the installer this new layout.
Where it lives
$XDG_DATA_HOME/modde/unknown-installers/<slug>/
├─ metadata.json — mod + context (game, Nexus ids, name, author, hash)
├─ archive_tree.txt — recursive listing of the extracted archive (≤500 entries)
├─ file_samples/ — up to 5 small text files copied verbatim
│ (READMEs, info.json, ModuleConfig.xml, meta.ini, …)
├─ analyzer_trace.json — which probes ran and how each voted
└─ PROMPT.md — a self-contained skill prompt
The <slug> is stable across retries — usually <domain>_<mod>_<file> for a
Nexus install, falling back to <game>_<hash-prefix> otherwise — so retrying the
same mod overwrites the same dossier rather than piling up duplicates.
What PROMPT.md contains
PROMPT.md is the actionable core. It restates the mod’s identity and the
detection verdict, then lays out exactly how to extend support:
- Read
archive_tree.txtandfile_samples/to understand the layout. - Decide where the fix belongs — a generic package format gets a new
InstallMethodvariant plus a detection branch inanalyze.rs; a game-specific quirk extends that game’sanalyze_mod_archiveimpl incrates/modde-games/. - Extend
execute.rsif the new variant needs custom staging. - Add a unit test built from the dossier’s file samples.
- Run the installer test suite.
- Rename the dossier directory with a
.resolvedsuffix so the UI surfaces a Retry Install button.
Reading a dossier from the CLI
modde mod diagnose <mod_id>
mod diagnose locates the dossier for an unknown-install mod and prints its path
followed by the inline PROMPT.md to stdout. It is read-only — it mutates
nothing — so it is safe to pipe straight into an agent or paste into a chat:
modde mod diagnose skyrimspecialedition_12345_67890 | claude
This is how an unrecognized layout becomes supported: the dossier turns “modde
doesn’t know what this is” into a precise, reproducible task, and once a fix lands
and the dossier is marked .resolved, the original install can be retried with no
re-download.
See also
- FOMOD installer — the interactive/declarative install method in depth.
- Deployment — how staged files reach the game directory.
- Conflicts & load order — what happens when two installs stage the same file.
- Mod scanning — discovering mods already installed on disk.
- Generic & user-defined games — how a game plugin
advertises the deploy targets that
UserConfigOverlayroutes to. - CLI reference —
modde install,modde mod,modde fomod.
FOMOD Installer
Overview
FOMOD is the most common scripted installer format on Nexus. Instead of shipping a flat archive that drops into the game’s mod directory, a FOMOD mod ships an XML script that asks the user questions — texture resolution, body type, compatibility patches, which optional plugins to enable — and copies a different set of files depending on the answers.
modde supports FOMOD two ways:
- Interactively, via a step-by-step wizard in the GUI.
- Declaratively, via a config file you can generate, edit, version, and replay non-interactively. This is what makes FOMOD installs reproducible and what lets a Home Manager profile pin a known-good set of answers.
Detection is automatic: during analysis modde looks for fomod/ModuleConfig.xml
inside the extracted archive and classifies the mod as
InstallMethod::Fomod. FOMOD detection runs after a
game plugin’s own probe, so a game that recognizes a layout authoritatively (for
example Cyberpunk’s REDmod) still wins over a stray fomod/ directory.
The FOMOD format
A FOMOD installer lives in a fomod/ directory at the archive root and is
driven by two files:
| File | Required | Purpose |
|---|---|---|
fomod/ModuleConfig.xml | yes | The installer script: steps, groups, options, conditions, file operations. |
fomod/info.xml | no | Display metadata: mod name, author, version. modde reads version (falling back to name) to stamp a revision on generated configs. |
modde locates ModuleConfig.xml case-insensitively, so archives that ship
FOMOD/ModuleConfig.xml or fomod/moduleconfig.xml are handled.
Structure: steps, groups, options
ModuleConfig.xml is a small tree. From outermost to innermost:
-
Module — the whole installer. Has a name and, optionally, a set of required install files (copied unconditionally regardless of answers).
-
Install steps — ordered pages of the wizard. Each step has a name and is shown one at a time. A step can be gated by a visibility condition so it only appears when an earlier answer set the right flag.
-
Groups — within a step, options are bucketed into named groups. A group has a type that controls how many options you may pick:
Group type Selection rule SelectExactlyOneradio buttons — exactly one option SelectAtMostOneradio buttons plus “none” SelectAtLeastOnecheckboxes — at least one required SelectAnycheckboxes — any number, including none SelectAllevery option is forced on -
Plugins (options) — the individual choices inside a group. Each has a name, an optional description, an optional image, a plugin type (
Required,Optional,Recommended,NotUsable,CouldBeUsable), the set of files/folders it copies when selected, and the flags it sets.
Flags and conditions
FOMOD is stateful. Selecting an option can set a named flag to a value, and later steps, groups, plugin type descriptors, or a final conditional file installs block can read those flags to decide what to show or what to copy. This is how a FOMOD says “if the user picked the 2K texture option in step 1, also install this 2K-only patch at the end.” modde’s installer applies your declarative selections, lets the underlying engine resolve every flag and condition, and only then computes the concrete file operations.
Interactive installation
When you install a mod that contains a FOMOD installer, modde detects it
automatically. In the GUI, a step-by-step wizard walks each install step, honors
the group selection rules above, evaluates flags/conditions as you go, and stages
the resulting files. Your selections are saved to the profile (fomod_config)
and replayed on future deployments, so re-deploying a profile never re-prompts.
On the CLI, modde install mod does not open an interactive wizard. A FOMOD
mod installed without answers is staged as pending user input — analysis
succeeds but execution returns RequiresUserInput until a config is supplied.
For headless installs, generate a config and pass it with --fomod-config
(below).
Declarative installation
For reproducible, non-interactive installs, define your FOMOD choices in a config
file. The on-disk format is a serialized fomod_oxide::DeclarativeConfig; modde
can emit it as TOML, JSON, or Nix.
Generating a config template
# Generate a template with default selections (recommended/required options)
modde fomod generate /path/to/mod --format toml
# Include all plugins, not just the defaults, so you can flip any of them on
modde fomod generate /path/to/mod --all --format toml
The path is a mod directory that contains fomod/ModuleConfig.xml (for example
an extracted archive, or a mod’s store directory). generate prints the config
to stdout — redirect it to a file you then edit:
modde fomod generate /path/to/mod --all --format toml > my-choices.toml
--all vs. defaults:
- Without
--all, the template encodes the installer’s default answers —RequiredandRecommendedoptions pre-selected. Good for “I just want the author’s intended install, pinned.” - With
--all, every option is enumerated so you can toggle any of them. Good for “I want the 2K textures and the optional patch, not the defaults.”
Output formats:
--format | Notes |
|---|---|
toml | Default. The format --fomod-config and fomod apply accept directly. |
json | Equivalent content; accepted by fomod apply when the config path ends in a non-.toml extension. |
nix | Requires the nix feature on fomod-oxide. The stock binary returns an error for --format nix — build with that feature, or generate TOML/JSON and convert. |
Applying a config to a directory
fomod apply resolves a config against a mod’s ModuleConfig.xml and writes the
selected files into a destination directory, without touching any profile:
modde fomod apply /path/to/mod --config my-choices.toml --dest /path/to/output
This is the low-level primitive — useful for scripting, testing your answers, or
inspecting exactly which files a given set of choices produces. It prints the
number of file operations and the destination. The config may be TOML or JSON;
modde parses TOML when the path ends in .toml and JSON otherwise.
During mod installation
To install straight from Nexus with answers pinned, pass the same config to
install mod:
modde install mod https://nexusmods.com/skyrimspecialedition/mods/12345 \
--profile my-skyrim \
--fomod-config my-choices.toml
modde downloads and extracts the mod, detects the FOMOD, applies your config in place of the wizard, and records the result in the profile. Because the config is stored with the profile, the install is reproducible: the same archive plus the same config always stages the same files. This is the path to use from Home Manager or any non-interactive automation.
Inspecting FOMOD structure
See what options a mod offers without installing anything:
modde fomod inspect /path/to/mod
inspect prints:
- the module name;
- the count of required install files (copied unconditionally), if any;
- each step with its name;
- each group within a step, with its name and group type
(e.g.
SelectExactlyOne); - each plugin/option within a group, with its index, name, plugin type, file count, and a one-line description preview.
Use it to understand a complex installer before you generate a config — the index numbers and names line up with what you will edit in the generated TOML.
A typical workflow for a tricky mod:
# 1. See the shape of the installer.
modde fomod inspect ./extracted-mod
# 2. Generate an editable template with every option exposed.
modde fomod generate ./extracted-mod --all --format toml > choices.toml
# 3. Edit choices.toml to pick the options you want.
# 4. Dry-run the result into a scratch directory and check the files.
modde fomod apply ./extracted-mod --config choices.toml --dest /tmp/fomod-out
# 5. Use the same config for the real, profile-tracked install.
modde install mod <nexus-url> --profile my-skyrim --fomod-config choices.toml
Troubleshooting
no fomod/ModuleConfig.xml found in <path>
generate, apply, and inspect all require a fomod/ directory with a
ModuleConfig.xml inside the path you pass. Common causes:
- You pointed at the archive instead of the extracted directory — extract first.
- The mod has a wrapper directory (
ModName-1.0/fomod/...). Point at the inner directory, or let the fullinstallpipeline handle it (its analyzer strips single-directory wrappers automatically). - The mod is simply not a FOMOD. Run
modde fomod inspectto confirm; if it is a plain archive, install it normally — no config needed.
failed to parse ModuleConfig.xml / FOMOD parse error
The installer’s XML is malformed or uses a construct modde’s parser does not
accept. Open fomod/ModuleConfig.xml and check for unescaped &, mismatched
tags, or a non-UTF-8 encoding. Some old mods ship a hand-edited script; fixing
the XML in place and re-running usually resolves it. If the script is valid but
still rejected, capture it and file an issue.
FOMOD apply error / invalid config TOML
The declarative config does not match the installer it is being applied to. This happens when:
- The config was generated for a different version of the mod. Re-run
fomod generateagainst the current archive — option names and indices change between versions. - You hand-edited the TOML/JSON and introduced a typo, a renamed option, or a
selection that violates a group rule (for example two options checked in a
SelectExactlyOnegroup). Re-generate and re-edit minimally.
installer requires user input (fomod) — run the wizard first
You installed a FOMOD mod on the CLI without --fomod-config. The mod is staged
but cannot finish until it has answers. Either install it from the GUI (which
runs the wizard) or supply a config:
modde fomod generate <mod-store-dir> --all --format toml > choices.toml
# edit choices.toml, then re-run install with --fomod-config choices.toml
Step or group does not appear / wrong files installed
FOMOD steps and options can be gated by flags set earlier in the installer. If a
step you expected never shows, an earlier answer did not set the flag that step’s
visibility condition requires. Re-run modde fomod inspect to review the steps,
then adjust the earlier selections in your config so the dependent step’s
condition is satisfied. Because conditions are evaluated by the engine at
apply/resolve time, the surest check is a fomod apply into a scratch
directory followed by inspecting the staged file tree.
See also
- How mod installation works — where FOMOD fits in the analyze → execute → record pipeline.
- Installing from Nexus — acquiring the archives FOMOD installers ship in.
- Deployment — how staged FOMOD output reaches the game.
- CLI reference:
modde fomod— flag-by-flag command reference.
Mod Scanning
Overview
The modde scan command discovers mods already installed on disk by analysing
the game directory structure. Use it to import an existing setup into modde
(a manual install, a Windows-era deployment, mods you placed by hand) or to
track an already-deployed Wabbajack list without reinstalling it.
Scanning works two ways, and they compose:
- Filesystem scan — a game-specific scanner walks known mod directories and recognises mods from their on-disk layout. This needs no manifest and works for any supported game.
- Wabbajack manifest matching — when you pass
--manifest <list>.wabbajack, modde matches the manifest’s archives against the files on disk, recovering each mod’s Nexus identity and the list’s load order.
modde scan --game cyberpunk2077
modde resolves the game’s scanner, auto-detects the install (or uses
--game-dir), builds a case-insensitive index of every file under the install,
and reports what it finds. Nothing is written unless you pass --import-to.
Game-specific scanners
Each supported game has a scanner that understands that game’s mod conventions. Scanners are assembled declaratively from a small set of composable rules, so a new game’s scanner is a list of “this directory holds mods of this shape” declarations rather than bespoke walking code.
DirectoryModRule — one subdirectory per mod
Treats each immediate subdirectory of a base directory as a single mod. The subdirectory name becomes the mod id and display name; all files beneath it are the mod’s footprint. An optional marker file raises the confidence score when present.
Examples: Cyberpunk CET mods under
bin/x64/plugins/cyber_engine_tweaks/mods/ (marker: init.lua), REDscript mods
under r6/scripts/, TweakXL mods under r6/tweaks/, and REDmod mods under
mods/.
SingleFileModRule — one file per mod
Treats each file with a given extension directly inside a directory as a mod. The file stem becomes the mod id/name. Configurable ignored prefixes skip known stock/engine files so they are not reported as user mods.
Example: each loose .archive under Cyberpunk’s archive/pc/mod/.
FileGroupRule — group sidecar files into one mod
Groups files that share a stem across several extensions into a single mod — so
a .pak plus its .ucas/.utoc sidecars (Unreal Engine games) are reported as
one mod, not three. It also descends one level into subdirectories so packaged
mods are grouped correctly.
What a scan emits
Every rule produces a DiscoveredMod with a stable mod_id (prefixed by the
rule, e.g. cet/<name>, archive/<stem>, redmod/<name>), a display name, the
list of files it owns, the scan location, and a confidence score. The scanner
also exposes its scan_directories() (printed at the top of a scan) and a
mod_id_footprint() mapping used by deduplication.
When to use which
| Situation | Use |
|---|---|
| Imported a manual / hand-placed install, no manifest | Filesystem scan — modde scan --game <id> |
You have the original .wabbajack for a deployed list | Manifest matching — add --manifest <list>.wabbajack |
| Both: a Wabbajack base plus your own extra mods | Both at once — manifest recovers Nexus identity + load order; the filesystem scan catches your additions |
Manifest matching is strictly more informative when you have the list: it recovers real Nexus mod/file ids (so updates and the mod-information side panel work), and it reconstructs the list’s load order as a lock. The filesystem scan is the fallback when no manifest exists, and the complement that finds mods the manifest never deployed.
Importing results into a profile
modde scan --game cyberpunk2077 --import-to my-cyberpunk
Discovered mods are merged into the named profile. If the profile doesn’t exist,
it’s created. Merging is additive and non-destructive: existing rows keep
their per-mod state (notes, category, per-mod locks) and only new mod_ids are
brought in.
Wabbajack manifest matching
When migrating from a Wabbajack install, match on-disk files against the manifest:
modde scan --game skyrim-se \
--manifest /path/to/modlist.wabbajack \
--import-to my-modlist
This:
- Parses the
.wabbajackmanifest. - Groups its
FromArchive/PatchedFromArchivedirectives by source archive and checks what fraction of each archive’sTopaths exist on disk. - Imports matched archives as mods, carrying their Nexus mod/file id and game
domain when the archive is
NexusDownloader-sourced. - Applies a Wabbajack load-order lock to the profile, reordering matched
mods to follow the manifest’s install-directive order (the closest
reproducible approximation of Wabbajack’s load order) and stamping a
manifest_hashso the lock is self-identifying. Mods not mentioned by the manifest keep their relative order and are appended after.
The .wabbajack source file is copied into modde’s content-addressed cache so
profile lock-info can find it later.
Matched mods use the same canonical mod_id scheme as a real
modde install wabbajack (Nexus archives → nexus_<domain>_<mod>_<file>,
others → wj_<hash>), so re-scanning a list you installed through modde matches
the existing rows instead of creating duplicates.
Threshold filtering
The --threshold flag controls how many of a mod’s expected files must be
present on disk for the archive to count as matched (present files ÷ expected
files):
# Require 80% of expected files to be present
modde scan --game skyrim-se --threshold 0.8 --manifest /path/to/modlist.wabbajack
# More lenient matching (default is 0.5)
modde scan --game skyrim-se --threshold 0.3 --manifest /path/to/modlist.wabbajack
Raise the threshold to avoid false positives (mods that share a few common
files); lower it when a list does heavy file overwriting and few archives keep
100% of their files intact. The scan report prints present/total and a
confidence percentage per matched archive so you can tune it empirically.
Dry-run mode
Preview what would be discovered without writing to the database:
modde scan --game skyrim-se --dry-run
--dry-run works with and without --manifest and --import-to. With
--import-to it reports how many mods would be imported but makes no changes —
always run a dry pass first on a large list.
Pruning duplicates
A filesystem scan and a manifest match can describe the same mod two
different ways: the manifest tracks it under its nexus_* id, while the
filesystem scanner may also pick up its directory (e.g. a CET mod folder that now
also contains a runtime-written settings.json the manifest never deployed).
modde skips filesystem mods that are already covered by the manifest during a
combined scan, using a directory-prefix coverage check for directory-based
mods and a per-file check for single-file mods — so a stray runtime file no
longer causes a whole mod to be re-added as a duplicate.
To also clean up duplicates that a previous (buggier) scan already leaked into a
profile, pass --prune-duplicates. It classifies each filesystem-scanner row in
the target profile against the manifest before merging: rows whose footprint
the manifest covers are removed as leaked duplicates; genuine additions (your
mods, not in the list) are preserved. It requires both --manifest and
--import-to:
modde scan --game skyrim-se \
--manifest /path/to/modlist.wabbajack \
--import-to my-modlist \
--prune-duplicates
For an existing profile you don’t want to re-scan, use the standalone equivalent:
modde profile dedup my-modlist --manifest /path/to/modlist.wabbajack --apply
Post-scan review workflow
A scan is a proposal, not a commit. Review before trusting it:
- Dry-run first.
modde scan --game <id> [--manifest …] --dry-runand read the report: the scanner’s scan directories, the manifest match table (present/total, confidence), the filesystem table (file count, location, confidence), and how many filesystem mods were skipped as already-covered. - Tune
--thresholdif the manifest match rate looks wrong — too many low-confidence matches means lower it is masking false positives; too few matches on a list you know is installed means raise the bar is too high. - Import with
--import-to <profile>once the proposal looks right. - Dedup if you imported into a profile that had prior filesystem-scan rows:
add
--prune-duplicates(or runmodde profile dedup … --apply). - Verify the load order. When a manifest was used, the profile gets a
Wabbajack lock; check it with
modde profile lock-info <profile>and review the mod order in the UI before deploying. - Deploy. Scanning only records what modde found on disk; run a normal deploy to bring the profile under modde’s management. See Deployment.
See also
- Wabbajack modlists — installing lists; the manifest format scanning matches against
- Profiles — load-order locks and
profile dedup - Deployment — applying a reviewed profile to the game
- Conflicts — resolving overlaps after import
- Supported games — which games have scanners
- CLI reference — full
scanflag list
Profile Management
What a profile is
A profile is the central organizing concept in modde. Concretely, one profile is a per-game bundle of four things:
- An ordered mod set — the list of
EnabledModentries (each with anenabledtoggle, version, Nexus metadata, category, tags, notes, and an optional per-mod lock). Position in the list is the install priority: later mods win file conflicts. See Conflicts & priority. - A load order — the mod ordering above, plus an independent plugin order (
.esp/.esm/.esl) for engines that distinguish the two, plus declarativeload_order_rules(load_after/load_before/incompatible). - Save assignments — a branch in the game’s git-backed save vault, swapped automatically whenever the active profile changes. See Save management.
- Deployment state — the
overridesdirectory and the per-mod file manifest that records exactly what was staged, so deployment and uninstall are precise. See Deployment.
Profiles are stored in modde’s SQLite database (modde.db), one row per profile, scoped by (name, game_id) — the pair is unique, so you can have a default profile for Skyrim and a default profile for Fallout 4 without collision. A profile also carries provenance (ProfileSource: Manual, NexusCollection, or Wabbajack) and an optional load order lock (covered below).
Where profiles, the database, saves, and the rest of modde’s state live on disk is documented separately in Data, instances & backups.
Creating profiles
Via home-manager (declarative)
programs.modde.profiles.my-skyrim = {
game = "skyrim-se";
};
For Wabbajack profiles, Home Manager can also manage the first install. If the game is not installed yet, keep the profile in an awaiting state:
programs.modde.profiles.my-skyrim = {
game = "skyrim-se";
installMode = "await-game";
wabbajackList = {
url = "https://example.com/modlist.wabbajack";
hash = "sha256-...";
};
};
Use wabbajackList.path instead when the .wabbajack file is already present
locally or in the Nix store.
After installing the game through Steam or Heroic, set gameDir and switch back
to the default installMode = "auto". The full option set lives in the
Home-Manager module reference.
Via CLI (imperative)
modde profile create my-skyrim --game skyrim-se
Profile names are validated before they touch the database: they cannot be empty,
cannot exceed 255 characters, and cannot contain filesystem-unsafe characters
(/ \ NUL : * ? " < > |) because the name doubles as an on-disk directory under
<modde_data>/profiles/<name>/.
Listing, inspecting, and switching
# List all profiles (grouped by game)
modde profile list
# List profiles for a specific game
modde profile list --game skyrim-se
# See which profile is active (and the current experiment depth)
modde profile active --game skyrim-se
# Switch to a profile (automatically swaps saves)
modde profile switch my-skyrim --game skyrim-se
profile list shows the profile name, game, mod count, and source type for each
row. profile active additionally reports the experiment depth (see
Experiment stack below) so you can tell whether you are
sitting on a try/rollback chain.
What switching actually does
When you switch profiles, modde:
- Captures the current profile’s saves into its vault branch (with a mod fingerprint embedded for later compatibility warnings).
- Restores the target profile’s saves from its branch.
- Sets the target as the active profile for that game.
If existing saves are detected on disk with no active profile assigned (you
modded the game before adopting modde), the switch reports an adoption required
state instead of silently overwriting anything — adopt them first with
modde save adopt (see Save management).
Forking profiles
fork creates a full clone of a profile — mods, load order rules, locks, and a
forked save branch — under a new name. The source profile is never modified.
modde profile fork my-skyrim my-skyrim-experimental --game skyrim-se
By default a fork is a faithful copy: if the source carried a profile-level load order lock (for example because it was installed from a Wabbajack list), the fork inherits that lock and every per-mod pin. This is what you want when you are duplicating a known-good setup to keep as a backup.
To create a freely editable copy that you intend to diverge from, pass
--unlock:
modde profile fork my-skyrim my-skyrim-custom --game skyrim-se --unlock
--unlock strips both the profile-level load_order_lock and every per-mod pin
from the new profile, so you can reorder it however you like. The source remains
locked and untouched. This is the canonical “I want to experiment on top of a
Wabbajack list without breaking the authoritative ordering” workflow.
Load order locking
Reordering a mod list that came from an authoritative source (a Wabbajack manifest, a Nexus Collection, or a shared TOML export) usually breaks it. modde models this with a lock, which comes in two granularities: a whole-profile lock and a per-mod pin.
Automatic locks
A profile-level lock is applied automatically when a profile is created from an
authoritative source, recording why in a structured LockReason:
| Source | LockReason | Provenance recorded |
|---|---|---|
| Wabbajack modlist | Wabbajack | manifest_hash (verifies which manifest) |
| Nexus Collection | NexusCollection | slug + version |
TOML import (modde import) | TomlImport | source_path at import time |
TOML import is preserve-before-overwrite: if the imported TOML file already
carried a lock (for example, you previously exported a Wabbajack-installed
profile), modde honors that original provenance and only stamps a fresh
TomlImport lock when the profile arrives unlocked. Round-tripping an
already-locked profile through TOML never destroys its origin.
Note that provenance (ProfileSource) and locking (LoadOrderLock) are now
separate: the source field is metadata only, while all reorder-blocking
business logic is driven by the lock. Forking with --unlock clears the lock but
leaves the provenance intact.
Manual locks
You can lock any profile by hand — useful once you have hand-tuned a load order and want to stop yourself from nudging it later:
# Lock a profile with an explanatory note
modde profile lock my-skyrim --note "tested and stable"
# Check lock status (shows the LockReason and the locked-at timestamp)
modde profile lock-info my-skyrim
# Unlock
modde profile unlock my-skyrim
A manual lock records LockReason::Manual { note } and an ISO-8601 UTC
locked_at timestamp.
Per-mod pins
Sometimes only a handful of mods are order-sensitive (a patch that must sit right after its parent, say). Pin those individually while leaving the rest of the list freely reorderable:
# Pin a mod in place (per-mod lock)
modde profile lock-mod my-skyrim "skyrim_12345_67890" --note "load order sensitive"
# Release the pin
modde profile unlock-mod my-skyrim "skyrim_12345_67890"
When a profile is locked at the profile level you can still add new mods — they append to the end of the list — but you cannot reorder or remove locked mods. Per-mod pins protect individual entries even on an otherwise unlocked profile.
The reorder enforcement model (ReorderError)
Every attempt to move a mod one step up or down — whether triggered from the UI’s mod list or a CLI path — funnels through one shared function so all callers refuse identically. The checks short-circuit in this precedence order, and on refusal the profile is left completely untouched:
| Order | Condition | Refusal (ReorderError) |
|---|---|---|
| 1 | The whole profile is locked | ProfileLocked { reason } |
| 2 | The target mod_id isn’t in the profile | ModNotFound { mod_id } |
| 3 | The target mod itself carries a pin | ModPinned { mod_id, reason } |
| 4 | The move would run off the top/bottom of the list | AtBoundary |
| 5 | The swap partner (the neighbor one step over) is pinned | AdjacentPinned { neighbor_id, reason } |
The fifth case is the subtle one: moving mod A past a pinned neighbor B
would shift B, which violates B’s pin contract — so the move is refused even
though A itself is unpinned. Each variant carries enough structure (the
offending id, the LockReason) for the UI or CLI to explain why without
string-matching an error message.
Experiment stack
The experiment stack lets you try profile changes non-destructively, like git
branches for your mod setup. try pushes the currently-active profile onto a
per-game stack and activates a different one; rollback pops back one level;
commit accepts wherever you’ve landed and clears the history.
# Push the current profile onto the stack and activate "experimental-skyrim"
modde profile try experimental-skyrim --game skyrim-se
# Try another on top (the stack grows)
modde profile try ultra-graphics --game skyrim-se
# Something broke? Roll back one level
modde profile rollback --game skyrim-se
# Happy with where you are? Commit — clears the whole stack
modde profile commit --game skyrim-se
How depth works — a worked example
The experiment depth is simply the number of entries on the stack. profile active reports it so you always know how deep you are. Start with profile A
active and depth 0:
| Command | Stack (bottom → top) | Active | Depth |
|---|---|---|---|
| (start) | [] | A | 0 |
try B | [A] | B | 1 |
try C | [A, B] | C | 2 |
rollback | [A] | B | 1 |
rollback | [] | A | 0 |
try B | [A] | B | 1 |
commit | [] | B | 0 |
The two important rules:
rollbackfrom depth 0 is an error (NotInExperiment). There is nothing on the stack to pop back to — you are not in an experiment.commitrequires depth ≥ 1. It accepts the current active profile and throws away the rollback history; it does not revert anything. In the table above, the finalcommitleaves B active and the stack empty.
Each try and each rollback swaps saves along with the profile, exactly like a
plain switch, and embeds the mod fingerprint in the capture so later restores
can warn about mismatches. See Save management for the fingerprint
mechanics.
Forgetting to
commitis harmless — you simply stay “in an experiment” with a non-zero depth. The only consequence is thatrollbackremains available until you commit. The stack is per game, so experimenting in Skyrim never touches your Fallout 4 state.
Categories and organization
Each profile can group its mods into categories (collapsible separators with an optional color), and each mod row carries notes and tags for filtering and export:
mod_categories(name, color, sort_index)— named, ordered, optionally colored groups. Deleting a category does not delete its mods; theircategory_idis simply nulled out (they become uncategorized).EnabledMod.category_id— the category a mod belongs to.EnabledMod.notes— free-text per-mod notes.EnabledMod.tags— a list of strings (stored as a JSON array) for filtering.
This mirrors MO2’s separator/category model. The data layer also keeps mod priority (install order — which files win, driven by list position) and plugin order (which plugin loads in the engine, stored independently per profile) as two distinct orderings, the same left-pane/right-pane split MO2 uses. For the precise feature-by-feature comparison against MO2 — including the organization features that are tracked in the schema but not yet surfaced in the UI — see the Parity audit.
Deduplication
When you combine a Wabbajack install with a filesystem scan (see
Scanning), the scanner can re-detect mods the Wabbajack manifest
already installed, leaving duplicate rows in the profile. The dedup command
finds and optionally removes those leaked rows. It runs in two layers:
Layer 1 — heuristic (no --manifest). A read-only report. On a locked
profile it lists filesystem-scanner rows whose ids look manifest-owned
(cet/*, reds/*, tweak/*, archive/*, redmod/*) as suspects. It never
deletes anything.
# Heuristic suspects only — pure dry-run
modde profile dedup my-modlist --game cyberpunk2077
Layer 2 — manifest classification (--manifest <path>). Uses the
.wabbajack file’s install directives as the authoritative reference to classify
each suspect as LEAKED (the manifest installs it, so the scanner row is a
duplicate — safe to delete) or GENUINE (a real user addition the manifest
does not cover — keep it).
# Classify against the manifest, but don't delete (dry-run report)
modde profile dedup my-modlist --manifest /path/to/modlist.wabbajack
# Actually delete the rows classified as LEAKED
modde profile dedup my-modlist --manifest /path/to/modlist.wabbajack --apply
--apply is the only thing that mutates the profile, and it only removes rows
classified LEAKED — GENUINE user additions are always preserved.
Deleting profiles
modde profile delete my-skyrim
# or, when the name is ambiguous across games:
modde profile delete my-skyrim --game skyrim-se
Deleting a profile is a cascade: the database foreign keys are declared
ON DELETE CASCADE, so removing the profile row also removes every dependent
record in one shot — its profile_mods, load_order_rules, assigned saves
rows, hidden_files, plugin_order, mod_categories, installed_mod_files
manifest, and any experiment_stack / active_profiles entries that pointed at
it.
What delete does not touch: the content-addressed mod store, the downloads cache, the stock vanilla snapshot, or the git save vault on disk. The save vault is the authoritative history of your saves and is intentionally preserved — deleting a profile does not throw away its save snapshots. Manage those through the data directory (see Data, instances & backups).
If a name exists for more than one game, the command errors with an
ambiguous profile message listing the candidate games; disambiguate with
--game.
Decision tree: lock, pin, or fork?
Use this to decide how to protect (or escape) a load order:
You have a working, order-sensitive setup. What do you want to do?
│
├─ Keep it frozen exactly as-is, but keep using it
│ └─ A few mods are order-sensitive, rest is free → lock-mod (per-mod pins)
│ └─ The whole order matters → profile lock (--note)
│
├─ It came from Wabbajack / a Collection / a TOML import
│ ├─ I just want to RUN it, never edit it → leave the automatic lock on
│ ├─ I want to tweak a FEW things on top of it → fork --unlock, then edit the fork
│ └─ I want a safety copy before I touch anything → fork (faithful, keeps the lock)
│
├─ I want to A/B test changes and bail if they break → try … / rollback … / commit
│
└─ I'm done hand-tuning and never want to nudge it again → profile lock --note "final"
Rules of thumb:
- Lock when you are the source of truth and want to stop future you from fat-fingering the order. Unlock when you genuinely need to edit.
- Pin (
lock-mod) when only specific mods are fragile and you still want to reorganize everything else. - Fork when you want to diverge from an authoritative list — use
--unlockto start editable, or a plain fork to keep a frozen safety copy. - Experiment (
try/rollback) when you want reversible A/B testing of whole-profile swaps without committing to a fork.
See also
- Save management — git-backed vaults, fingerprinting, and the auto-swap that rides along with every switch/try/rollback
- Deployment — how the active profile’s mods reach the game directory
- Conflicts & priority — what “load order” means for file overrides
- Wabbajack lists — installing the locked profiles
dedupand--unlockexist for - Scanning — the filesystem detection that
dedupreconciles against a manifest - Data, instances & backups — where profiles, the database, and save vaults live on disk
- Playing with modde — deploy and launch the active profile
- Parity audit — the precise
Done/Partialstatus of every profile and organization feature - Home-Manager module reference — declaring profiles in Nix
Conflicts & Load Order
Two different kinds of ordering
Modding has two independent ordering systems, and conflating them is the single most common source of confusion:
| Concept | What it orders | Who resolves it |
|---|---|---|
| Mod install priority | Which file wins when two mods ship the same path | The VFS — later mod in the list overrides earlier |
| Plugin load order | The order Bethesda .esp/.esm/.esl plugins load | LOOT + plugins.txt — a separate axis |
Install priority decides what lands on disk (textures, meshes, scripts, configs). Plugin load order decides how the game engine merges plugin records at runtime. A mod can win the file fight and still need its plugin sorted correctly — these are orthogonal. The first half of this page is install-priority conflicts; the second half is Bethesda plugin order.
Mod conflicts (install priority)
When multiple enabled mods provide the same relative path, only one version can be deployed. modde resolves this by load order priority: walking the resolved order, the last (highest-priority) provider of a path wins. Hidden files and profile overrides shift the winner, exactly as in deployment.
Analysing conflicts
# Show config and dangerous collisions
modde collisions --profile my-skyrim
# Include cosmetic collisions too
modde collisions --profile my-skyrim --all
By default, pairs whose worst severity is purely cosmetic are hidden; --all
shows them. The report contains:
- Collision pairs —
[SEVERITY] loser vs winner (N files), each followed by per-file lines - Per-file detail —
path -> winner: <mod>with origin/hidden tags - Shadowed mods — mods whose every file is overridden by higher-priority mods
- Loose vs archive conflicts — a loose file overriding archive contents (or vice versa)
- Redundant files — files that never win (always overridden)
Collision severity
Severity is classified per file from its extension by the game’s classifier. The four levels, lowest to highest risk:
| Severity | Display | Meaning | Typical extensions |
|---|---|---|---|
| Cosmetic | COSMETIC | Visual/audio only; low risk | dds, nif, png, wav, hkx, bsa |
| Config | CONFIG | May change behaviour | ini, cfg, json, toml, xml |
| Dangerous | DANGEROUS | Scripts/plugins/DLLs; crashes or save corruption | esp, esm, esl, dll, pex, psc |
| Unknown | UNKNOWN | Extension not in the game’s table | anything unrecognised |
A mod pair’s reported severity is the worst severity among its colliding files. Pairs are sorted most-severe first, then by file count.
Per-game classifiers
Each game supplies its own classifier — both the severity table and which extensions count as indexable archives:
| Game family | Archive extensions | Notable Dangerous extensions |
|---|---|---|
| Bethesda (Skyrim, Fallout, Starfield) | bsa, ba2 (indexed) | esp, esm, esl, dll, pex, psc |
| Cyberpunk 2077 | archive (not indexed) | reds, lua, tweak, xl, dll, yaml |
| UE4/UE5 titles | pak, ucas, utoc | pak, ucas, utoc, dll, lua |
Note that for Bethesda games the archive container extensions (bsa, ba2)
classify as cosmetic — the danger is judged by what is inside the archive,
not the wrapper.
Archive-aware conflicts (BSA / BA2 / pak)
modde does not stop at loose files. For each mod it walks loose files and, for
any file whose extension is an archive extension, asks the classifier to index the
archive’s contents. Every entry inside the archive is registered into the conflict
map under its normalised internal path, so a loose textures/sky.dds in one mod
correctly collides with a textures/sky.dds packed inside another mod’s .bsa.
When a conflict spans a loose file and an archived one, the per-file line is tagged so you can see which form won:
[loose > archive]— a loose file beat an archived one[archive > loose]— an archived file beat a loose one
These are also collected separately under Loose vs archive conflicts in the report.
Indexing support is classifier-specific and not uniform:
- Bethesda
.bsa/.ba2are read and their contents indexed. - Cyberpunk
.archiveis listed as an archive extension but its proprietary format is not yet indexed — Cyberpunk conflicts are detected at the loose file level only. - If an archive fails to index, modde warns and treats it as contributing no internal entries (it still counts as a loose-file collision on the archive itself).
Shadowed mods and redundant files
Two pre-deploy optimisation signals fall out of the analysis:
- A shadowed mod is one where every file it provides is overridden by a
higher-priority mod. It contributes nothing to the deployed VFS. The report
lists it as
"<mod>" (N files, all overridden by <mods>); a diagnostic also suggests disabling it to shrink the deployment. - A redundant file is a single path from a mod that always loses. The count
is reported, and
--suggest-hidesturns each into an actionable hide command.
Suggesting hides
modde collisions --profile my-skyrim --suggest-hides
For every redundant file this prints a ready-to-run command, e.g.:
Suggested hide commands:
modde profile hide "SomeTextureMod" "textures/landscape/dirt.dds"
Without --suggest-hides, the report just notes how many redundant files exist
and reminds you of the flag.
Reading the output
A trimmed modde collisions --profile my-skyrim run might look like:
Collision Report for profile "my-skyrim" (skyrim-se)
============================================================
7 file collisions across 3 mod pairs
[DANGEROUS] CombatOverhaulB vs CombatOverhaulA (1 files)
scripts/combat.pex -> winner: CombatOverhaulA
[CONFIG] BaseINI vs TweakedINI (1 files)
SkyrimPrefs.ini -> winner: TweakedINI
[COSMETIC] HiResTextures vs CustomSky (2 files)
textures/sky/clouds.dds -> winner: CustomSky [archive > loose]
textures/sky/stars.dds -> winner: CustomSky (hidden)
Shadowed mods (all files overridden):
- "OldTexturePack" (2 files, all overridden by HiResTextures, CustomSky)
Loose vs archive conflicts: 1 files
Redundant files (never win): 3
Run with --suggest-hides to get hide commands
How to read it:
- The header line reports total file-level collisions and how many mod pairs they span.
[DANGEROUS] CombatOverhaulB vs CombatOverhaulA— the second name is the winner. Here a.pexscript collides, which is high-risk, so verify this is intentional.[archive > loose]onclouds.ddsmeansCustomSkywon via a packed archive entry over a loose file inHiResTextures.(hidden)onstars.ddsmeans the loser was explicitly hidden by you.OldTexturePackis fully shadowed — disabling it changes nothing on disk.
Per-file hiding
Hide a single file from a mod without disabling the whole mod:
modde profile hide "mod_id" "path/to/file.nif"
Hidden files are recorded per profile and excluded during the VFS
build phase: the next-highest provider
of that path wins instead. This is modde’s equivalent of MO2’s .mohidden
system. In the collision report, a hidden loser is tagged (hidden) so you can
confirm the override took effect.
Bethesda plugin load order
For Bethesda games, plugin load order (.esp, .esm, .esl) is managed
separately from mod install priority. These commands read the game’s real
plugins.txt for the active plugin set.
LOOT sorting
modde loot sort --game skyrim-se
modde parses the community LOOT masterlist (a YAML file of per-plugin
after / requires / incompatible rules) and derives load-order rules for the
plugins you actually have active:
afterandrequiresboth become load-after rules (arequireswhose target is not in the active set is logged as a dependency gap).incompatiblebecomes an incompatible rule.- Rules referencing plugins that are not active are dropped.
The masterlist must be cached locally first; if it is missing, sort prints the
exact curl command (and target path) to fetch the right masterlist for your
game. Masterlists are available for skyrim-se/skyrim-ae, fallout4,
fallout76, and starfield. sort prints the generated rules
(X loads after Y, INCOMPATIBLE: A <-> B) so you can review what the
masterlist implies for your setup.
Validating plugins
modde loot validate --game skyrim-se
modde reads only the first ~1 KB of each active plugin’s TES4 header — it never loads multi-GB plugins in full — and reports:
- Form 43 plugins —
[FORM43] <plugin> (vX.XX) — Oldrim format, may cause CTDs in SSE. Form 43 (version0.94) is the old Skyrim LE record format; using it in SSE/AE (which expects Form 44,1.70) can crash the game. The fix is to resave the plugin in the Creation Kit. This check only runs forskyrim-se/skyrim-ae. - Missing masters —
[MISSING] <plugin> requires '<master>' which is not loaded. A plugin depends on a master that is not in the active load order; the game will crash on load. Matching is case-insensitive.
A clean run prints All N plugins are valid.
Plugin order backup and restore
Take a restore point before reordering, and roll back if a sort goes wrong:
# Save the current plugin order
modde backup plugins --profile my-skyrim --game skyrim-se
# Restore the saved order
modde backup restore-plugins --profile my-skyrim --game skyrim-se
Diagnostics
modde diagnostics runs a rule engine over the fully analysed profile — resolved
load order, the archive-aware conflict map, and the collision report together:
modde diagnostics --game skyrim-se --profile my-skyrim
Each finding carries a severity (ERROR, WARN, INFO), a title and detail, the
affected mod (when applicable), and a suggested fix. Findings are sorted with
errors first. Built-in rules include:
- Empty / missing store mod (
WARN) — an enabled mod with no files in the store; suggests re-installing or disabling it. - Completely shadowed mod (
WARN) — every file is overridden; suggests disabling to shrink the deploy. - Dangerous collision (
WARN) — a mod pair whose worst collision is a script/plugin/DLL file; suggests reviewing that the winner is intentional. - Bethesda plugin rules — Form 43 and missing-master findings surfaced as diagnostics for Bethesda games.
Diagnostics are advisory: they never change your load order or files on their own.
See also
- Deployment & VFS — how the winning file is projected on disk
- Profiles — enabling/disabling mods, overrides, hidden files
- Wabbajack — modlists ship a pre-resolved layout
- Troubleshooting — CTDs, missing masters, broken links
- Parity reference — conflict tooling coverage vs MO2
- Supported games — which games ship a classifier
Deployment & VFS
Overview
modde never copies mods into your game directory and never edits the original
files. Instead it builds a symlink farm: a virtual filesystem (VFS) assembled
from symlinks that point back at a content-addressed store. The game — and any
external tool — sees a fully merged, modded Data/ (or ~mods/, or mods/,
depending on the game), while the real install on disk stays pristine.
This is the Linux-native equivalent of Mod Organizer 2’s USVFS, with one decisive
advantage: USVFS hooks file-system calls per-process, so only the hooked process
sees the merged tree. modde’s symlinks live in the real filesystem, so they are
globally visible to every process — the game, xEdit, a packer, a shell —
without any API hooking.
The VFS symlink farm is the deploy path for Nexus and Manual (store-backed) profiles. Wabbajack profiles take a different, hardlink/copy path — see Wabbajack profiles below.
The deployment pipeline
Deployment is a typestate pipeline enforced at compile time:
SymlinkFarm<Built> link map computed in memory (paths only, no I/O)
│ .materialize()
▼
SymlinkFarm<Materialized> symlinks written into the profile staging dir
│ .deploy_to() / deploy_to_install()
▼
(symlinks in the game's mod directory)
Each stage is a distinct Rust type. You cannot call deploy_to() on a Built
farm — the compiler rejects it — so it is impossible to deploy a farm that was
never written to disk. The state marker is a zero-sized PhantomData, so this
safety costs nothing at runtime.
1. Build
SymlinkFarm::build() computes a single HashMap<relative_path, source_path>
where source_path is the absolute path of the winning file inside the
store. No filesystem writes happen yet — this phase is pure path resolution.
The build takes:
| Input | Meaning |
|---|---|
profile_name | Selects the staging directory location |
resolved | The mods in final priority order (first = lowest priority) |
mod_files | Per-mod (relative_path, store_source_path) listings |
overrides | Optional profile-level override files (always win) |
hidden | Optional (mod_id, relative_path) pairs to exclude |
How files map to winners
The build walks resolved.order from lowest to highest priority and inserts
each (rel_path → source) into the map. Because later inserts overwrite earlier
ones for the same key, the highest-priority provider of each path wins.
The effective priority chain, highest first:
- Profile overrides — files in the profile’s
overridesdirectory are layered last and beat every mod. - Later mods in the load order — install priority, last-wins.
- Earlier mods in the load order.
A file listed in hidden as (mod_id, rel_path) is skipped while walking that
mod, so the next-highest provider of that path wins instead. This is how
per-file hiding takes effect — the equivalent
of MO2’s .mohidden. A mod that appears in resolved.order but has no entry in
mod_files (its store directory is missing) is silently skipped during build;
the deploy command logs the missing mods separately.
Install priority is not plugin load order. The symlink farm resolves which file wins on disk. Bethesda
.esp/.esm/.eslordering is a separate concern handled by LOOT andplugins.txt. See Conflicts & Load Order.
2. Materialize
materialize() writes the farm to the per-profile staging directory:
~/.local/share/modde/profiles/<profile>/staging/
It first removes any existing staging directory in full, recreates it, then for
each (rel_path → source) it creates the parent directories and writes a
symlink at staging/<rel_path> pointing at the absolute store source.
Because materialize wipes and rebuilds staging from scratch, the staging tree
always reflects exactly the current Built farm — there are no stale leftovers
from a previous deploy.
The content store these links point into lives at:
~/.local/share/modde/store/<mod_id>/...
3. Deploy
The materialized staging tree is projected into the game’s resolved mod directory. For each relative path the deploy step removes any pre-existing entry at the destination and creates a symlink pointing at the corresponding file inside the staging directory (again an absolute path). The result is a two-hop chain:
<game>/Data/textures/sky.dds ─sym→ staging/textures/sky.dds ─sym→ store/<mod_id>/textures/sky.dds
Final placement is delegated to the game plugin via deploy_to_install(), so a
game can apply its own deploy strategy (for example, UE4 titles route paks into
Content/Paks/~mods/). After deployment modde runs the game’s post_deploy
hook and configures Wine DLL overrides (WINEDLLOVERRIDES) for any proxy DLLs
the mods deploy, such as version.dll for CET or winmm.dll for ASI loaders.
Symlinks: absolute, not relative
Both hops use the symlink target exactly as passed — these are absolute
paths in modde, not relative ones. On Linux the call is a plain
symlink(2); on Windows modde inspects the target to choose symlink_file
vs symlink_dir.
If a store entry is missing, the symlink is still created but dangles. modde
does not error at deploy time for a broken link — the missing-mod case is caught
earlier during build (the mod is skipped and reported), and dangling links are
surfaced by modde verify, which flags any
broken symlink -> <target>.
Deploying
Manual deployment
# Deploy the active profile
modde deploy
# Deploy a specific profile
modde deploy --profile my-skyrim --game skyrim-se
Deploy via play
modde play deploys before launching the game:
modde play --game skyrim-se
Automatic deployment
After rebuilding your NixOS / home-manager configuration, modde deploys profiles
via an activation script when their prerequisites are present. Wabbajack
profiles can wait non-fatally for the game install: set
installMode = "await-game" or leave gameDir unset until Steam or Heroic has
installed the game, then set gameDir and rebuild. For Home Manager Wabbajack
profiles, the install runs before deployment only when the configured gameDir
exists and contains the expected game content directory (for example Data/ for
Skyrim SE); otherwise activation prints an awaiting message and continues.
The store link strategy
The symlink farm only ever creates symlinks. A second, distinct mechanism —
link_or_copy — is used wherever modde must materialise an actual file rather
than a link (notably the Wabbajack deploy path). It tries three strategies in
order and reports which one it used:
| Strategy | When it applies | Cost |
|---|---|---|
| Hardlink | Source and destination on the same filesystem | Free; shares inode/blocks |
| Reflink | Hardlink crossed a filesystem (EXDEV) but the FS supports copy-on-write (btrfs, XFS, ZFS) | Free until written; CoW |
| Copy | Neither of the above worked | Full byte copy |
The destination is removed first if it exists. A cross-device error
(EXDEV on Linux, ERROR_NOT_SAME_DEVICE on Windows) triggers the fall to
reflink; a reflink failure falls to a plain copy. This keeps deployments cheap
on a single CoW filesystem and correct everywhere else.
Rollback
modde rollback swaps the profile’s staging and staging.bak directories:
modde rollback --profile my-skyrim
The swap is performed with directory renames, which are atomic on a single filesystem:
- If a current
stagingexists, it is renamed aside tostaging.old,staging.bakis renamed into place asstaging, andstaging.oldis removed. - If no current
stagingexists,staging.bakis renamed straight into place.
If there is no staging.bak for the profile, rollback fails with
no backup staging found for profile '<name>'. modde does not silently create a
backup on every deploy — materialize() rebuilds staging in place — so
staging.bak is a deliberate restore point you (or higher-level tooling)
preserve before a risky change. After a successful rollback you must re-run
modde deploy to re-project the restored staging tree into the game directory.
Wabbajack profiles
Wabbajack-installed profiles do not use the symlink farm. A Wabbajack modlist
ships a pre-built, already-conflict-resolved file layout under
~/.local/share/modde/staging/<profile>/mods/<mod>/..., so there is nothing for
the VFS to resolve. Deploying one walks that mods/ tree and places each file
into the game directory with link_or_copy (hardlink → reflink → copy):
- MO2 metadata files (
meta.ini,meta.json) are skipped. - A file already hardlinked to its source (same inode and device) is skipped, so re-deploys are near-instant.
- Wine DLL overrides are configured afterwards, exactly as for store-backed profiles.
Hardlinking (rather than symlinking) is used here because the staging tree is the authoritative, fully-merged layout — the game sees real files, which is what Wabbajack lists and their tools expect. See the Wabbajack guide for the full install flow.
Verifying integrity
modde verify --profile my-skyrim
verify walks every enabled mod’s store directory, hashes each file with xxHash,
and checks symlink health. It reports:
- Total files checked
- Missing — enabled mods absent from the store, or files that vanished
- Mismatches/errors — files it could not hash, and any
broken symlink -> <target>where the link target no longer exists
A clean run prints All files OK. This is the fastest way to catch a store
entry that was deleted out from under a deployed profile.
Vanilla (stock) snapshots
A stock snapshot captures the original game installation so you can detect when something — a mod, a tool, a botched deploy — has modified files that should have stayed vanilla:
# Capture a snapshot of the detected install
modde stock snapshot skyrim-se
# Re-verify the install against the snapshot later
modde stock verify skyrim-se
snapshot detects the install directory via the game plugin, records the stock
file set, and prints the snapshot path and file count. verify re-checks the
current install against the recorded snapshot and prints either
Stock snapshot for <game>: OK or
Stock snapshot for <game>: MISMATCH (re-snapshot recommended). Take the
snapshot before you first deploy mods, while the game is genuinely vanilla.
See also
- Conflicts & Load Order — how the winning file is chosen and how Bethesda plugin order differs from install priority
- Wabbajack — the hardlink/copy deploy path for modlists
- Profiles — overrides, hidden files, and per-profile staging
- Playing —
modde playand launch integration - Troubleshooting — broken symlinks and missing-store fixes
- Parity reference — VFS feature coverage vs MO2
Playing a Game
Overview
modde play is the one command you run to actually play a modded game. It orchestrates the full session lifecycle end to end:
- Switch to the requested profile (swapping saves automatically).
- Deploy the profile’s mods (build the symlink farm and wire up tools).
- Launch the game through whichever launcher owns it (Steam or Heroic).
- Capture saves into the vault when the game exits.
Each of those stages can be turned off independently with a flag, so modde play doubles as a precise tool when you only want part of the workflow.
Basic usage
modde play --game skyrim-se
With no profile argument, modde uses the game’s active profile. If there is no active profile, it errors and tells you to name one explicitly. To play a specific profile:
modde play my-skyrim --game skyrim-se
What happens, step by step
1. Profile switch
If the named profile isn’t already active, modde activates it. Activation captures the current profile’s saves, parks them so Steam Cloud stays happy, and restores the target profile’s saves to the live save directory — the same swap described in Save Management. The current mod fingerprint is embedded in the capture commit so future restores can warn about mismatches.
If the profile is already active, the switch is skipped silently.
There is one guardrail here: if modde finds saves in the game directory but no profile is active to own them, it refuses to overwrite and asks you to adopt them first:
Found 3 unadopted save(s) in the game directory.
Run `modde save adopt --game skyrim-se --profile my-skyrim` first.
Run the suggested save adopt and then modde play again. See Adopting existing saves.
2. Mod deployment
modde builds and deploys the profile’s symlink farm and applies tool integration (Wine DLL overrides, the launch wrapper, tool config files, and so on). This is the same work modde deploy does on its own; see Deployment and Tools for the details.
3. Launch
modde detects which launcher owns the game and launches it (see Launcher detection below). It prints the source it’s using, e.g. Launching via Steam (1716740)....
4. Save auto-capture
When the game exits, modde captures the resulting saves into the vault for the active profile, with the mod fingerprint attached. Whether modde can do this synchronously depends on the launcher — see Steam vs Heroic.
Skipping steps
Each flag disables one stage. They compose, so you can combine them.
| Flag | Effect |
|---|---|
--no-switch | Skip the profile switch; deploy and launch the already-active profile. |
--no-deploy | Skip deployment; switch and launch without rebuilding the symlink farm (use when nothing changed since the last deploy). |
--no-capture | Skip save auto-capture after the game exits. |
Some useful combinations:
# I already deployed and the right profile is active — just launch it
modde play --game skyrim-se --no-switch --no-deploy
# Switch + deploy but launch read-only (don't touch my saves on exit)
modde play my-skyrim --game skyrim-se --no-capture
Steam vs Heroic
The crucial difference between launchers is whether modde can wait for the game to exit, which determines how saves get captured.
Heroic (and other waited launches)
Heroic launches (GOG, Epic/Legendary, and sideloaded apps) are run as a child process with --no-gui --launch <app_id>. modde waits for that process to exit, then captures saves directly:
Game exited (status: 0).
Saves auto-captured for profile 'my-skyrim'.
This is the simplest path: capture happens inline, in the same modde play invocation.
Steam (fire-and-forget)
Steam is launched by handing the OS a steam://rungameid/<app_id> URI. Steam itself then starts the game, and the launching call returns immediately — modde never sees the game process and cannot wait for it. So modde prints:
Game launched via Steam (fire-and-forget).
Saves will be captured by the launch wrapper on exit, or run:
modde save auto-capture --game skyrim-se
For Steam, saves are captured one of two ways:
-
The launch wrapper — during deployment modde generates a launch wrapper script and (where it can) registers it so it runs
modde save auto-capture --game <id>after the game process exits. This is the hands-off path. On Steam you may need to add the wrapper to the game’s launch options yourself (modde prints the snippet); see Tools and the wrapper notes in Launcher detection. -
Manually, after you finish playing:
modde save auto-capture --game skyrim-se
For games where save profiles aren’t supported at all, modde simply notes the fire-and-forget launch and skips capture.
Launcher detection
modde auto-detects installed games by scanning launcher libraries — you don’t enter paths by hand. To see everything it found:
modde detect
What modde scans
| Source | How it’s detected |
|---|---|
| Steam | Reads every Steam library folder, parses each appmanifest_<appid>.acf, and matches the app ID against modde’s registry. Falls back to matching known install-dir names under steamapps/common/. |
| Heroic / GOG | Parses gog_store/installed.json and matches GOG app IDs. |
| Heroic / Epic | Parses legendary_store/installed.json and matches Epic/Legendary app IDs. |
| Heroic / Sideload | Parses sideload_apps/installed.json and matches by install-directory name. |
A game can be detected from more than one source. The detected source carries the launcher and app ID, and is what modde play uses to launch (Steam (<app_id>), Heroic/GOG (<app_id>), etc.).
Wine / Proton DLL override registration
On Linux, games run under Wine/Proton, and some mods ship proxy DLLs (for example a version.dll used by script-extender front-ends or winmm.dll used by RED4ext). Wine won’t load a native DLL over its own built-in stub unless WINEDLLOVERRIDES tells it to. modde handles this per launcher during deployment:
-
Heroic — modde writes the override directly into Heroic’s
GamesConfig/<id>.jsonunderenviromentOptions, merging with any existingWINEDLLOVERRIDESso it doesn’t clobber your settings. It also inserts modde’s launch wrapper into Heroic’swrapperOptionschain (after fgmod, if present) so DLLs deleted by fgmod get restored before the game starts. -
Steam — modde can’t edit Steam’s per-game launch options programmatically, so it prints the exact string to paste into the game’s launch options, e.g.:
WINEDLLOVERRIDES="version=n,b" %command% -
Unknown launcher — modde prints the
WINEDLLOVERRIDESvalue to export before launching.
Wine DLL overrides are a Linux/Proton concern only; on native Windows there’s nothing to override.
When detection fails or the game won’t launch
If modde play reports it could not detect launcher, work through these:
- Confirm detection. Run
modde detect. If the game isn’t listed, modde didn’t find an install for it. - Is it actually installed via a scanned launcher? modde scans Steam libraries and Heroic’s GOG/Epic/Sideload stores. A game installed some other way (a raw extracted copy, a different launcher) won’t be detected automatically.
- For Steam: make sure the app appears in a Steam library folder with a valid
appmanifest_*.acfand that the install directory understeamapps/common/actually exists. modde skips manifests whose install path is missing. - For Heroic: make sure the game shows as installed in Heroic — the
installed.jsonfor its store must list it with aninstall_paththat exists on disk. Heroic must be installed as a Flatpak (com.heroicgameslauncher.hgl) or be on yourPATHfor modde to launch it; if neither is found you’ll seeHeroic Games Launcher not found (checked flatpak and PATH). - Override the path. If detection genuinely can’t see your install, you can set an explicit game path in modde’s settings; an override path takes precedence over the launcher scan.
- Launch manually, capture manually. As a fallback you can always start the game from Steam/Heroic yourself after a
modde play ... --no-capture(or justmodde deploy), then capture saves withmodde save auto-capture --game <id>or a runningmodde save watch.
If the game launches but mods don’t appear or crash on load, that’s a deployment/conflict issue rather than a launch issue — see Deployment, Conflicts, and Troubleshooting.
See also
- Save Management — how the profile switch swaps saves and how capture works
- Deployment — what the deploy stage builds
- Tools — the launch wrapper, Wine overlays, and per-game tool config
- Profiles — choosing and managing the profile you play
- Troubleshooting — launch and detection problems
- CLI reference — full command and flag listing
Save Management
Overview
modde manages save files with a git-backed save vault: one git repository per game, one branch per profile. Every capture is an ordinary git commit, so you get the full power of git for free — complete history, branching, snapshots, and the ability to roll a profile’s saves back to any previous point. On top of plain git, modde adds a mod fingerprint so it can warn you before you restore a save that was made under a different set of save-breaking mods.
The whole system is built so you can run multiple profiles of the same game side by side — a vanilla playthrough, a heavily modded one, a testbed — without their saves ever colliding, and without losing the ability to go back.
The vault layout
There is one vault per game, living under modde’s data directory:
<data>/saves/<game_id>/
.git/ # full git history; this is a real git repo
<save files> # the working tree for the currently checked-out profile branch
On Linux <data> is ~/.local/share/modde by default, so a Skyrim SE vault lives at ~/.local/share/modde/saves/skyrim-se/. The path is resolved internally by save_vault_dir(game_id); you never have to construct it by hand.
Key facts about the vault:
- One repository per game. All profiles for a game share the same repo and differ only by branch.
- One branch per profile. The branch name is the profile name with git-unsafe characters (
~ ^ : ? * [ \) replaced by-. So a profile namedMy Skyrimbecomes the branchMy-Skyrim. mainis the root. When a vault is first created, modde makes an emptyinit save vaultcommit onmain. Profile branches are cut from there.- The working tree is whatever profile is checked out. modde checks out the relevant branch (force checkout) before any capture, deploy, or restore, so the files on disk always belong to exactly one profile.
It’s plain git — go ahead and inspect it
The vault is not a custom binary format. It is a normal git repository, and modde uses git2 (libgit2) under the hood. You can point any git tooling at it to audit what modde has stored:
cd ~/.local/share/modde/saves/skyrim-se/
git branch # one branch per profile
git log --oneline # the snapshot history of the checked-out branch
git show <commit> # exactly what changed in a snapshot, including the fingerprint trailers
Treat it as read-only-ish: modde owns the working tree and will force-checkout over local edits. Inspecting, logging, and diffing are safe; hand-committing into it is not recommended.
Save-breaking mods and the fingerprint
A save-breaking mod is a mod whose presence (or absence) can make a save game incompatible — typically anything that changes the game’s serialized data: script extenders and their plugins, mods that add scripts or records baked into saves, framework mods, and so on. Cosmetic mods (textures, reshades, UI tweaks) are not save-breaking: adding or removing them does not invalidate a save.
Each game plugin classifies its own mods. modde does not hard-code a list in the save layer; instead SaveFingerprint::compute takes a classify callback so the save vault stays independent of game-specific logic — the caller resolves the game plugin and asks it whether each mod is save-breaking.
How the fingerprint is computed
The fingerprint is a SHA-256 hash over only the enabled, save-breaking mods of the profile:
- Take every mod in the profile that is enabled and classified save-breaking. Disabled mods and cosmetic mods are ignored entirely.
- Collect their mod IDs, sort them, and de-duplicate.
- Feed each ID (followed by a
\0separator) into SHA-256. - The hex digest is the fingerprint; the first 12 hex characters are used for display.
Because the input is sorted and de-duplicated, two profiles with the same set of enabled save-breaking mods produce the same fingerprint, regardless of install order or how many cosmetic mods differ between them. A profile with no save-breaking mods has the canonical “empty” fingerprint.
Where the fingerprint is stored
When modde captures saves, it embeds the fingerprint as git commit trailers in the snapshot’s commit message:
capture saves for profile 'my-skyrim'
Mod-Fingerprint: a1b2c3d4e5f6
Save-Breaking-Mods: skse, ussep, immersive_armors
Mod-Fingerprint carries the short hash; Save-Breaking-Mods is the human-readable list (omitted when there are no save-breaking mods). On restore, modde reads these trailers back out to compare against your current profile.
Capturing saves
A capture copies the live save files into the vault working tree and commits them on the profile’s branch. modde skips the commit entirely if the tree is byte-identical to the last snapshot, so repeated captures with no new saves don’t clutter history.
Saves are captured automatically in several situations:
- On profile switch —
profile switch,profile try, andprofile rollbackcapture the outgoing profile’s saves before swapping (see Profile-integrated swapping). - On game exit after
modde play— unless you pass--no-capture. See Playing a Game. - Via the launch wrapper — for Steam launches, modde’s generated launch wrapper runs
modde save auto-capturewhen the game process exits. - On demand with
save auto-capture, which detects new saves and commits them. - Continuously with
save watch.
You can also capture manually at any time:
modde save capture --game skyrim-se --profile my-skyrim -m "before dragon fight"
The optional -m/--message text is recorded as the commit message; the fingerprint trailers are appended automatically.
Auto-capture on game exit
modde save auto-capture --game skyrim-se
This is the hook the launch wrapper calls. With no --profile, it captures into the game’s active profile. It also runs the game plugin’s save tracker to produce a descriptive commit subject (for example capture: Lydia — Save 14 [manual]), so history reads like a save list rather than a string of identical messages.
Continuous watching
For games you launch outside of modde play, run a watcher that captures as you save:
modde save watch --game skyrim-se --interval 60
--interval is the poll interval in seconds (default 30). With --profile omitted, the watcher captures into the active profile. It watches the save directory for changes and commits a new snapshot whenever the saves change. Leave it running in a terminal alongside the game.
Browsing history
modde save history --game skyrim-se --profile my-skyrim --limit 10
--limit defaults to 20. Each entry is a snapshot (a git commit) showing the short commit ID, timestamp, a parsed title (character name + save label where the tracker could extract them), the file count, and the mod fingerprint when present. The short ID is what you pass to save restore.
Restoring saves
Restore the saves from a specific snapshot back onto your live save directory:
modde save restore --game skyrim-se --profile my-skyrim <commit>
<commit> is a snapshot ID (or unique prefix) from save history. modde checks out the profile branch, resets it to that commit, clears the active live saves (preserving Steam Cloud and parked metadata — see below), and copies the snapshot’s files back to the game.
Pre-restore compatibility warnings
Before restoring, modde compares the snapshot’s stored fingerprint against your current profile’s fingerprint and reports one of:
| Result | Meaning |
|---|---|
| Compatible | Fingerprints match — the same save-breaking mods are present. Safe to restore. |
| No fingerprint | The snapshot predates fingerprinting (no trailer). Restore at your own risk. |
| Mismatch | The save-breaking mods differ. modde lists which mods were added to your current profile and which were removed since the snapshot. |
A mismatch is a warning, not a hard block: you may legitimately want to restore an older save and then re-add the mods. But restoring a save under a different set of save-breaking mods is the classic recipe for corrupted or broken saves, so read the diff before you proceed. modde also nudges you toward modde save history to find a snapshot whose fingerprint matches your current setup.
Adopting existing saves
If you already have save files from before you set up modde, adopt them into a profile so they become the first snapshot on that profile’s branch:
modde save adopt --game skyrim-se --profile my-skyrim
This ensures the profile’s branch exists and captures every existing save in the game’s save directory as the initial snapshot. If no profile is active for that game yet, the adopted profile is also made active, so that the next profile switch can safely park those saves rather than treating them as orphaned.
This adoption flow is also what protects you during a normal profile switch. If modde finds saves in the game directory but no profile is active, it refuses to overwrite them and tells you to adopt first — see the AdoptionRequired path in Playing a Game.
Steam Cloud handling
modde does not treat the live save directory as disposable. Many games sync their save folder through Steam Cloud, which drops a steam_autocloud.vdf marker file there. Blowing that directory away on every profile switch would fight Steam Cloud and risk re-downloading stale saves.
Instead, during a profile switch modde:
- Captures the outgoing profile’s saves into its vault branch (the git history is the authoritative record).
- Parks the outgoing root saves under a modde-owned directory,
.modde/profiles/<profile>/, inside the live save folder. Because the files are moved rather than deleted, Steam Cloud sees them as relocated, not lost. - Leaves the
steam_autocloud.vdfmarker (and the.modde/directory) untouched. - Restores the incoming profile’s saves at the root.
So the game only ever sees the active profile’s saves at the top of its save directory, while Steam’s cloud marker stays put and the git vault remains the source of truth. The steam_autocloud.vdf marker and the .modde/ live-state directory are treated as live metadata: modde never copies them into the vault and never deletes them when clearing active saves.
If Steam later downloads stale root saves behind modde’s back, just switch profiles again or run modde save restore for the snapshot you actually want.
Profile-integrated save swapping
Save management is wired into the profile system so switching profiles swaps saves automatically. The full activate flow (activate_with_fingerprint) does the following when you move from a current profile to a new one:
- Capture the current profile’s live saves into its vault branch, embedding the current fingerprint in the commit.
- Park the current profile’s root saves under
.modde/profiles/<current>/, preserving Steam Cloud metadata. - Ensure the new profile’s branch exists (creating it if needed).
- Deploy the new profile’s saves from its vault branch to the live save directory root.
This is what makes modde play <profile> and modde profile switch feel seamless: your saves follow your profile.
Forking a profile branches its saves
When you fork a profile, modde also forks its save vault branch:
modde profile fork <source> <new-name> --game skyrim-se
Under the hood, fork_saves looks up the source profile’s branch tip and creates a new branch pointing at that same commit. The fork therefore starts with a complete copy of the source profile’s save history and then diverges — new captures on the fork land on the fork’s branch, new captures on the source land on the source’s branch, and neither disturbs the other. This is the git-native answer to “I want to try a risky change but keep my main playthrough safe.”
Cross-profile save loading — be careful
The whole point of per-profile branches is that a save made under one profile belongs to that profile’s mod set. modde does not stop you from manually pointing one profile at another profile’s save (for instance, by restoring a commit from a different branch or hand-copying files), but doing so is exactly the situation the fingerprint exists to warn about.
Loading a save into a profile whose enabled save-breaking mods differ from the ones the save was made with can corrupt the save or crash the game — missing scripts, dangling records, version mismatches. If you must move a save across profiles:
- Prefer
modde profile fork, which carries the history across cleanly. - If you restore across branches, read the fingerprint mismatch report and reconcile the save-breaking mods first.
- When in doubt, match the mod set to the save rather than the save to the mod set.
Managing individual save assignments
Separate from the vault, modde keeps a lightweight database table that tracks individual save files by path and links them to a profile with an optional label. This is handy for annotating specific saves without committing a full snapshot.
# Assign a save file (or directory) to a profile, with an optional label
modde save assign ~/saves/quicksave.ess --profile my-skyrim --label "quick save"
# List the saves assigned to a profile
modde save list --profile my-skyrim
# Find saves in the game directory not yet assigned to any profile
modde save scan --game skyrim-se
# Remove an assignment (the file itself is untouched)
modde save unassign ~/saves/quicksave.ess
save assign and save unassign take the save path as a positional argument. save scan reports files in the game’s save directory that aren’t yet tracked. These assignments are purely metadata — they record that a save belongs to a profile; the vault is still what captures and restores the actual bytes.
Command reference
| Command | Purpose |
|---|---|
save adopt --game G --profile P | Import existing game-directory saves as the first snapshot |
save capture --game G --profile P [-m MSG] | Create a snapshot on the profile branch |
save auto-capture --game G [--profile P] | Detect and commit new saves (the game-exit / wrapper hook) |
save watch --game G [--profile P] [--interval N] | Poll every N seconds (default 30) and capture changes |
save history --game G --profile P [--limit N] | List snapshots (default limit 20) |
save restore --game G --profile P <commit> | Restore a snapshot, with a pre-restore fingerprint check |
save assign <path> --profile P [--label L] | Link a save file/dir to a profile |
save unassign <path> | Remove a save assignment |
save list --profile P | List assigned saves |
save scan --game G | Find unassigned saves in the game directory |
See also
- Playing a Game — how
modde playcaptures saves on exit - Profiles — switching, trying, rolling back, and forking profiles
- Supported Games — which games ship with save trackers
- CLI reference — full command and flag listing
- Troubleshooting — recovering from a bad restore or a Steam Cloud fight
Tools & Overlays
Overview
modde can manage gaming tools and overlays that enhance or modify how games run: performance overlays, graphics post-processing layers, upscaling and frame-generation frameworks, performance boosters, and per-game Proton launch settings. Each tool is a per-game integration with its own enabled flag and JSON settings blob persisted in modde’s database.
The current truthful scope is narrower than Mod Organizer 2’s tool management (which is why tool management is marked Partial in the capability matrix): modde manages a fixed set of six gaming tools through a uniform enable/configure/apply pipeline. It does not provide arbitrary executable-tool registration inside the tools panel — that lives in the separate, fully shipped Executables & external tools feature.
Each tool contributes one or more of the following at launch or deploy time:
| Mechanism | What it does | Tools that use it |
|---|---|---|
| Environment variable | Exported into the game’s launch environment | MangoHud, vkBasalt, OptiScaler, Proton |
| Wrapper command | Chained before the game executable | GameMode |
| Generated config file | Written under ~/.local/share/modde/tools/<game_id>/ | MangoHud, vkBasalt |
| File patch (apply/revert) | DLLs / shaders copied into the executable directory | ReShade, OptiScaler |
| Wine DLL override | Forces Wine to load the native proxy DLL | ReShade, OptiScaler, Proton |
Supported tools
| Tool ID | Category | Description | Linux only |
|---|---|---|---|
mangohud | Overlay | Performance HUD (FPS, frame timing, CPU/GPU telemetry) | Yes |
vkbasalt | Post-Processing | Vulkan post-processing layer (CAS sharpening, FXAA, shaders) | Yes |
gamemode | Performance | Feral GameMode wrapper for system performance tuning | Yes |
reshade | Post-Processing | ReShade shader injection via proxy DLL | No (Wine/Proton) |
optiscaler | Upscaler | DLSS/FSR/XeSS upscaling and frame-generation replacement | No (Wine/Proton) |
proton | Performance | Per-game Proton, prefix, environment, and DLL-override settings | No |
Command summary
All tool commands take --game <game_id>. The verbs are the same across every tool, but each verb only does something for tools that implement it.
# Discovery
modde tool list --game skyrim-se # auto-detect known external tool executables
modde tool status --game skyrim-se # enabled/disabled + availability for all six tools
# Lifecycle
modde tool enable <tool_id> --game skyrim-se
modde tool disable <tool_id> --game skyrim-se
modde tool configure <tool_id> --game skyrim-se -- key=value key=value ...
# File patches (ReShade, OptiScaler)
modde tool apply <tool_id> --game skyrim-se
modde tool revert <tool_id> --game skyrim-se
# Release-backed tools (OptiScaler)
modde tool releases <tool_id> --game skyrim-se
modde tool install-release <tool_id> --game skyrim-se --tag <tag> --asset <asset>
configure parses each key=value pair: true/false become booleans, anything that parses as a number becomes a number, and everything else is stored as a string. enable and configure regenerate the tool’s config file when it has one; disable preserves stored settings so re-enabling restores them.
Checking status
modde tool status --game <id> prints every tool with its category, enabled state, the number of files it has applied, and whether the underlying program is available on the system:
modde tool status --game stellar-blade
Availability is detected per tool: MangoHud, vkBasalt, and GameMode look for their binary or Vulkan layer on Linux; Proton reports whether protonup-rs is present and how many GE-Proton installs exist; ReShade and OptiScaler always report “available” because their payloads are user-provided or downloaded on demand. For OptiScaler, status additionally prints a live install scan of the executable directory (see OptiScaler below).
MangoHud
Category: Overlay · Linux only · enabled via environment variable + generated config.
MangoHud is a performance HUD that draws FPS, frame timing, and CPU/GPU telemetry over the running game.
How it is enabled
When enabled, MangoHud contributes the environment variable MANGOHUD=1. If the per-game config is non-empty, modde also exports MANGOHUD_CONFIG pointing at the generated file:
~/.local/share/modde/tools/<game_id>/MangoHud.conf
Settings
MangoHud exposes 100+ settings organised into UI sections: Layout, FPS and Frame Timing, CPU, GPU, Memory and IO, Compatibility, Logging, and Filtering. A few representative keys:
| Key | Section | Type | Notes |
|---|---|---|---|
position | Layout | select | top-left … bottom-right |
fps_limit / fps_limit_method | FPS | text / late|early | frame-rate cap and limiter mode |
background_alpha | Layout | number 0–1 | HUD background opacity |
cpu_temp, gpu_temp, gpu_junction_temp | CPU / GPU | bool | per-sensor toggles |
custom_text_center | Layout | text | free-form HUD text |
gamemode, vkbasalt | Compatibility | bool | show whether those layers are active |
output_folder, log_duration, upload_logs | Logging | path / number / bool | benchmark logging |
The fresh-enable defaults turn on position=top-left, fps, frametime, cpu_stats, and gpu_stats.
Config generation
The generated MangoHud.conf is a flat key/value file. Booleans render as a bare key when true (e.g. fps) and as key=0 when false; numbers and strings render as key=value. Keys beginning with _ (modde’s internal markers such as _game_id) are skipped. The file always starts with # Generated by modde — do not edit manually.
modde tool enable mangohud --game skyrim-se
modde tool configure mangohud --game skyrim-se -- \
position=top-right fps_limit=60 fps_limit_method=late gpu_temp=true custom_text_center=skyrim
Apply / revert
MangoHud writes no files into the game directory, so apply/revert are no-ops. Disabling the tool stops exporting the env vars; the config file is left on disk and reused on re-enable.
vkBasalt
Category: Post-Processing · Linux only · enabled via environment variable + generated config.
vkBasalt is a Vulkan post-processing layer that applies Contrast Adaptive Sharpening (CAS), FXAA, and ReShade-compatible shader effects to any Vulkan game (including DXVK/VKD3D titles under Proton).
How it is enabled
When enabled, vkBasalt contributes ENABLE_VKBASALT=1 plus VKBASALT_CONFIG_FILE pointing at:
~/.local/share/modde/tools/<game_id>/vkBasalt.conf
Availability detection looks for the implicit Vulkan layer JSON (/usr/share/vulkan/implicit_layer.d/vkBasalt.json, /etc/vulkan/...), a vkbasalt binary on PATH, or vkBasalt.json under any directory in VK_LAYER_PATH (covers Nix profile installs).
Settings
| Key | Section | Type | Default | Notes |
|---|---|---|---|---|
toggleKey | Activation | text | Home | key that toggles vkBasalt at runtime |
enableOnLaunch | Activation | bool | true | start with effects already active |
effects | Effects | list | ["cas"] | colon/comma-joined list, e.g. cas:fxaa |
casSharpness | Effects | number 0–1 | 0.4 | CAS strength |
reshadeTexturePath | ReShade Paths | path | — | optional ReShade texture dir |
reshadeIncludePath | ReShade Paths | path | — | optional ReShade shader include dir |
Config generation
The generated vkBasalt.conf writes toggleKey = <key>, enableOnLaunch = True/False, a colon-joined effects = ... line, casSharpness = ..., and any ReShade paths. It also begins with the # Generated by modde banner.
modde tool enable vkbasalt --game cyberpunk2077
modde tool configure vkbasalt --game cyberpunk2077 -- casSharpness=0.5 toggleKey=F10
Apply / revert
No files are written into the game directory; apply/revert are no-ops.
GameMode
Category: Performance · Linux only · enabled via a wrapper command.
GameMode is Feral Interactive’s daemon that applies CPU governor, scheduling, and GPU performance tuning for the lifetime of the game process.
How it is enabled
GameMode has no env vars and no config files. When enabled it contributes a single wrapper command, gamemoderun, which modde chains in front of the game executable in the launch pipeline. Availability detection looks for gamemoderun on PATH.
The only “setting” is a read-only note explaining that the wrapper is applied when enabled.
modde tool enable gamemode --game skyrim-se
modde tool status --game skyrim-se # confirm "yes" availability and "enabled"
Apply / revert
No files; apply/revert are no-ops. Disabling removes gamemoderun from the launch chain.
ReShade
Category: Post-Processing · Wine/Proton · enabled via proxy-DLL copy + Wine DLL override.
ReShade injects post-processing shaders into DirectX games running under Wine/Proton by placing a proxy DLL (typically dxgi.dll) next to the game executable. Wine must be told to load the native proxy instead of its built-in stub, so ReShade contributes a WINEDLLOVERRIDES entry for the chosen DLL base name.
Settings
| Key | Section | Type | Notes |
|---|---|---|---|
source_dir | Source | path | Directory holding ReShade DLLs, ReShade.ini, and shader folders. Required for apply. |
dll_name | Deployment | select | dxgi.dll (default), d3d11.dll, or dinput8.dll |
derived_executable_dir | Detected Game | read-only | Where files land, derived from the game’s metadata |
Because ReShade binaries are Windows files the user supplies, ReShade always reports as “available (user-provided)”; you must point source_dir at a folder you have populated.
Apply / revert
modde tool apply reshade copies, into the game’s executable directory:
- the selected proxy DLL (
dll_name) fromsource_dir, ReShade.iniif present, and- the
reshade-shaders/andreshade-presets/directories if present (copied recursively).
modde records every file it wrote so modde tool revert reshade removes exactly those files. The Wine DLL override (e.g. dxgi) is contributed automatically while the tool is enabled.
modde tool enable reshade --game cyberpunk2077
modde tool configure reshade --game cyberpunk2077 -- \
source_dir=/home/me/reshade dll_name=dxgi.dll
modde tool apply reshade --game cyberpunk2077
# later …
modde tool revert reshade --game cyberpunk2077
The apply has a non-mutating preview internally (it reports which planned files are missing, changed, or already match), so re-applying after editing a preset only rewrites what changed.
OptiScaler
Category: Upscaler · Wine/Proton · enabled via proxy-DLL copy + Wine DLL override (and an optional env var). Release-backed.
OptiScaler replaces a game’s upscaler/frame-generation inputs (DLSS/FSR/XeSS) by hooking through a proxy DLL. It also subsumes modde’s old fgmod DLL-restore logic. This is the most involved tool; it supports release downloading, community profiles, FSR4 payload variants, OptiPatcher, and companion DLLs.
Sources
Where OptiScaler files come from is chosen with source_mode:
source_mode | UI label | Meaning |
|---|---|---|
github_release | Official GitHub releases | Releases from optiscaler/OptiScaler (encoded as official:<tag>) |
goverlay_builds | GOverlay builds | The external benjamimgois/OptiScaler-builds feed, with a goverlay_channel selector |
goverlay_fgmod | GOverlay fgmod directory | An existing local ~/.local/share/goverlay/fgmod install (the default) |
local_dir | Local OptiScaler directory | A folder you point at with local_source_dir, containing OptiScaler.dll |
When source_mode = goverlay_builds, a goverlay_channel setting selects edge (bleeding-edge), stable, master, or any. Bleeding-edge FSR 4.0.2-capable builds are typically distributed through the GOverlay builds feed rather than official OptiScaler releases.
Releases: list and install
OptiScaler is the only release-backed tool (supports_releases() == true). modde tool releases optiscaler merges official and GOverlay releases and lists installable .zip/.7z assets. install-release downloads and extracts the chosen asset into modde’s per-tag cache, then records the selection back into the tool config:
modde tool releases optiscaler --game stellar-blade
modde tool install-release optiscaler --game stellar-blade \
--tag official:v0.9.1 --asset OptiScaler.7z
Archives are flattened so that OptiScaler.dll, OptiScaler.ini, companion DLLs, OptiPatcher.asi, and the FSR4 payload directories land predictably in the cache. (.7z extraction shells out to 7zz/7z.) Installing from a local archive path is also supported internally for offline pinning.
Key settings
| Key | Type | Notes |
|---|---|---|
proxy_dll | select | DLL OptiScaler is loaded as: dxgi.dll (default), version.dll, dbghelp.dll, d3d12.dll, wininet.dll, winhttp.dll, winmm.dll, nvngx.dll, or OptiScaler.asi |
dll_overrides | text | Extra Wine DLL override base names (comma/space separated) |
copy_companion_files | bool (default on) | Copy fakenvapi.dll, nvngx-wrapper.dll, and other DLLs found beside OptiScaler |
fsr4_variant | select | latest_fp8 (“Latest (FP8)”) or int8_402 (“4.0.2c (INT8)”) — copied as amd_fidelityfx_upscaler_dx12.dll |
emulate_fp8 | bool | Only meaningful for the FP8 variant; exports DXIL_SPIRV_CONFIG=wmma_rdna3_workaround |
enable_optipatcher | bool | Deploy plugins/OptiPatcher.asi to unlock DLSS/DLSS-FG inputs without whole-game spoofing |
spoof_dlss | bool | Fallback DXGI spoofing path for games that still need it |
There are also exposed OptiScaler .ini overrides (ini_overrides.*), e.g. the in-game menu shortcut key, menu scale, the FSR upscaler/frame-generation backend indices, and fakenvapi/LatencyFlex toggles. Additional .ini keys discovered in the selected release’s OptiScaler.ini are surfaced as advanced overrides.
FSR4 variants and FP8 emulation
fsr4_variant selects which FSR4 payload is copied as amd_fidelityfx_upscaler_dx12.dll: latest_fp8 (FP8) or int8_402 (INT8, 4.0.2c). The two payloads ship in FSR4_LATEST/ and FSR4_INT8/ directories inside the source. emulate_fp8 only applies to the FP8 variant and, when set, exports DXIL_SPIRV_CONFIG=wmma_rdna3_workaround so FP8 paths run on RDNA3 via the WMMA workaround.
OptiPatcher and companion DLLs
When enable_optipatcher is on, apply deploys plugins/OptiPatcher.asi and sets Plugins.LoadAsiPlugins=true in the merged .ini. If the patcher is not cached, install-release fetches the latest OptiPatcher.asi from optiscaler/OptiPatcher. With copy_companion_files on, any extra .dll next to OptiScaler (fakenvapi, nvngx wrapper, etc.) is copied alongside the proxy.
Install scanning and backups
Before apply, OptiScaler scans the executable directory and classifies the install as absent, managed, unmanaged, partially managed, or conflicted (more than one recognised proxy DLL). modde tool status prints this as a one-line summary, e.g.:
OptiScaler install: unmanaged; version v0.9.1; proxy dxgi.dll
If an existing install is unmanaged, partially managed, or conflicted, apply first backs up the recognised files (with a JSON manifest) so the previous state can be recovered. When you adopt an existing working install rather than reinstalling, modde records the files as managed.
fgmod restore
OptiScaler carries the fgmod DLL-restore behaviour: fgmod deletes certain DLLs at launch (dxgi.dll, winmm.dll, nvngx.dll, _nvngx.dll, nvngx-wrapper.dll, dlss-enabler.dll, OptiScaler.dll). modde’s launch wrapper scans the staging mods/<mod>/bin/x64 directories and restores any of those DLLs into the executable directory so a re-deploy does not leave the game broken.
Community profiles
For some games modde ships community-tested OptiScaler profiles (game-owned metadata). Selecting a profile via optiscaler_profile=<id> applies its proxy_dll, source, FSR4 variant, OptiPatcher flag, .ini overrides, and records its tested version, source URL, and notes. Profiles never imply OptiScaler is enabled by default.
For Stellar Blade the default profile is community-dxgi, which uses dxgi.dll as the proxy and enables OptiPatcher so DLSS/DLSS-FG inputs are unlocked without spoofing. You can also define your own profiles in ~/.local/share/modde/games/<game_id>.optiscaler.toml (an [[optiscaler.profile]] array); user profiles merge over built-ins of the same id.
modde tool enable optiscaler --game stellar-blade
modde tool configure optiscaler --game stellar-blade -- optiscaler_profile=community-dxgi
modde tool apply optiscaler --game stellar-blade
See the deeper deployment notes in the supported-game page for Stellar Blade and Cyberpunk 2077.
Proton
Category: Performance · enabled via launch environment + DLL overrides. Does not install Proton or the game.
The proton tool stores per-game launch-compatibility settings that feed modde’s existing env-var, DLL-override, and wrapper collection. It does not install Steam, the game, or a Proton runner.
Runner selection
| Key | Type | Notes |
|---|---|---|
version_mode | select | launcher_default (use the launcher’s runner), installed_version (use a specific installed GE-Proton), or install_with_protonup_rs (request an install via protonup-rs) |
selected_version | select | latest or a specific GE-Proton tag; the option list merges the GE-Proton catalog with locally installed compatibility tools |
install_target | select | Target passed to protonup-rs (steam) |
version_mode = launcher_default is the default and needs nothing else. The UI can load the upstream GE-Proton catalog from GloriousEggroll/proton-ge-custom and merge it with installed runners found under Steam’s compatibilitytools.d directories. Installing a selected version still requires protonup-rs on PATH; modde invokes it non-interactively (--tool GEProton --version <tag> --for steam).
Environment, prefix, and overrides
| Key | Type | Notes |
|---|---|---|
prefix_path_override | path | Exports WINEPREFIX; blank uses launcher detection |
extra_env | text | Extra KEY=VALUE lines (one per line; # comments ignored) exported at launch |
dll_override_mode | select | auto, forced, or off — how Proton contributes forced DLL overrides |
forced_dll_overrides | text | Comma/space-separated DLL base names, e.g. dxgi, winmm |
wrapper_order | select | after-modde or before-tools — where Proton integration sits in the launch chain |
When dll_override_mode is not off, the names in forced_dll_overrides (with any .dll suffix stripped) are contributed as Wine DLL overrides.
Compatibility toggles (40+ env flags)
Proton exposes 40+ boolean toggles, each of which exports one environment variable when set. A non-exhaustive sample:
| Setting | Exports |
|---|---|
steamdeck | SteamDeck=1 |
proton_enable_hdr | PROTON_ENABLE_HDR=1 |
enable_hdr_wsi | ENABLE_HDR_WSI=1 |
proton_enable_wayland | PROTON_ENABLE_WAYLAND=1 |
proton_log | PROTON_LOG=1 |
radv_perftest_rt | RADV_PERFTEST=rt,emulate_rt |
proton_enable_nvapi | PROTON_ENABLE_NVAPI=1 |
mesa_loader_zink | MESA_LOADER_DRIVER_OVERRIDE=zink |
proton_fsr4_upgrade | PROTON_FSR4_UPGRADE=1 |
proton_dlss_upgrade | PROTON_DLSS_UPGRADE=1 |
proton_xess_upgrade | PROTON_XESS_UPGRADE=1 |
proton_use_wow64 | PROTON_USE_WOW64=1 |
proton_no_ntsync | PROTON_NO_NTSYNC=1 |
enable_mesa_antilag | ENABLE_LAYER_MESA_ANTI_LAG=1 |
# Keep the launcher's default runner
modde tool configure proton --game cyberpunk2077 -- version_mode=launcher_default
# Export extra variables and enable HDR
modde tool configure proton --game cyberpunk2077 -- \
proton_enable_hdr=true extra_env='PROTON_LOG=1'
# Force DLL overrides a mod needs
modde tool configure proton --game cyberpunk2077 -- \
dll_override_mode=forced forced_dll_overrides=dxgi,winmm
Apply / revert
Proton writes no files into the game directory; it only contributes environment variables and DLL overrides at launch. There is nothing to apply/revert.
See also
- Executables & external tools — the shipped (Done) named-executable manager for xEdit, BodySlide, Nemesis, etc.
- Playing & launching — how launch env vars and wrappers reach the game
- Deployment & VFS — how applied files and staging interact
- Supported games — per-game OptiScaler/Proton notes
- Feature parity — why tool management is Partial
Executables & external tools
Overview
modde manages named executable launch targets — the modding tools you run against a game install, such as xEdit, BodySlide, Nemesis, FNIS, the Creation Kit, or LOOT. Each target stores a name, an executable path, default arguments, a working directory, environment variables, optional Wine DLL overrides, and an output mod. When you run a target, modde snapshots the mod directory, runs the tool, and captures any new files the tool wrote into an output mod so they survive future deploys.
This feature is shipped end to end and user-reachable from both the CLI and the GUI, so it is marked Done in the capability matrix. It is distinct from Tools & Overlays, which manages the fixed set of six gaming overlays (MangoHud, OptiScaler, Proton, …).
Configurations are stored in modde’s database (the executable_configs table), keyed uniquely by (game_id, name). Saving a target with an existing name overwrites it, so add doubles as edit.
Two equivalent command surfaces
The same storage is exposed through two CLI namespaces. modde exec ... is a thin alias for the executable subset of modde tool ...; modde exec list and modde tool list-executables print the same rows.
Short form (modde exec) | Long form (modde tool) | Action |
|---|---|---|
modde exec add | modde tool add-executable | Save or update a named target |
modde exec list | modde tool list-executables | List configured targets for a game |
modde exec remove | modde tool remove-executable | Delete a target |
modde exec run | modde tool run-executable | Run a saved target with overwrite capture |
There is also a one-shot form, modde tool run <executable> [-- args], which runs an arbitrary executable with overwrite capture without saving a configuration.
Fields
Every saved target has the following fields. On the CLI they map to positional arguments and flags on modde exec add / modde tool add-executable:
| Field | CLI | Required | Default | Notes |
|---|---|---|---|---|
| Name | positional <name> | yes | — | Display name, e.g. xEdit. Unique per game. Cannot be blank. |
| Executable | positional <executable> | yes | — | Path to the program to run |
| Game | --game <id> | yes | — | Must be a supported game id |
| Working directory | --working-dir <dir> | no | detected game install dir | The cwd the tool runs in |
| Output mod | --output-mod <name> | no | __overwrite__ | Mod that captures newly written files. Cannot be blank. |
| Wine DLL overrides | --wine-dll-overrides <str> | no | none | Exported as WINEDLLOVERRIDES, e.g. dinput8=n,b;winmm=n,b |
| Environment | --env KEY=VALUE (repeatable) | no | none | Each must be KEY=VALUE with a non-empty key |
| Default args | -- ARGS... | no | none | Everything after -- is the tool’s default argument list |
add-executable validates eagerly: an unsupported game id, a blank name, a blank output mod, or a malformed --env entry (missing = or empty key) is rejected before anything is written.
Adding a target
modde tool add-executable xEdit /games/skyrim/SSEEdit.exe \
--game skyrim-se \
-- -quickautoclean
The equivalent short form:
modde exec add xEdit /games/skyrim/SSEEdit.exe --game skyrim-se -- -quickautoclean
Both store the same row. The arguments after -- are saved as the tool’s defaults; when you later run the target you can append more arguments after another -- and they are concatenated onto the saved defaults.
Listing and removing
modde exec list --game skyrim-se
# Executables for skyrim-se:
# xEdit
# path: /games/skyrim/SSEEdit.exe
# args: -quickautoclean
# output mod: __overwrite__
modde exec remove xEdit --game skyrim-se
remove reports an error if no target with that name exists for the game.
Running with overwrite capture
When you run a saved target (or modde tool run <exe> directly), modde performs the same four-step capture used for any external tool:
- Snapshot the game’s mod directory (the deployed VFS tree) before the tool runs.
- Run the tool with the configured working directory (defaulting to the detected install dir), saved
--envvariables,WINEDLLOVERRIDES(if set), and the saved arguments plus any you appended. - Snapshot again after the tool exits and diff against the before-snapshot.
- Move every newly written file into the output mod under modde’s store directory (
~/.local/share/modde/store/<output_mod>/).
# Run the saved xEdit target, appending an extra flag this time
modde exec run xEdit --game skyrim-se -- -autoload
# Run against a specific profile's deployed tree
modde exec run xEdit --game skyrim-se --profile my-skyrim
If the tool exits non-zero, modde warns but still captures whatever was written. If nothing new appeared, modde prints No new files written to mod directory. and stops. Otherwise it lists each captured file and reminds you to add the output mod to your profile:
Tool wrote 3 new file(s). Moving to output mod '__overwrite__':
meshes/actors/character/...
...
Output mod: ~/.local/share/modde/store/__overwrite__
Add '__overwrite__' to your profile mod list to include these files in future deploys.
What lands in the output mod
Only files that are new relative to the pre-run snapshot of the mod directory are captured. Files the tool modified in place that already existed in the deployed tree are not moved (they belong to whichever mod owns them). The captured files are moved (renamed, or copied-then-removed across filesystems) into the output mod, so they leave the deployed tree and become a first-class mod you can enable, disable, and order like any other. The default __overwrite__ mod mirrors Mod Organizer 2’s “Overwrite” pseudo-mod; you can target a different mod with --output-mod to keep, say, BodySlide output separate from xEdit output.
The GUI Executables view
The graphical UI ships an Executables view that manages the same database rows. It lists each configured target with its name, executable path, and a metadata line summarising args, working directory, Wine DLL overrides, and output mod. Per row you get Run, Edit, and Remove buttons (Run and Remove are disabled while that target is already running). A Refresh button reloads the list, and an Add executable button opens an editor form with fields for name, executable path (with a Browse picker), arguments, output mod, working directory (with a Browse picker), WINEDLLOVERRIDES, and KEY=VALUE env lines. Saving an existing name updates it in place (the button reads “Save / update”).
Worked examples (Proton)
Windows modding tools run under Proton/Wine on Linux. The patterns below assume the tool’s .exe lives in or near the game install and that you have a usable Wine/Proton runner. Use --wine-dll-overrides when a tool needs native DLLs, --working-dir when it must run from a specific folder, and --output-mod to keep generated assets out of the catch-all overwrite.
xEdit (SSEEdit) — quick auto clean
modde tool add-executable xEdit /games/skyrim/SSEEdit.exe \
--game skyrim-se \
--output-mod xedit-output \
-- -quickautoclean
modde exec run xEdit --game skyrim-se
xEdit writes cleaned plugins and backups; capturing them into xedit-output lets you review and order them as a dedicated mod.
BodySlide — built meshes to a dedicated mod
modde tool add-executable BodySlide "/games/skyrim/Data/CalienteTools/BodySlide/BodySlide x64.exe" \
--game skyrim-se \
--working-dir "/games/skyrim/Data/CalienteTools/BodySlide" \
--output-mod bodyslide-output \
--wine-dll-overrides "dinput8=n,b"
modde exec run BodySlide --game skyrim-se
BodySlide builds meshes into the game’s Data tree; the new .nif/.tri files are captured into bodyslide-output, which you then place after the body/armor mods in your load order.
Nemesis — generated behavior files
modde tool add-executable Nemesis \
"/games/skyrim/Data/Nemesis_Engine/Nemesis Unlimited Behavior Engine.exe" \
--game skyrim-se \
--working-dir "/games/skyrim/Data/Nemesis_Engine" \
--output-mod nemesis-output \
--wine-dll-overrides "vcruntime140=n,b"
modde exec run Nemesis --game skyrim-se
Nemesis regenerates behavior files under meshes/; capturing them into nemesis-output keeps the generated animation data as a single, re-buildable mod that wins over the source animation mods.
See also
- Tools & Overlays — the separate gaming-overlay manager (MangoHud, OptiScaler, Proton)
- Deployment & VFS — how the mod directory you capture from is built
- Profiles — enabling and ordering the captured output mod
- Conflict resolution — where the output mod sits in priority
- CLI reference — full command and flag listing
Data, instances & backups
Overview
modde keeps all of its state in a single data directory plus a small config file. This guide documents the on-disk layout, how to point modde at a different location, how to run several isolated instances, the built-in backup and stock snapshot facilities, and how to reproduce your setup across machines.
Everything here is filesystem and metadata; the conceptual model of a profile itself lives in Profile management, and the save vaults referenced below are covered in Save management.
Where modde stores data
modde follows the platform’s standard base directories. On Linux that means the XDG specification, with the usual macOS and Windows fallbacks:
| Purpose | Linux | macOS | Windows |
|---|---|---|---|
Data (modde_data) | $XDG_DATA_HOME or ~/.local/share | ~/Library/Application Support | %APPDATA% |
| Config | $XDG_CONFIG_HOME or ~/.config | ~/Library/Application Support | %APPDATA% |
| Cache | $XDG_CACHE_HOME or ~/.cache | ~/Library/Caches | %LOCALAPPDATA% |
On Linux, an explicitly-set XDG_DATA_HOME / XDG_CONFIG_HOME / XDG_CACHE_HOME
is honored before the ~/.local/share-style fallback.
The data directory layout
All persistent state lives under <data_dir>/modde/ — for a default Linux setup,
~/.local/share/modde/:
~/.local/share/modde/
├── modde.db # SQLite: profiles, mods, load order, locks,
│ # saves index, categories, tools, executables,
│ # stock-snapshot metadata, experiment stack
├── store/ # content-addressed mod file store (the staged bytes)
├── staging/ # scratch space used while installing/extracting
├── profiles/ # per-profile on-disk dirs: <name>/overrides, <name>/staging,
│ # <name>/ini (per-profile game INIs), etc.
├── downloads/ # downloaded mod archives
├── stock/ # vanilla game snapshots (one subdir per game_id)
├── saves/ # git-backed save vaults (one repo per game_id)
├── wabbajack_cache/ # content-addressed cache of .wabbajack manifest files
└── backups/ # zip mod backups + JSON plugin-order backups
A few of these are worth calling out:
modde.dbis the source of truth for everything except file bytes and git history. The profile, mod list, load order, locks, categories, tags, per-game tool config, named executables, and the experiment stack all live here. Its schema is migrated forward automatically on open.store/holds the actual mod files content-addressed; the database’sinstalled_mod_filesmanifest maps each profile’s mods to the precise files they staged, which is what makes uninstall surgical.saves/<game_id>/is a real git repository per game, with one branch per profile. See Save management for the vault mechanics.wabbajack_cache/<manifest_hash>.wabbajackcaches manifest files keyed by their hash, so re-installs anddedupruns don’t re-download the list.
Non-essential cache (as opposed to data) lives separately under
<cache_dir>/modde/ — for example ~/.cache/modde/ on Linux.
The config directory holds modde’s small configuration state, including the instance registry:
~/.config/modde/
└── instances.toml # the instance registry (see below)
MODDE_DATA_DIR
You can override the data directory wholesale with the MODDE_DATA_DIR
environment variable, exposed as a global CLI flag:
# Point this invocation at a portable data directory
modde --data-dir /mnt/usb/modde-portable profile list
# Equivalent via the environment
MODDE_DATA_DIR=/mnt/usb/modde-portable modde profile list
When set, this path becomes <modde_data> directly — modde.db, store/,
profiles/, saves/, and all the rest are created underneath it. The override is
read once at startup and wins over both the default location and any active
instance. This is the simplest way to keep modde’s entire footprint on an external
drive or a per-project directory.
Precedence: an explicit
--data-dir/MODDE_DATA_DIRoverride beats the active instance, which in turn beats the default<data_dir>/modde/. If you use both instances and the override, the override wins for that invocation.
Multiple instances
An instance is a named, self-contained data directory. Instances let you keep
entirely separate modde states — for example one for your daily driver and one for
testing a risky Wabbajack list — without them sharing a database, store, or save
vaults. The active instance is recorded in ~/.config/modde/instances.toml.
# Create a new instance backed by its own data directory
modde instance create testing --data-dir ~/modde-instances/testing
# List instances (the active one is marked)
modde instance list
# Switch the active instance
modde instance switch testing
The registry file is plain TOML and easy to inspect:
active = "testing"
[[instances]]
name = "default"
data_dir = "/home/you/.local/share/modde"
is_default = true
[[instances]]
name = "testing"
data_dir = "/home/you/modde-instances/testing"
is_default = false
Behavior worth knowing:
- The first instance you create becomes the default (
is_default = true), and if no instance was active it is made active automatically. - Creating an instance creates its
data_dirfor you. Creating one whose name already exists is an error. - Switching to a name that doesn’t exist is an error; it never silently creates.
- Once an instance is active,
modde_dataresolves to that instance’sdata_dirfor every command — unless you override it withMODDE_DATA_DIR/--data-dirfor a single invocation (see the precedence note above).
Because each instance is just a directory, you can back one up, move it to another
disk, or delete it by operating on the data_dir path directly (and editing
instances.toml to drop the entry).
Backups
modde keeps two kinds of explicit backup under <modde_data>/backups/, separate
from the always-on git save vaults.
Mod backups (zip)
Snapshot a single mod’s staged directory into a timestamped zip, then restore the latest one on demand:
# Zip up a mod's current files
modde backup create <mod_id>
# List a mod's backups (oldest first)
modde backup list <mod_id>
# Restore the most recent backup for a mod
modde backup restore <mod_id>
Backups are stored at backups/mods/<mod_id>/<mod_id>_<timestamp>.zip. restore
extracts the newest zip for that mod; list is sorted oldest-first, so the last
entry is the one restore will pick.
Plugin-order backups (JSON)
The plugin load order (which .esp/.esm/.esl files load, in what order, and
whether each is enabled) can be snapshotted per profile and game:
# Save the current plugin order
modde backup plugins --profile my-skyrim --game skyrim-se
# Restore the most recent plugin-order backup
modde backup restore-plugins --profile my-skyrim --game skyrim-se
These land at backups/plugins/<game_id>/<profile>_<timestamp>.json and record
each plugin’s name and its enabled state, so a restore brings back exactly
what was loaded. Older, name-only backups are still readable — on restore they are
treated as all-enabled — so historical backups keep working across upgrades.
Stock (vanilla) game snapshots
A stock snapshot is a preserved copy of a clean, vanilla game install. modde
uses it as the known-good baseline for deployment and verification, so it can tell
whether the game directory has drifted from vanilla. Snapshots are stored under
<modde_data>/stock/<game_id>/.
# Snapshot the detected vanilla install for a game
modde stock snapshot skyrim-se
# Verify a snapshot still matches what was recorded
modde stock verify skyrim-se
How it works:
- The snapshot is built by hardlinking each file from the detected install into the snapshot directory (falling back to a copy when source and destination are on different filesystems). Hardlinks make the snapshot cheap in space — it shares bytes with the original install until either side changes.
- A deterministic tree hash is computed over the snapshot: every file’s
relative path is paired with its
xxh3-64content hash, the pairs are sorted by path, and the whole concatenation is hashed again. The result and the file count are written both into a.modde-tree-hash.tomlfile inside the snapshot and into thestock_snapshotstable inmodde.db. modde stock verifyrecomputes the tree hash and compares it against the recorded one. A match printsOK; a mismatch printsMISMATCHand recommends re-snapshotting, because something changed the snapshot’s contents (an updated game, a stray write, link breakage).
The detector locates the install via the game plugin (Steam library folders, etc., as resolved by modde’s path layer). If it can’t find a vanilla install, the snapshot command reports that the game wasn’t detected rather than snapshotting a modified directory.
Multi-machine sync
modde has no built-in cloud sync, but its on-disk layout is designed so you can reproduce a setup across machines with tools you already use. The reliable pattern is: declare profiles reproducibly, and version-control the save vaults.
Reproduce profiles with home-manager
The most robust way to get the same profiles on another machine is to declare
them in home-manager rather than copy the database. Your programs.modde.profiles
definitions are the source of truth; rebuilding on the second machine recreates
the profiles, Wabbajack lists, and tool overlays from the same inputs:
programs.modde.profiles.my-skyrim = {
game = "skyrim-se";
wabbajackList = {
url = "https://example.com/modlist.wabbajack";
hash = "sha256-...";
};
};
Because the Wabbajack list is pinned by hash and the home-manager module is the same flake input on both machines, the resulting profile is reproducible rather than copied. See Installation and the Home-Manager module reference.
If you maintain profiles imperatively instead, you can move them between machines
as TOML: drop exported profile.toml files into the destination’s
<modde_data>/profiles/<name>/ directories and run modde import, which scans the
profiles directory and loads any TOML profiles not already in the database.
Imported profiles are lock-stamped with TomlImport provenance (preserving any
pre-existing lock), so a shared, authoritative ordering survives the trip — see
Load order locking.
Git the save vaults
Each game’s save vault under <modde_data>/saves/<game_id>/ is already a git
repository, which makes it the natural unit to synchronize. Add a remote and push
it like any other repo:
cd ~/.local/share/modde/saves/skyrim-se
git remote add origin git@example.com:you/skyrim-saves.git
git push -u origin --all
On the second machine, clone (or pull) into the same path and modde will pick up the per-profile branches and history. This carries your actual save snapshots — including the mod fingerprints embedded in each commit — so the compatibility warnings on restore keep working across machines. The save-vault mechanics are documented in Save management.
What not to sync directly
modde.dbis machine-local truth and races badly if two machines write it concurrently — prefer reproducing profiles (home-manager or TOML import) over copying the database.store/,downloads/,staging/,wabbajack_cache/are large, derivable caches. Re-downloading (or re-deploying from a reproduced profile) is safer than rsyncing gigabytes of content-addressed blobs.stock/snapshots are hardlink trees tied to a specific install path on a specific machine; re-runmodde stock snapshoton each machine instead.
See also
- Profile management — what lives in
modde.db, locks, the experiment stack, andmodde import - Save management — the git-backed vaults under
saves/and how fingerprints ride along - Deployment — how the store and stock snapshot feed the game directory
- Wabbajack lists — the manifests cached under
wabbajack_cache/ - Installation — flake and home-manager setup for reproducible profiles
- Home-Manager module reference — declaring profiles in Nix for multi-machine reproduction
Troubleshooting
This page is organized as symptom → cause → fix. Start from the index below, jump to the matching section, and follow the commands. Every problem links to the guide that covers the underlying workflow in depth.
If a command itself is misbehaving (wrong flags, missing subcommand), check the
CLI reference first — this page covers runtime and
environment problems, not command syntax. For install-time and build-host
problems (Nix flake errors, openssl-sys, Home-Manager wiring), see the
Installation guide’s Troubleshooting section;
the most common one is reproduced under Build / openssl-sys failures
below.
By symptom
| Symptom | Section | Related guide |
|---|---|---|
modde detect doesn’t list my game | Game not detected | Playing |
| Generic game registered but install not found | Generic game not detected | Generic games |
modde game detect finds nothing / wrong dir | Generic game not detected | Generic games |
| Wabbajack list wants local game files | Wabbajack install issues | Wabbajack |
| Home Manager says profile is “awaiting game install” | Wabbajack install issues | Wabbajack |
| Wabbajack URL/hash mismatch | Wabbajack install issues | Wabbajack |
| modde reports unavailable authored files | Wabbajack install issues | Wabbajack |
| Nexus auth fails / CDN download refused | Nexus API authentication fails | Nexus |
| Deployment fails with symlink errors | Deployment fails | Deployment |
| Game left in a broken state after deploy | Deployment fails | Deployment |
modde tool apply says “could not detect install dir” | Tool apply / revert failures | Tools |
modde tool apply says “No files to apply” | Tool apply / revert failures | Tools |
modde tool revert says “No applied files to revert” | Tool apply / revert failures | Tools |
| OptiScaler: game crashes on first boot | OptiScaler first-boot crash | Tools |
| Save vault wants “adoption” | Save vault issues | Saves |
save restore warns about fingerprint mismatch | Save vault issues | Saves |
| Can’t reorder mods (profile locked) | Profile is locked | Profiles |
| Mod stuck on “pending user input” | FOMOD install stuck | FOMOD |
| “Unknown install type” / dossier written | Unknown install type | Mod installation |
Extraction fails (7z / unrar missing) | Missing extractors | Mod installation |
| Database looks corrupted | Database issues | Data management |
cargo build/test fails in openssl-sys | Build / openssl-sys failures | Installation |
Game not detected
Symptom: modde detect does not list a game you have installed.
Cause: modde auto-detects games via Steam and Heroic (GOG, Epic, Sideload). A game only appears when the launcher’s own metadata can be read and matched to a built-in or user-defined game registration. A game installed outside a supported launcher, or in a non-default library, may not be found automatically.
Fix:
modde detect
If your game doesn’t appear:
- Verify the game is installed via a supported launcher.
- For Steam, ensure the game’s
appmanifest_*.acffile exists in your Steam library (including extra libraries listed inlibraryfolders.vdf). - For Heroic, check that the game appears in
~/.config/heroic/GamesConfig/. - Use
--game-dirflags to specify the path manually on commands that accept it (for examplemodde scan --game <id> --game-dir <path>), or setgameDirin a Home-Manager profile.
See Playing a game for how detection feeds into modde play, and
Deployment & VFS for what happens once a game is detected.
Generic game not detected
Symptom: You registered a user-defined game with modde game add, but modde
cannot find its install directory, or modde game detect reports the wrong
directory (or none at all).
Cause: Generic (user-defined) game support is Partial — it gives you
deployment, conflict classification, launcher integration, and executable
management, but no bespoke scanner or save tracker. Detection runs an
override-free chain: install_path_override → install_dir_name under a Steam
steamapps/common/ library → modde’s generic launcher detection keyed on the
game ID. If none of those resolve, modde has nowhere to deploy. The most common
causes are an install_dir_name that doesn’t exactly match the Steam folder, a
missing steam_app_id, or an executable_dir pointing at the launcher stub
instead of the real binary.
Fix:
-
Find the real executable directory.
modde game detectwalks an install path (up to 4 levels deep) and lists executable-bearing directories, largest first:modde game detect "/home/you/.local/share/Steam/steamapps/common/Voidrunner"The top result (largest
.exe) is almost always yourexecutable_dir. If it reportsNo executable-bearing directories found under <path>, the path is wrong — pass the actual install root. -
Inspect and correct the spec:
modde game show <id> # full resolved spec + source path modde game list # all user-defined games and their files -
Re-register with the right detection fields (use
--forceto overwrite):modde game add <id> \ --display-name "<name>" \ --executable-dir "Binaries/Win64" \ --steam-app-id 998877 \ --install-dir-name "Voidrunner" \ --forceinstall_path_overrideis not settable fromadd. If detection still fails (non-Steam install, unusual layout), edit~/.local/share/modde/games/<id>.tomlby hand to add an absoluteinstall_path_override = "/abs/path", ormodde game importa spec that contains it. When set and present on disk, the override short-circuits all other detection.
What you do not get:
modde scan --game <id>has no game-specific heuristics for a generic game, andmodde savesave-profile features are unavailable — generic games shipscanner: Noneandsave_tracker: None. That is the boundary of thePartialstatus, not a bug. Import mods directly into a profile instead of scanning.
See Generic & user-defined games for the full
GameSpec schema, validation rules, and the modde game CLI surface.
Wabbajack install issues
Symptom / cause / fix for the common Wabbajack failure modes. See the Wabbajack modlists guide for the full workflow.
If a Skyrim SE modlist reports that it references local game files, pass
--game-dir "/path/to/Skyrim Special Edition" or set profiles.<name>.gameDir
in Home Manager. Lists such as Legends of the Frost need vanilla files from the
local Data/ directory (these are Wabbajack GameFileSourceDownloader entries —
not downloads, but reads of the installed game).
If Home Manager says a profile is awaiting game install, install the game with
Steam or Heroic first. modde waits for launcher-managed game installs; it does
not install Skyrim itself. After the game exists, set gameDir and rebuild Home
Manager. (Use installMode = "await-game" to keep activation non-fatal until
then — see Deployment & VFS.)
If a local game file fails hash verification, verify that Skyrim SE is fully installed, up to date, and not modified in place. Wabbajack expects exact vanilla file contents for game-file sources.
If Home Manager reports a Wabbajack URL/hash mismatch, recompute the Nix fetch
hash for the actual authored-files .wabbajack URL and update
wabbajackList.hash. Note that some authored-files CDN links resolve through
Wabbajack’s chunked download page, so pkgs.fetchurl may 404 even when modde’s
chunk-aware downloader works — in that case download with
modde wabbajack download and use wabbajackList.path instead.
If downloads fail before staging begins, check that
programs.modde.nexus.apiKeyFile points to a readable Nexus API key file (see
Nexus authentication).
If modde reports unavailable Wabbajack authored files, the upstream
authored-files entries referenced by the .wabbajack manifest are missing.
The error lists each archive, expected hash, metadata URL, and a curl -fI
validation command. The upstream modlist or Wabbajack authored-files publisher
must restore those exact files or publish a newer .wabbajack manifest.
If you have the exact archives locally, import them explicitly:
modde wabbajack import-archive /path/to/list.wabbajack /path/to/archive.7z
Import matches by Wabbajack hash only. A file with the same name but a different hash is refused.
Nexus API authentication fails
Symptom: Browsing, downloading, or installing from Nexus fails with an auth error, or downloads are refused.
Cause: A missing/invalid API key, or a Free (non-Premium) account trying to generate CDN download links. See Nexus Mods for the full setup.
Fix. Check your API key status:
modde nexus status
If invalid, re-authenticate:
modde nexus auth
API key lookup order: OAuth token, ~/.config/modde/nexus_api_key,
NEXUS_API_KEY env var, system keyring, NEXUS_API_KEY_FILE env var, legacy
settings.toml key.
CDN downloads require Premium: Free Nexus accounts cannot generate CDN download links. You’ll see an error if you try to download without Premium. For declarative (sops-nix / Home-Manager) key wiring, see Nexus → Using sops-nix.
Deployment fails
See Deployment & VFS for how the symlink farm is built and deployed.
Symlink errors
Symptom: deployment fails with symlink errors.
Cause: the game directory is missing, read-only, or has files locked by another process; or the staging directory is incomplete.
Fix:
- Check that the target game directory exists and is writable.
- Ensure no other process has locked files in the game directory.
- Verify the staging directory at
~/.local/share/modde/staging/<profile>/is intact.
Rollback
Symptom: a deployment left the game in a broken state.
Cause: the most recent deploy produced an unwanted layout; the previous good
state is preserved in staging.bak/.
Fix:
modde rollback --profile my-skyrim
This atomically swaps back to the previous deployment. To check that deployed
files still match their sources, run modde verify --profile my-skyrim (see
Deployment → Verifying integrity).
Tool apply / revert failures
Symptom: modde tool apply <tool> --game <id> or
modde tool revert <tool> --game <id> fails, or prints that there was nothing
to do.
Cause and fix depend on the message:
| Message | Cause | Fix |
|---|---|---|
unknown tool: '<tool>' | The tool ID is not one modde manages. | Use a valid ID: mangohud, vkbasalt, gamemode, reshade, optiscaler, or proton. See Tools & overlays. |
unsupported game: '<id>' | The game ID isn’t a built-in or registered user-defined game. | Check modde game list / modde game show <id>; register it with modde game add. |
could not detect install dir for <game> | apply/revert need the real game directory and detection failed. | Make sure the game is installed and detected (see Game not detected). For a generic game, fix detection (see Generic game not detected). |
No files to apply for <tool> | The tool is a launch/config integration (Proton, MangoHud, vkBasalt, GameMode) that does not write files into the game directory — or the chosen profile/config yields no files. | This is expected for those tools; they contribute env vars, wrapper commands, generated config files, and Wine DLL overrides at deploy/launch time, not patched files. Only ReShade and OptiScaler normally stage files into the game directory. |
No applied files to revert for <tool> | modde has no record of files it applied for that tool/game, so there is nothing to remove. | Nothing to revert. If you applied files outside modde, it will not own or remove them — modde only reverts what it recorded via tool apply. |
How tracking works. modde tool apply records the exact relative paths it
wrote into the database, and modde tool revert removes only those recorded
files, then clears the record. This is why reverting is clean and why a manual,
out-of-band copy of a DLL is not something revert will touch. If you adopted an
existing OptiScaler install (rather than letting modde install it), modde
records the working files so revert stays accurate — see
Tools → Applying tool patches.
# Re-apply cleanly: revert what modde tracked, then apply again
modde tool revert reshade --game skyrim-se
modde tool apply reshade --game skyrim-se
OptiScaler first-boot crash
Symptom: After applying an OptiScaler profile (for example Stellar Blade’s
community-dxgi), the game crashes on the very first launch with the proxy
DLL in place.
Cause: This is a known OptiScaler quirk for some titles: the proxy DLL
(dxgi.dll) initialises on first boot and the game can die before the next
launch succeeds. It is documented in the bundled profile notes, not a bad
install.
Fix: Relaunch the game. It typically works on the second launch. Before assuming the install is broken:
-
Confirm the prerequisites first. For Proton/UE titles like Stellar Blade, the Proton prefix must exist (
compatdata/<appid>/pfx) — launch the game once so Proton materialises the prefix, then apply OptiScaler and relaunch. -
Verify the proxy and companion files were staged:
modde tool apply optiscaler --game stellar-bladeThis writes
dxgi.dlland companion files into the game’sBinaries/Win64. -
If frame-gen HUD elements look wrong (interpolation artifacts on the HUD under DLSSG), set the in-game sharpness slider to 0 — also a documented workaround, not a modde bug.
If it still crashes on every launch, revert and re-apply to rule out a partial write:
modde tool revert optiscaler --game stellar-blade
modde tool apply optiscaler --game stellar-blade
See Tools & overlays for the OptiScaler source selector (official releases vs GOverlay builds) and the Stellar Blade page for that game’s specific profile notes.
Save vault issues
See Save management for how the git-backed vault, branches, and fingerprinting work.
“Adoption required”
Symptom: Switching to a profile for the first time refuses to proceed because existing saves are detected.
Cause: modde will not silently overwrite saves it has never seen. It asks you to adopt them into a profile first, so they become the initial vault snapshot.
Fix:
modde save adopt --game skyrim-se --profile my-skyrim
Restoring from an incompatible snapshot
Symptom: save restore warns about a fingerprint mismatch.
Cause: the snapshot was created with a different set of save-breaking mods (modde embeds a SHA-256 fingerprint of save-breaking mods in each snapshot).
Fix: You can still restore, but the save may not load correctly in-game. Browse history to find a compatible snapshot:
modde save history --game skyrim-se --profile my-skyrim
See Save fingerprinting for how compatibility is computed and reported.
Profile is locked
Symptom: you can’t reorder or remove mods.
Cause: the profile is locked — automatically after a Wabbajack install, a Nexus Collection, or a TOML import (to preserve a curator’s load order), or manually.
Fix. Inspect the lock:
modde profile lock-info my-skyrim
To unlock:
modde profile unlock my-skyrim
Or fork the profile with --unlock to create a freely editable copy (this strips
both the profile-level lock and all per-mod pins):
modde profile fork my-skyrim my-skyrim-custom --game skyrim-se --unlock
See Profile management → Load order locking.
FOMOD install stuck on “pending user input”
Symptom: a mod’s install status is PendingUserInput and it never finishes.
Cause: the mod ships a FOMOD installer that needs your selections (texture resolution, compatibility patches, and so on).
Fix. Either complete the wizard in the GUI, or apply a declarative config:
- Open the GUI (
modde gui) and complete the wizard, or - Generate and apply a declarative config:
modde fomod generate /path/to/mod --format toml > choices.toml
# Edit choices.toml to select your options
modde fomod apply /path/to/mod --config choices.toml --dest /path/to/output
See the FOMOD installer guide for inspecting a mod’s options and for
passing --fomod-config during a Nexus install.
Unknown install type (dossier written)
Symptom: modde can’t determine how to install a mod and stops.
Cause: the archive layout doesn’t match any recognized install method (single file set, FOMOD, or a game-specific layout). modde writes a dossier so you can investigate instead of guessing.
Fix. The dossier lands at
~/.local/share/modde/unknown-installers/<slug>/ and contains the archive tree,
file samples, and metadata.
modde mod diagnose <mod_id>
This prints the dossier path for investigation. See the Mod installation guide for how install methods are detected and what a valid layout looks like.
Missing extractors
Symptom: extraction fails for .7z or .rar archives.
Cause: modde shells out to 7z (7-Zip) and unrar for certain archive
formats, and they are not on your PATH.
Fix: On NixOS these are included in the development shell (7zz, unrar).
If running modde standalone, install them and ensure they are on your PATH.
This is the same class of problem as a bare-host build — the Nix dev shell
provides every external tool modde needs (see
Build / openssl-sys failures).
Database issues
Symptom: modde commands error out reading state, or the database looks corrupted.
Cause: the SQLite database at ~/.local/share/modde/modde.db was interrupted
mid-write (power loss, killed process), leaving recovery files behind.
Fix:
- Check if a
.db-journalor.db-walfile exists (SQLite recovery files). - Back up the database before attempting repairs.
- Try running any modde command — the schema migration system may repair minor issues.
See the Data management guide for the full on-disk layout (database, store, staging, save vaults) and backup guidance.
Exporting your mod list
For sharing or debugging, export your profile to CSV:
modde export --profile my-skyrim --output modlist.csv
See Data management for other export/backup paths.
Build / openssl-sys failures (build-host issue)
Symptom: A cargo build, cargo install modde-cli, or cargo test fails in
openssl-sys with something like Could not find directory of OpenSSL installation, or fails to link with errors mentioning wayland, xkbcommon,
or vulkan.
Cause: This is a build-host environment problem, not a modde bug. modde
does not vendor OpenSSL, and the GUI links against system Wayland / libxkbcommon
/ Vulkan libraries. A bare host without those headers cannot build the workspace,
and a plain cargo build/cargo test outside the Nix shell is not a reliable
signal.
Fix — use the Nix dev shell (recommended):
nix develop . -c cargo build --release
# or, against the remote flake:
nix develop codeberg:caniko/rs-modde
Inside nix develop, OpenSSL, SQLite, the GUI system libraries, and the external
extractors (7zz, unrar) are all provided, and LD_LIBRARY_PATH is set for
you. This is the authoritative build/test environment.
If you must build on a bare host, install the dev headers and point pkg-config
at them. On Debian/Ubuntu:
sudo apt install ca-certificates gcc pkg-config \
libssl-dev libsqlite3-dev libdbus-1-dev \
libwayland-dev libxkbcommon-dev libvulkan-dev
On Fedora: sudo dnf install openssl-devel pkg-config sqlite-devel dbus-devel wayland-devel libxkbcommon-devel vulkan-loader-devel. If OpenSSL is in a
non-standard prefix, export OPENSSL_DIR (or PKG_CONFIG_PATH). The full
treatment, including the Home-Manager and flake-registry pitfalls, is in the
Installation guide’s Troubleshooting section.
See also
- Playing a game — launch flow and game detection
- Wabbajack modlists —
.wabbajackinstall workflow - Nexus Mods — API keys, downloads, collections
- Deployment & VFS — symlink farm, rollback, verify
- Tools & overlays — apply/revert, OptiScaler, Proton
- Save management — vaults, fingerprinting, restore
- Profile management — locks, forks, experiment stack
- FOMOD installer — interactive and declarative installs
- Generic & user-defined games — register your own title
- CLI reference — every command and flag
- Installation — build-host and Nix issues
Home-Manager Module
If you use Nix, modde’s home-manager module lets you declare your mod profiles
as code. It is built directly on the flake’s package output, and on every
home-manager switch it runs an activation script that drives the modde CLI to
install and deploy the profiles you declare. This is one powerful option for Nix
users; it sits alongside the cross-platform
settings file & environment, which is how everyone configures
modde imperatively (and the only thing you need if you do not use Nix — manage
preferences there and drive modde with the CLI). This page documents every
option the module exposes, the validation assertions it enforces, the per-tool
settings schemas, and several complete worked examples.
The option source is nix/hm-module.nix;
the typed tool-settings schema is generated into
nix/tool-schema.nix
by modde dev export-tool-schema. To import the module, see
Installation → Flake input + home-manager module.
Top-level options
programs.modde.enable
Whether to enable the modde game mod manager. When false, the module adds
nothing to your closure and runs no activation work.
- Type:
bool - Default:
false
programs.modde.package
The modde package to use. Defaults to the flake’s modde package for your
system, which puts both modde (CLI) and modde-ui (GUI) on PATH. Override it
to pin a specific build or substitute a patched package.
- Type:
package - Default:
flake.packages.${pkgs.system}.modde
programs.modde.nexus.apiKeyFile
Path to a file containing your Nexus Mods API key. The activation script exports
its path as the NEXUS_API_KEY_FILE environment variable, which modde resolves
late in its API-key lookup chain —
making it sops-nix / agenix friendly, since the secret never enters the Nix
store.
- Type:
nullorpath - Default:
null
programs.modde.nexus.apiKeyFile = config.sops.secrets.nexus-api-key.path;
programs.modde.profiles
An attribute set of mod profiles to manage. The attribute name is the profile
name modde uses for --profile. Each value is a profile submodule.
- Type:
attrsOfprofile submodule - Default:
{}
The profile submodule
Each entry under programs.modde.profiles.<name> accepts the following options.
profiles.<name>.game
Game identifier string, passed to the CLI as --game. This is a free-form
string in the module — modde validates it at runtime — so generic / user-defined
games declared via modde game add and a GameSpec TOML work here too.
- Type:
str - Required: yes
The 15 shipping games use identifiers such as "skyrim-se", "skyrim-ae",
"fallout4", "fallout76", "starfield", "cyberpunk2077", and
"stellar-blade". See Supported games for the
full, authoritative list of built-in identifiers.
profiles.<name>.gameDir
Runtime path to the game installation. modde does not install base games; it
waits for a launcher-managed install. gameDir is required for Wabbajack
modlists that reference local vanilla game files — for example a Skyrim SE list
built from the vanilla Data directory.
- Type:
null,path, orstr - Default:
null
During activation, for Bethesda games (skyrim-se, skyrim-ae, fallout4,
fallout76, starfield) the module additionally checks that gameDir contains
a Data subdirectory before installing. If gameDir is unset, does not exist,
or is missing the required Data directory, a Wabbajack profile prints an
“awaiting game install” message and continues without failing.
profiles.<name>.installMode
Controls what home-manager activation does for this profile.
- Type: enum —
"auto","await-game", or"disabled" - Default:
"auto"
| Value | Behavior |
|---|---|
"auto" | Install and deploy when prerequisites are present; otherwise print an awaiting message |
"await-game" | Always skip install/deploy and print the next setup step (use before the game is present) |
"disabled" | Skip all activation work for the profile entirely |
profiles.<name>.wabbajackList
Wabbajack modlist source. Mutually exclusive with nexusCollection (see
Assertions).
- Type:
nullor submodule - Default:
null
| Option | Type | Default | Description |
|---|---|---|---|
url | null or str | null | URL to the .wabbajack modlist file |
hash | null or str | null | SHA-256 hash of the modlist file (paired with url) |
path | null, path, or str | null | Local or Nix store path to an already-available .wabbajack |
missingArchivePolicy | enum fail / omit-files / omit-mods | "fail" | What to do when optional manual/Nexus archives are absent |
manualArchives.<key> | attrsOf submodule | {} | User-provided manual / Nexus archives (see below) |
Set exactly one source: either path, or both url and hash together. Use
path when composing with requireFile or another fetcher that already
materializes the .wabbajack file. When url + hash are used, the module
fetches the modlist with pkgs.fetchurl at build time.
missingArchivePolicy is forwarded to the CLI as
--missing-archive-policy when the profile is installed:
| Policy | Effect |
|---|---|
"fail" | Abort the install if any required archive cannot be resolved |
"omit-files" | Skip individual files sourced from a missing archive |
"omit-mods" | Skip entire mods whose archive is missing |
wabbajackList.manualArchives.<key>
Manual or Nexus archives that Wabbajack cannot download automatically (premium
Nexus files, off-site mirrors, etc.). Each entry maps an archive to a local
source file. The attribute key may be either the Wabbajack xxh64 hex hash
(16 hex characters) or a readable label, in which case hash must be set
inside the entry.
| Option | Type | Default | Description |
|---|---|---|---|
hash | null or str | null | Wabbajack xxh64 hex hash; required when the key is a readable label, not a hash |
path | null, path, or str | null | Path to the exact source archive for this archive hash |
optional | bool | false | Allow activation to omit affected outputs when this archive is absent |
During activation, every entry with a non-null path is imported via
modde wabbajack import-archive before the install runs. Required entries (those
not marked optional) must set path, or the module fails an assertion.
profiles.<name>.nexusCollection
Nexus Collection source. Mutually exclusive with wabbajackList.
- Type:
nullor submodule - Default:
null
| Option | Type | Required | Description |
|---|---|---|---|
slug | str | yes | Nexus Collection slug |
version | str | yes | Collection version to install |
profiles.<name>.tools.<id>
Per-tool configuration for this profile, keyed by tool ID. Every tool slot
defaults to null (unconfigured). Setting a tool to an attrset opts it in to the
activation logic — enable = true runs modde tool enable, while leaving
enable at its default of false runs modde tool disable during activation.
- Type:
nullor per-tool submodule - Default:
null(for each known tool ID)
Recognized tool IDs:
| Tool ID | Description | settings shape |
|---|---|---|
mangohud | Performance HUD overlay | free-form |
vkbasalt | Vulkan post-processing | typed |
gamemode | System performance tuning | typed |
reshade | D3D / OpenGL post-processing for Wine-backed games | typed |
optiscaler | DLSS / FSR / XeSS upscaling | free-form |
proton | Proton runtime selection and DLL overrides | free-form |
Common options on every tool submodule:
| Option | Type | Default | Description |
|---|---|---|---|
enable | bool | false | Whether the tool is enabled for this profile |
applyOnActivation | bool | false | Re-run modde tool apply after configuration during activation |
settings | typed or free-form attrset | see below | Tool-specific settings (schema depends on the tool) |
release | null or release submodule | null | Pinned release asset (only valid for release-backed tools) |
profile | null or str/enum | null | OptiScaler per-game preset (only exists on the optiscaler tool) |
settings is typed for vkbasalt, gamemode, and reshade (each key is a
distinct option with its own type, default, and validation derived from the Rust
settings_schema()), and free-form for mangohud, optiscaler, and
proton (an attrsOf accepting bool, int, float, str, or a list of
strings). The per-tool key tables are in Tool settings reference
below. The reserved keys _game_id and optiscaler_profile are ignored if set
directly in settings (the module prints a warning and uses the dedicated
profile option instead).
tools.<id>.release
Pins a downloaded release asset for a release-backed tool. Today only
optiscaler supports release pinning (sourced from
nix/release-supporting-tools.nix);
setting release on any other tool fails an assertion.
| Option | Type | Default | Description |
|---|---|---|---|
tag | str | — | Release tag to pin (required) |
asset | str | — | Release asset file name (required) |
url | null or str | null | Download URL for the pinned asset |
hash | null or str | null | Fixed-output hash for the pinned asset (paired with url) |
path | null, path, or str | null | Local / store path to an already-downloaded asset |
Provide the asset either by path or by url + hash (the two are
mutually exclusive). When url + hash are set the module materializes the
asset with pkgs.fetchurl; with path you can hand it a requireFile result.
During activation the asset is installed with
modde tool install-release-from-path <id> --game <game> --tag <tag> --asset <asset> <src>.
tools.optiscaler.release = {
tag = "v0.7.7";
asset = "OptiScaler_v0.7.7.7z";
path = pkgs.requireFile {
name = "OptiScaler_v0.7.7.7z";
sha256 = "0000000000000000000000000000000000000000000000000000";
url = "https://github.com/optiscaler/OptiScaler/releases";
};
};
tools.optiscaler.profile
Selects an OptiScaler per-game preset. This option exists only on the
optiscaler tool. Its type is dynamic: if the game has registered presets (see
nix/optiscaler-profiles.nix),
the option becomes an enum of those presets; if the game has no registered
presets, setting profile to a non-null value fails an assertion.
- Type:
null,str, orenumof the game’s registered presets - Default:
null
Currently stellar-blade registers the community-dxgi preset; other games have
no registered presets yet. The chosen preset is forwarded to the CLI as the
optiscaler_profile=<preset> setting.
Assertions
The module evaluates a set of assertions per profile and per configured tool.
Each is reported with a programs.modde.profiles.<name>... message so failures
point straight at the offending option.
| Assertion | Rule |
|---|---|
| Exclusive source | wabbajackList and nexusCollection are mutually exclusive — set at most one |
| Wabbajack source | wabbajackList must set exactly one of: path, or url + hash together |
| Wabbajack url/hash pairing | wabbajackList.url and wabbajackList.hash must be set together (neither alone) |
| Required manual archives | Each manualArchives entry must set path, or mark optional = true |
| Manual-archive hashes | A manualArchives entry keyed by a readable label (not a 16-hex-char hash) must set hash |
| Duplicate manual-archive hashes | manualArchives entries must not resolve to duplicate hashes |
| Tool release support | release may only be set on a tool that supports release pinning (today: optiscaler only) |
| Tool release source | release.path is mutually exclusive with release.url + release.hash; exactly one source must be present |
| Tool release url/hash pairing | release.url and release.hash must be set together |
| OptiScaler profile registered | tools.optiscaler.profile may only be non-null if the profile’s game has registered OptiScaler presets |
Tool settings reference
The tables below enumerate every key in the typed and notable free-form tool
schemas, generated from
nix/tool-schema.nix.
Every key defaults to null (unset → modde uses its own default), and each typed
key is validated to its declared type during evaluation.
vkbasalt (typed)
| Key | Type | Description |
|---|---|---|
casSharpness | float (0 – 1, step 0.05) | Contrast Adaptive Sharpening amount |
effects | text | Colon/comma-separated effect list, such as cas or cas:fxaa |
enableOnLaunch | bool | Start with vkBasalt effects enabled |
reshadeIncludePath | path | Optional ReShade shader include directory for vkBasalt |
reshadeTexturePath | path | Optional ReShade texture directory for vkBasalt |
toggleKey | text | Key used to toggle vkBasalt |
gamemode (typed)
gamemode exposes no typed settings keys — its schema in tool-schema.nix is
empty. Use tools.gamemode.enable = true; (and optionally applyOnActivation)
to turn it on; there are no per-key settings to configure.
reshade (typed)
| Key | Type | Description |
|---|---|---|
dll_name | enum: dxgi.dll, d3d11.dll, dinput8.dll | DLL name copied into the executable directory |
source_dir | path | Directory containing ReShade DLLs, ReShade.ini, and shader folders |
optiscaler (free-form)
optiscaler settings are free-form (passed through to modde), but the schema
documents the recognized keys and their accepted values:
| Key | Type / values | Description |
|---|---|---|
copy_companion_files | bool | Copy fakenvapi, nvngx wrapper, and other DLLs found next to OptiScaler |
dll_overrides | text | Comma/whitespace-separated Wine DLL override base names |
emulate_fp8 | bool | Set DXIL_SPIRV_CONFIG=wmma_rdna3_workaround for the Latest (FP8) FSR4 variant |
enable_optipatcher | bool | Use OptiPatcher to unlock DLSS / DLSS frame-gen inputs without whole-game spoofing |
fsr4_variant | enum: latest_fp8, int8_402 | FSR4 payload copied as amd_fidelityfx_upscaler_dx12.dll |
ini_overrides.FSR.FGIndex | enum: auto, 0, 1 | OptiScaler [FSR] FGIndex override |
ini_overrides.FSR.UpscalerIndex | enum: auto, 0, 1, 2 | OptiScaler [FSR] UpscalerIndex override |
ini_overrides.Menu.Scale | float (0.5 – 2, step 0.1) | OptiScaler [Menu] Scale override |
ini_overrides.Menu.ShortcutKey | enum: auto, INSERT, HOME, END, DELETE, BACKQUOTE, F1…F12 | OptiScaler [Menu] ShortcutKey override |
ini_overrides.NvApi.OverrideNvapiDll | tri-state bool | OptiScaler OverrideNvapiDll override |
ini_overrides.fakenvapi.enable_trace_logs | tri-state bool | fakenvapi enable_trace_logs override |
ini_overrides.fakenvapi.force_latencyflex | tri-state bool | fakenvapi force_latencyflex override |
ini_overrides.fakenvapi.force_reflex | enum: 0, 1, 2 | fakenvapi force_reflex override |
ini_overrides.fakenvapi.latencyflex_mode | enum: 0, 1, 2 | fakenvapi latencyflex_mode override |
proxy_dll | enum: dxgi.dll, version.dll, dbghelp.dll, d3d12.dll, wininet.dll, winhttp.dll, winmm.dll, nvngx.dll, OptiScaler.asi | DLL name used to load OptiScaler |
source_mode | enum: github_release, goverlay_builds, goverlay_fgmod, local_dir | Where modde should get OptiScaler files from |
spoof_dlss | bool | Fallback DXGI spoofing path for games that still need whole-game spoofing |
proton (free-form, typed values)
proton settings are free-form, but most keys are boolean environment-variable
toggles. Notable typed keys:
| Key | Type / values | Description |
|---|---|---|
version_mode | enum: launcher_default, installed_version, install_with_protonup_rs | How modde chooses the Proton runner |
selected_version | enum: latest | Installed or requested GEProton version |
install_target | enum: steam | Target application passed to protonup-rs |
dll_override_mode | enum: auto, forced, off | How Proton contributes forced DLL overrides |
forced_dll_overrides | text | Comma/whitespace-separated DLL base names (e.g. dxgi, winmm) |
extra_env | text | Additional KEY=VALUE lines exported at launch |
prefix_path_override | path | Optional Proton/Wine prefix override |
wrapper_order | enum: after-modde, before-tools | Where Proton wrapper integration appears in the launch chain |
The remaining proton_*, radv_*, mesa_*, glx_*, enable_*, staging_*,
and steamdeck keys are booleans that export the matching environment variable
(for example proton_enable_nvapi = true exports PROTON_ENABLE_NVAPI=1,
proton_enable_wayland exports PROTON_ENABLE_WAYLAND=1, radv_perftest_rt
exports RADV_PERFTEST=rt,emulate_rt). See nix/tool-schema.nix for the full
list and the exact variable each one sets.
mangohud (free-form passthrough)
mangohud settings are free-form and forwarded to modde verbatim. The schema
documents the full MangoHud key surface (fps, gpu_temp, cpu_temp,
frametime, position, font_size, background_alpha, fps_limit,
toggle_hud, and dozens more), but no key is constrained by the Nix type system
beyond the free-form value types (bool, int, float, str, list of str).
For example:
tools.mangohud = {
enable = true;
settings = {
fps = true;
gpu_temp = true;
cpu_temp = true;
frametime = true;
position = "top-left";
font_size = 20;
};
};
Worked examples
Wabbajack Skyrim profile with gameDir and await-game
A complete Skyrim SE Wabbajack list, declared before the game is installed. With
installMode = "await-game", the first rebuild prints the next setup step and
does nothing else; once Steam has installed the game, switch to
installMode = "auto" and set gameDir, and the next rebuild installs and
deploys the list. This example also pins a manual (premium-Nexus) archive.
{ config, ... }:
{
programs.modde = {
enable = true;
nexus.apiKeyFile = config.sops.secrets.nexus-api-key.path;
profiles.lotf = {
game = "skyrim-se";
installMode = "auto"; # flip from "await-game" once the game exists
gameDir = "/home/me/.local/share/Steam/steamapps/common/Skyrim Special Edition";
wabbajackList = {
url = "https://example.com/legends-of-the-frost.wabbajack";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
missingArchivePolicy = "fail";
manualArchives = {
# Keyed by a readable label, so `hash` is required:
unofficial-patch = {
hash = "0123456789abcdef";
path = "/home/me/Downloads/USSEP.7z";
};
# An optional archive — activation may omit affected outputs if absent:
optional-enb = {
hash = "fedcba9876543210";
optional = true;
};
};
};
};
};
}
Nexus Collection Cyberpunk profile
programs.modde.profiles.cyberpunk-mods = {
game = "cyberpunk2077";
installMode = "auto";
nexusCollection = {
slug = "my-cyberpunk-collection";
version = "2.1.0";
};
};
MangoHud + OptiScaler with real settings
A Stellar Blade profile that turns on MangoHud with a real overlay layout and
enables OptiScaler with its community-dxgi preset, real INI overrides, and a
pinned release asset applied on every activation.
{ pkgs, ... }:
{
programs.modde.profiles.stellar-blade = {
game = "stellar-blade";
installMode = "auto";
gameDir = "/home/me/Games/StellarBlade";
tools = {
mangohud = {
enable = true;
settings = {
fps = true;
frametime = true;
gpu_temp = true;
cpu_temp = true;
gpu_load_change = true;
position = "top-left";
font_size = 20;
background_alpha = 0.4;
};
};
optiscaler = {
enable = true;
applyOnActivation = true;
profile = "community-dxgi";
settings = {
source_mode = "github_release";
proxy_dll = "dxgi.dll";
fsr4_variant = "latest_fp8";
"ini_overrides.FSR.UpscalerIndex" = "auto";
"ini_overrides.Menu.Scale" = 1.2;
};
release = {
tag = "v0.7.7";
asset = "OptiScaler_v0.7.7.7z";
path = pkgs.requireFile {
name = "OptiScaler_v0.7.7.7z";
sha256 = "0000000000000000000000000000000000000000000000000000";
url = "https://github.com/optiscaler/OptiScaler/releases";
};
};
};
};
};
}
With applyOnActivation = true, the release asset is written into the game
directory every time home-manager switches to the profile, keeping the pin in
sync automatically. Leave it off if you prefer to run modde tool apply by hand.
sops-nix apiKeyFile
Keep the Nexus API key out of the Nix store entirely by pointing apiKeyFile
at a sops-nix (or agenix) secret. The module exports it as NEXUS_API_KEY_FILE
during activation, which modde reads through its
API-key precedence chain.
{ config, ... }:
{
sops.secrets.nexus-api-key = {
sopsFile = ./secrets.yaml;
# mode/owner default to the activating user
};
programs.modde = {
enable = true;
nexus.apiKeyFile = config.sops.secrets.nexus-api-key.path;
profiles.cyberpunk-mods = {
game = "cyberpunk2077";
nexusCollection = {
slug = "my-cyberpunk-collection";
version = "2.1.0";
};
};
};
}
First install flow
modde does not install Steam or Heroic games. You can declare a profile before
the game exists by omitting gameDir or setting installMode = "await-game".
home-manager activation prints the next step and continues without failing.
After installing the game through Steam or Heroic, set gameDir to its install
directory and use installMode = "auto". The next activation installs the
Wabbajack profile (if its lock info is missing), then deploys it and applies the
configured tools. For non-Wabbajack profiles, activation simply runs
modde deploy and applies tools.
See also
- Settings file & environment — the imperative
settings.toml, environment variables, and full Nexus key precedence - Installation — install channels and how to import the module
- Supported games — authoritative game identifiers
- Tools guide — what each tool does at runtime
- Wabbajack guide — manual archives and modlist install
Settings file & environment
The settings file is modde’s primary, cross-platform configuration mechanism: it works the same on Linux, macOS, and Windows, with no Nix required. modde’s configuration state lives in a small set of plain TOML files under your platform config directory, plus a handful of environment variables that override paths and behavior at runtime. The CLI and GUI read and write these files directly. This page documents the on-disk schema, the environment surface, the instance registry, and how all the configuration layers rank against each other.
If you use Nix, you can additionally declare profiles as code through the home-manager module; that is one option layered on top of the same runtime, not a replacement for the settings file.
The authoritative sources are
crates/modde-core/src/settings.rs,
crates/modde-core/src/paths.rs,
and
crates/modde-core/src/instance.rs.
settings.toml
Location
settings.toml lives in modde’s config directory, <config_dir>/modde/:
| OS | Config dir | Full path |
|---|---|---|
| Linux | $XDG_CONFIG_HOME, else ~/.config | ~/.config/modde/settings.toml |
| macOS | ~/Library/Application Support | ~/Library/Application Support/modde/settings.toml |
| Windows | %APPDATA% (e.g. C:\Users\X\AppData\Roaming) | %APPDATA%\modde\settings.toml |
On Linux, setting XDG_CONFIG_HOME relocates the directory. The file is written
with toml::to_string_pretty, so it stays human-readable and hand-editable.
Loading behavior
Settings loading is forgiving by design:
- A missing file loads as all-defaults.
- A malformed file (parse error) also falls back to all-defaults rather than erroring.
- Unknown keys are ignored, so a newer file does not break an older binary.
- Missing keys are filled with their defaults, so a partial file is valid.
Keys
| Key | Type | Default | Description |
|---|---|---|---|
nexus_api_key | string | "" | Legacy inline Nexus API key (lowest-precedence key source — prefer the keyring or nexus auth) |
game_paths | array of { game_id, path } | [] | Configured game install paths (typically 1–4 games) |
download_dir | path (optional) | unset | Override for the downloads directory |
theme | string | "" | UI theme name (e.g. Nord); empty means the built-in default |
selected_game | string (optional) | unset | The game last selected in the UI |
update_check.enabled | bool | true | Whether modde checks for newer releases on startup |
nexus_api_key is serialized only when non-empty, so a clean settings file does
not contain it. It is the legacy key source — modde keeps reading it for
backward compatibility, but it ranks last in the
Nexus key precedence. Prefer modde nexus auth, the
system keyring, or NEXUS_API_KEY / NEXUS_API_KEY_FILE.
game_paths is an array of tables; each entry pairs a game_id with its install
path. modde looks up a game’s path by game_id, and setting a path either adds
a new entry or updates the existing one (no duplicates).
Example
nexus_api_key = "legacy-key-if-you-must"
download_dir = "/data/modde/downloads"
theme = "Nord"
selected_game = "cyberpunk2077"
[[game_paths]]
game_id = "cyberpunk2077"
path = "/data/games/Cyberpunk 2077"
[[game_paths]]
game_id = "skyrim-se"
path = "/data/games/Skyrim Special Edition"
[update_check]
enabled = true
A minimal file is equally valid — every omitted key falls back to its default:
selected_game = "skyrim-se"
Data, cache, and downloads directories
settings.toml only stores configuration; modde’s actual mod store, profiles,
database, and downloads live under the data directory, which is separate from
the config directory and platform-aware:
| OS | Data dir base |
|---|---|
| Linux | $XDG_DATA_HOME, else ~/.local/share |
| macOS | ~/Library/Application Support |
| Windows | %APPDATA% |
modde’s data root is <data_dir>/modde/ (unless overridden — see
Precedence), with this layout:
| Path | Purpose |
|---|---|
<modde_data>/store/ | Content-addressed mod file store |
<modde_data>/staging/ | Staging scratch space |
<modde_data>/profiles/ | Per-profile state |
<modde_data>/downloads/ | Default downloads directory |
<modde_data>/stock/ | Stock (vanilla) game snapshots |
<modde_data>/saves/ | Save vaults (one git repo per game) |
<modde_data>/wabbajack_cache/ | Cached .wabbajack manifest files |
<modde_data>/modde.db | SQLite database |
The cache directory is <cache_dir>/modde/ ($XDG_CACHE_HOME or ~/.cache on
Linux, ~/Library/Caches on macOS, %LOCALAPPDATA% on Windows). The downloads
directory defaults to <modde_data>/downloads/; the download_dir key in
settings.toml overrides it.
Environment variables
modde honors the following environment variables:
| Variable | Effect |
|---|---|
MODDE_DATA_DIR | Override the data directory. Equivalent to the global --data-dir flag (the flag wins if both set) |
NEXUS_API_KEY | Nexus API key, read directly from the environment (see precedence) |
NEXUS_API_KEY_FILE | Path to a file containing the Nexus API key — sops-nix / agenix friendly; set by the home-manager module |
MODDE_NO_UPDATE_CHECK | When set to 1/true/yes/on, disables the startup update check regardless of settings.toml |
MODDE_UPDATE_CHECK_URL | Override the release endpoint queried by the update check (defaults to the Codeberg releases API) |
XDG_CONFIG_HOME | Linux: relocates the config dir (and therefore settings.toml and instances.toml) |
XDG_DATA_HOME | Linux: relocates the data dir base |
XDG_CACHE_HOME | Linux: relocates the cache dir base |
A few more variables exist for niche purposes: MODDE_HEAP_PROFILE (write a DHAT
heap profile; requires the heap-profile feature), MODDE_CHROMIUM (path to a
Chromium binary used by the Wabbajack login flow), and, only when modde is built
with the optional remote-telemetry feature, RS_MODDE_TELEMETRY_ENDPOINT /
RS_MODDE_TELEMETRY_TOKEN. These are not part of normal configuration.
The global --data-dir CLI flag (backed by MODDE_DATA_DIR) overrides the data
directory for a single invocation, for example:
modde --data-dir /mnt/big/modde profile list
# or, equivalently for the whole session:
MODDE_DATA_DIR=/mnt/big/modde modde profile list
Instance registry (instances.toml)
modde supports multiple instances — independent, self-contained data
directories — registered in <config_dir>/modde/instances.toml (next to
settings.toml). Each instance names a data_dir; one instance can be marked
default, and one is active at a time. When an active instance is set, its
data_dir becomes modde’s data directory (see
Data directory resolution).
Schema:
| Field | Type | Description |
|---|---|---|
active | string (optional) | Name of the currently active instance |
instances | array of { name, data_dir, is_default } | Registered instances |
instances[].name | string | Unique instance name |
instances[].data_dir | path | The instance’s self-contained data directory |
instances[].is_default | bool | Whether this is the default instance |
Example:
active = "modding-rig"
[[instances]]
name = "default"
data_dir = "/home/me/.local/share/modde"
is_default = true
[[instances]]
name = "modding-rig"
data_dir = "/mnt/nvme/modde"
is_default = false
Creating an instance makes a directory and registers it; the first instance
created becomes the default and the active one. Switching changes active.
Instance names must be unique.
Configuration precedence
modde’s configuration comes from several layers. Two precedence chains matter: the data directory resolution and the Nexus API key resolution.
Data directory resolution
The effective data directory is chosen in this order (first match wins):
- Explicit override — the global
--data-dirflag orMODDE_DATA_DIRenvironment variable (the flag takes precedence over the env var). This also wins over an in-process override set at startup. - Active instance — the
data_dirof the active instance ininstances.toml, if one is set. - Default —
<data_dir>/modde/, where<data_dir>honorsXDG_DATA_HOMEon Linux and the platform data directory elsewhere.
Nexus API key precedence
When modde needs a Nexus API key it walks this chain and uses the first source
that yields a non-empty key (from
crates/modde-sources/src/nexus/auth.rs):
- OAuth token — a non-expired token from
modde nexus authOAuth login. - modde config file —
<config_dir>/modde/nexus_api_key, written bymodde nexus auth(created with0600permissions on Unix). NEXUS_API_KEY— the environment variable, read directly.- System keyring — the secret-service / D-Bus keyring entry
(
service = "modde",key = "nexus-api-key"). NEXUS_API_KEY_FILE— a file path; the file’s trimmed contents are used. This is the sops-nix-friendly source that the home-manager module’snexus.apiKeyFileexports.- Legacy
settings.toml— thenexus_api_keykey, kept for backward compatibility and ranked last.
If none yield a key, modde reports
No Nexus API key found. Set NEXUS_API_KEY env var or run "modde nexus auth".
How Nix, the settings file, env vars, and the keyring relate
The layers do not conflict so much as compose:
- The home-manager module owns the declarative surface — profiles,
Wabbajack lists, Nexus collections, and per-tool config — and drives the CLI on
every rebuild. It exports
NEXUS_API_KEY_FILEwhen you setnexus.apiKeyFile, but it does not writesettings.tomlkeys liketheme,download_dir, orselected_game. settings.tomlowns the imperative, user-facing preferences (game paths, download dir, theme, selected game, update-check toggle). The CLI and GUI read and write it. For the Nexus key specifically it is the lowest-precedence source.- Environment variables override at runtime —
MODDE_DATA_DIRfor the data dir,NEXUS_API_KEY/NEXUS_API_KEY_FILEfor the key,MODDE_NO_UPDATE_CHECKfor the update check. They take effect regardless ofsettings.toml. - The system keyring is the recommended interactive store for the Nexus key
(set it with
modde nexus auth); it outranksNEXUS_API_KEY_FILEand the legacysettings.tomlkey, but is itself outranked byNEXUS_API_KEYand the modde-owned config file.
In short: the CLI/GUI manage settings.toml and store the Nexus key in the
keyring or environment on every platform. If you use Nix, you can additionally
declare profiles in the home-manager module and keep the Nexus secret in
nexus.apiKeyFile — layered on top of, not instead of, the settings file.
See also
- Home-Manager module — the declarative configuration surface
- Installation — install channels and the home-manager module import
- Nexus guide — authenticating with Nexus Mods
- Downloads guide — the download directory and backends
CLI Reference
This is the exhaustive reference for the modde command-line interface. Every
top-level command group, every subcommand, every flag, and the environment
variables modde reads are documented here. The binary is invoked as modde
(installed via the Nix flake); the GUI is
the same binary under modde gui.
Run modde --help or modde <command> --help at any time for the
clap-generated usage of a specific command.
Global flags
These flags are accepted on every command (clap global = true).
| Flag | Environment | Description |
|---|---|---|
--data-dir <path> | MODDE_DATA_DIR | Override the data directory for a single invocation (default: ~/.local/share/modde) |
--heap-profile <path> | MODDE_HEAP_PROFILE | Feature-gated. Write a DHAT heap profile. Requires building with --features heap-profile; errors out otherwise |
--debug-panic | — | Feature-gated, hidden. Panic after startup to smoke-test the remote-telemetry crash-capture path. Only compiled in with --features remote-telemetry |
--data-dir overrides the active instance (see modde instance)
for that one command only — it does not change the persisted active instance. The
release builds shipped through the flake do not enable heap-profile or
remote-telemetry, so --heap-profile returns an explanatory error and
--debug-panic is absent unless you build those features yourself.
Environment variables
modde reads the following environment variables in addition to the --data-dir
override above.
| Variable | Default | Purpose |
|---|---|---|
MODDE_DATA_DIR | ~/.local/share/modde | Data directory (DB, store, downloads, backups). Equivalent to --data-dir. |
NEXUS_API_KEY | — | Nexus Mods API key, used when no modde-owned config-file key is present. |
NEXUS_API_KEY_FILE | — | Path to a file containing the Nexus API key (read after the keyring is consulted). |
GITHUB_TOKEN | — | Bearer token for the GitHub API; raises rate limits for release-backed tools (reshade, optiscaler). |
RUST_LOG | — | tracing/env_logger-style filter controlling log verbosity (e.g. RUST_LOG=modde=debug). |
MODDE_BYTE_CACHE_MIB | 512 | Size (MiB) of the in-memory LRU byte cache used by the download/source layer. |
MODDE_ZSTD_MIN_BYTES | 1048576 | Minimum file size (bytes) before Wabbajack staging entries are zstd-compressed. Default 1 MiB. |
MODDE_ZSTD_LEVEL | 9 | zstd compression level for staging (clamped to the 1–22 range). |
MODDE_ARCHIVE_RETENTION | keep | Default source-archive retention after a Wabbajack archive batch integrates. Accepts keep, prune-applied (aliases prune/delete), or auto. The --archive-retention flag on install wabbajack overrides this. |
MODDE_HEAP_PROFILE | — | Same as the --heap-profile flag; only meaningful in a heap-profile build. |
Nexus API key resolution order
When a command needs a Nexus key, modde resolves it in this order and uses the first source that yields a non-empty value:
- The modde-owned config file (written by
modde nexus auth). NEXUS_API_KEY.- The system keyring.
NEXUS_API_KEY_FILE.- A legacy key stored in app settings.
An OAuth token (if present and unexpired) takes precedence over the API-key
chain. Under Home Manager, prefer programs.modde.nexus.apiKeyFile, which feeds
NEXUS_API_KEY_FILE.
modde profile
Manage mod profiles.
profile list
List all profiles.
modde profile list [--game <id>]
| Flag | Description |
|---|---|
--game | Filter by game ID |
profile create
Create a new profile.
modde profile create <name> --game <id>
profile delete
Delete a profile.
modde profile delete <name> [--game <id>]
profile switch
Switch to a profile. Automatically swaps saves.
modde profile switch <name> --game <id>
profile active
Show the active profile for a game.
modde profile active --game <id>
profile try
Push a profile onto the experiment stack. Can be stacked multiple times.
modde profile try <name> --game <id>
profile rollback
Pop the experiment stack, returning to the previous profile.
modde profile rollback --game <id>
profile commit
Accept the current experiment and clear the rollback stack.
modde profile commit --game <id>
profile fork
Clone a profile including its mods, load order rules, and saves.
modde profile fork <source> <name> --game <id> [--unlock]
| Flag | Description |
|---|---|
--unlock | Strip profile-level lock and all per-mod pins from the fork |
profile lock
Apply a manual load order lock to a profile.
modde profile lock <name> [--game <id>] [--note <text>]
profile unlock
Clear the profile-level load order lock.
modde profile unlock <name> [--game <id>]
profile lock-info
Show lock status and per-mod pins for a profile.
modde profile lock-info <name> [--game <id>]
profile lock-mod
Pin an individual mod in place.
modde profile lock-mod <name> <mod_id> [--game <id>] [--note <text>]
profile unlock-mod
Release a per-mod pin.
modde profile unlock-mod <name> <mod_id> [--game <id>]
profile dedup
Detect filesystem-scanner rows that duplicate Wabbajack manifest entries.
modde profile dedup <name> [--game <id>] [--manifest <path>] [--apply]
| Flag | Description |
|---|---|
--manifest | Path to a .wabbajack file as the authoritative reference |
--apply | Actually delete leaked rows (without this flag, dry-run only) |
Without --manifest, this runs the layer-1 heuristic: it lists filesystem-scanner
rows (cet/*, reds/*, tweak/*, archive/*, redmod/*) on a locked profile as
read-only “suspects”. With --manifest, the manifest’s install directives classify
each suspect as LEAKED (safe to delete) or GENUINE (a user addition to keep), and
--apply deletes the LEAKED rows.
modde play
Switch profile, deploy mods, and launch the game.
modde play [profile] --game <id> [--no-deploy] [--no-switch] [--no-capture]
| Flag | Description |
|---|---|
--no-deploy | Skip mod deployment |
--no-switch | Skip profile switch |
--no-capture | Skip save auto-capture after game exit |
modde deploy
Deploy mods for the active or specified profile.
modde deploy [--profile <name>] [--game <id>]
modde rollback
Rollback to the previous deployment.
modde rollback [--profile <name>] [--game <id>]
modde install
Install mods from various sources.
install wabbajack
Install from a Wabbajack modlist. This is the largest single command in the CLI; the defaults below match a normal full install.
modde install wabbajack <path> [--profile <name>] [--game-dir <path>] [flags...]
| Flag | Default | Description |
|---|---|---|
--profile | — | Target profile (created if it doesn’t exist) |
--game-dir | — | Game installation directory to deploy mods into |
--force | false | Force full reinstall, skipping preflight checks |
--no-deploy | false | Stage into modde’s data dir but skip the final copy into --game-dir (Stock-Game lists) |
--continue-on-error | false | Log per-archive failures instead of aborting; install proceeds as far as possible |
--reset-staging | false | Explicitly discard existing Wabbajack staging before installing |
--skip-validate | false | Skip staging validation before deploy |
--diagnostics-dir | — | Write apply diagnostics JSONL to this directory (consumed by wabbajack analyze-diagnostics) |
--diagnostics-interval | 30 | Diagnostics heartbeat interval in seconds |
--stall-warn-seconds | 600 | Warn when apply makes no batch/sentinel progress for this many seconds |
--stall-abort-seconds | 1800 | Abort when stalled this long and cgroup memory/swap are saturated |
--archive-retention | keep | Source-archive retention after successful integration: keep, prune-applied, or auto |
--missing-archive-policy | fail | Behavior for missing optional manual/Nexus archives: fail, omit-files, or omit-mods |
--acquire-missing | false | Frontload assisted manual-archive acquisition before applying |
--acquire-download-dir | — | Browser download directory to watch during assisted acquisition |
--acquire-timeout | 900 | Per-archive assisted acquisition timeout in seconds |
--acquire-include-nexus | false | Include Nexus archives in frontloaded acquisition |
--acquire-browser-controller | false | Use controlled Chromium tabs for frontloaded acquisition |
--no-acquire-missing | false | Disable automatic frontloaded manual-archive acquisition |
# Smoke-run a large list without writing into the live game install
modde install wabbajack ./lotf.wabbajack --profile lotf \
--no-deploy --continue-on-error --skip-validate \
--diagnostics-dir ./wj-diag --missing-archive-policy omit-mods
See the Wabbajack guide for the full readiness workflow.
install nexus-collection
Install a Nexus Collection.
modde install nexus-collection <slug> [--version <rev>] [--profile <name>]
| Flag | Description |
|---|---|
--version | Pin a specific collection revision |
--profile | Target profile (created if it doesn’t exist) |
install mod
Install a single mod from Nexus.
modde install mod <url> [--profile <name>] [--fomod-config <path>]
| Flag | Description |
|---|---|
--fomod-config | Path to a FOMOD declarative config (TOML or JSON) for non-interactive installation |
modde mod
Manage individual mods.
mod remove
Remove an installed mod from a profile and unlink its staged files. Removal uses
the V8 installed_mod_files manifest, so it is precise — no orphaned files, no
collateral damage.
modde mod remove <mod_id> [--profile <name>]
| Flag | Description |
|---|---|
--profile | Profile to remove from. Defaults to the active / unambiguous profile. |
The mod_id is the id stored in the profile — usually
<domain>_<mod_id>_<file_id> for Nexus installs.
mod diagnose
Print the skill-dossier path and an inline prompt for a mod whose install type could not be detected. Handy for piping into an agent or pasting into a chat.
modde mod diagnose <mod_id>
modde wabbajack
Search, download, inspect, prepare, and support Wabbajack modlists. These are the
read-only and supporting utilities; the actual install runs through
modde install wabbajack.
wabbajack search
Search public Wabbajack modlist catalogs.
modde wabbajack search [query] [--game <id>] [--source official|authored|both] [--json]
| Flag | Default | Description |
|---|---|---|
--game | — | Filter by game ID |
--source | both | Catalog source: official, authored, or both |
--json | — | Emit machine-readable JSON |
wabbajack download
Download a .wabbajack file by URL, machine URL, or title. Authored-files URLs
are downloaded through modde’s chunk-aware downloader when required.
modde wabbajack download <url-or-machine-url> [--output <dir>]
| Flag | Description |
|---|---|
--output | Output directory for the downloaded file |
wabbajack hm-snippet
Generate a Home Manager profile snippet for a .wabbajack source.
modde wabbajack hm-snippet <url-or-file> --profile <name> --game <id> [--game-dir <path>] [--output <path>]
| Flag | Description |
|---|---|
--profile | Profile name to emit in the snippet (required) |
--game | Game ID to emit in the snippet (required) |
--game-dir | Game install directory to emit |
--output | Write the snippet to a file instead of stdout |
wabbajack import-archive
Import local archives into the modde store by matching the Wabbajack manifest hash. Useful when an upstream authored-files archive disappeared but you still have the exact archive in an old Wabbajack cache.
modde wabbajack import-archive <path.wabbajack> <archive-path>...
The command hashes each archive and imports only files whose Wabbajack hash is referenced by the manifest. Matching by filename is intentionally refused — only exact hash matches are accepted.
wabbajack acquire-missing
Open manual Wabbajack archive pages and import matching browser downloads.
modde wabbajack acquire-missing <manifest> [flags...]
| Flag | Default | Description |
|---|---|---|
--download-dir | — | Browser download directory to watch |
--data-dir | — | Override the data directory for this acquisition |
--browser-profile | — | Browser profile directory to use |
--include-nexus | false | Include Nexus archives in acquisition |
--browser-controller | false | Drive controlled Chromium tabs instead of just watching downloads |
--timeout | 900 | Per-archive acquisition timeout in seconds |
--json | false | Emit machine-readable JSON |
wabbajack missing-impact
Report missing manual/Nexus archives and their install impact (how many files / mods would be omitted).
modde wabbajack missing-impact <manifest> [--data-dir <path>] [--json] [--nix-snippet]
| Flag | Description |
|---|---|
--data-dir | Override the data directory |
--json | Emit machine-readable JSON |
--nix-snippet | Print Home Manager manualArchives entries for the missing archives |
wabbajack manual-links
Print missing manual-archive URLs that require an operator to visit each page.
modde wabbajack manual-links <manifest> [--data-dir <path>] [--json]
wabbajack assess
Assess readiness for a large Wabbajack install without mutating any state.
modde wabbajack assess <manifest> [--profile <name>] [--game-dir <path>] [--json]
| Flag | Description |
|---|---|
--profile | Profile context for the assessment |
--game-dir | Game install directory to assess against |
--json | Emit machine-readable JSON |
wabbajack analyze-diagnostics
Summarize the Wabbajack diagnostics JSONL written by a previous install run (the
directory passed to --diagnostics-dir).
modde wabbajack analyze-diagnostics <diagnostics-dir> [--json]
# Assess, resolve missing archives, then analyze the run afterwards
modde wabbajack assess ./lotf.wabbajack --game-dir ~/Games/skyrim
modde wabbajack missing-impact ./lotf.wabbajack --nix-snippet
modde wabbajack analyze-diagnostics ./wj-diag
modde game
Manage user-defined games. Built-in games are resolved from the internal registry; these commands register and inspect generic / user-defined games via a GameSpec TOML. (User-defined game support is Partial — deployment, conflict analysis, and launchers work, but there is no bespoke per-game scanner or save tracker. See the generic games guide.)
game add
Add or overwrite a user-defined game registration.
modde game add <id> --display-name <name> --executable-dir <path> [flags...]
| Flag | Description |
|---|---|
--display-name | Human-readable name (required) |
--executable-dir | Directory containing the game executable, relative to the install (required) |
--steam-app-id | Steam App ID, for launcher detection |
--install-dir-name | Steam steamapps/common directory name |
--mod-dir | Mod deployment subdirectory |
--nexus-domain | Nexus Mods domain slug for this game |
--proxy-dll <name> | Proxy DLL name (repeatable) used by OptiScaler/ReShade-style hooks |
--force | Overwrite an existing user-defined game TOML |
modde game add stalker2 \
--display-name "S.T.A.L.K.E.R. 2" \
--executable-dir "Stalker2/Binaries/Win64" \
--steam-app-id 1643320 \
--nexus-domain stalker2heartofchornobyl \
--proxy-dll dinput8.dll
game list
List user-defined games.
modde game list
game remove
Remove a user-defined game registration.
modde game remove <id> [--yes]
| Flag | Description |
|---|---|
--yes | Skip the confirmation prompt |
game detect
Detect executable-bearing directories under a game install (helps you pick the
right --executable-dir for game add).
modde game detect <install-path>
game show
Show a resolved game registration (user-defined first, then the built-in registry).
modde game show <id>
game export
Export a game registration to TOML.
modde game export <id> [--with-optiscaler] [--output <path>]
| Flag | Description |
|---|---|
--with-optiscaler | Append resolved OptiScaler profiles to the exported TOML |
--output | Write to a file instead of stdout |
game import
Import a game registration from TOML.
modde game import <path> [--force]
| Flag | Description |
|---|---|
--force | Overwrite an existing destination TOML |
game import-profile
Import OptiScaler profiles from TOML for a game.
modde game import-profile <path> --for <game-id> [--force]
| Flag | Description |
|---|---|
--for | Game ID the profiles apply to (required) |
--force | Overwrite an existing destination TOML |
modde exec
Manage named launch targets (xEdit, BodySlide, FNIS, etc.). This is a thin
alias for the executable subset of modde tool — the underlying
storage is shared, so modde exec list and modde tool list-executables print
the same rows. Executable management is Done end to end (named executables
with args, working dir, env, Wine DLL overrides, a configurable output mod, and
overwrite capture). See the executables guide.
exec add
Save (or update) a named launch target. Re-running with the same name overwrites
the existing entry, so add doubles as edit.
modde exec add <name> <executable> --game <id> [flags...] [-- <args>...]
| Flag | Default | Description |
|---|---|---|
--game | — | Game ID (required) |
--working-dir | game install | Working directory for the launched process |
--output-mod | __overwrite__ | Mod that captures files written during the run |
--wine-dll-overrides | — | Wine DLL overrides, e.g. dinput8=n,b;winmm=n,b |
--env KEY=VALUE | — | Environment variable assignment (repeatable) |
-- <args>... | — | Default arguments to pass when running this executable |
modde exec add xEdit "~/tools/SSEEdit/SSEEdit.exe" --game skyrim-se \
--wine-dll-overrides "dinput8=n,b" --env "SSE_GAME_PATH=Z:\\game" -- -IKnowWhatImDoing
exec list
List configured launch targets for a game.
modde exec list --game <id>
exec remove
Remove a launch target.
modde exec remove <name> --game <id>
exec run
Run a saved launch target with overwrite capture.
modde exec run <name> --game <id> [--profile <name>] [-- <args>...]
| Flag | Description |
|---|---|
--profile | Profile context for the run |
-- <args>... | Additional arguments appended after the saved defaults |
modde nexus
Nexus Mods account management.
nexus auth
Save your API key to modde’s config (and the system keyring where available).
modde nexus auth
nexus status
Show API key validity and premium status.
modde nexus status
modde nxm
Handle nxm:// download links from Nexus Mods.
nxm handle
Handle an nxm:// download URI.
modde nxm handle <uri> [--profile <name>]
nxm install
Install the nxm:// URI handler for your desktop environment.
modde nxm install
modde save
Manage save files and the git-backed save vault. See the saves guide for the conceptual model.
save assign
Assign a save file to a profile.
modde save assign <path> --profile <name> [--game <id>] [--label <text>]
| Flag | Description |
|---|---|
--profile | Target profile (required) |
--game | Game ID |
--label | Optional label for this save |
save unassign
Remove a save assignment.
modde save unassign <path>
save list
List saves for a profile.
modde save list --profile <name> [--game <id>]
save scan
Scan for unassigned save files.
modde save scan --game <id>
save adopt
Import existing saves from the game directory into a profile’s vault. If no profile is active for the game, the adopted profile becomes active.
modde save adopt --game <id> --profile <name>
save capture
Snapshot current saves into the vault (creates a new snapshot).
modde save capture --game <id> --profile <name> [-m <message>]
| Flag | Description |
|---|---|
-m, --message | Optional message for the snapshot |
save history
Show save snapshot history.
modde save history --game <id> --profile <name> [--limit <n>]
| Flag | Default | Description |
|---|---|---|
--limit | 20 | Max entries to show |
save restore
Restore saves from a specific snapshot.
modde save restore <commit> --game <id> --profile <name>
The <commit> may be a full commit ID or an unambiguous prefix.
save auto-capture
Detect and capture new saves (called by the launch wrapper on game exit; defaults to the active profile).
modde save auto-capture --game <id> [--profile <name>]
save watch
Watch for save changes and auto-capture via polling.
modde save watch --game <id> [--profile <name>] [--interval <secs>]
| Flag | Default | Description |
|---|---|---|
--interval | 30 | Poll interval in seconds |
modde update
Check for, and apply, updates. With no flags, update check checks for a new
modde release; with --mods, --profile, or --game, it checks tracked
profile mods on Nexus instead.
update check
modde update check [--profile <name>] [--game <id>] [--period <period>] [--mods]
| Flag | Default | Description |
|---|---|---|
--profile | — | Check this profile’s tracked Nexus mods |
--game | — | Restrict to this game |
--period | 1w | Time window: 1d, 1w, or 1m |
--mods | — | Check Nexus-tracked profile mods instead of modde itself |
If none of --mods, --profile, or --game is given, this performs the product
(modde) update check.
update apply
Download and install the latest MAIN file for any tracked mod in the profile that has a newer Nexus version.
modde update apply [--profile <name>] [--game <id>] [flags...]
| Flag | Description |
|---|---|
--period <period> | Time window to scan: 1d, 1w, or 1m (default 1w) |
--dry-run | Print the mods that would be updated without downloading |
--confirm-locked | Acknowledge that the profile is locked (Wabbajack / Collection / TOML import); required for any locked profile, since updating drifts it away from its authoritative source |
--accept-breaking | Permit applying updates that look like breaking semver bumps (major version change) |
--yes | Skip interactive prompts (assume “yes”); still refuses breaking updates unless --accept-breaking is also set |
Breaking (major semver) updates always require an interactive y/N confirmation
per mod even with --accept-breaking, unless --yes is also passed.
# Preview, then apply on a locked Wabbajack profile, allowing major bumps
modde update apply --profile lotf --period 1m --dry-run
modde update apply --profile lotf --period 1m --confirm-locked --accept-breaking
modde loot
LOOT masterlist integration for Bethesda plugin sorting.
loot sort
Sort plugins using LOOT masterlist rules.
modde loot sort --game <id> [--data-dir <path>]
| Flag | Description |
|---|---|
--data-dir | Path to the game Data directory (auto-detected if omitted) |
loot validate
Validate plugins for Form 43 and missing master errors.
modde loot validate --game <id>
modde tool
External tool, executable, and overlay management. This command exposes both the
overlay/patch surface (MangoHud, vkBasalt, GameMode, ReShade, OptiScaler, Proton)
and the full named-executable surface (the same storage modde exec aliases).
tool run
Run an external tool by path with overwrite capture.
modde tool run <executable> [--profile <name>] [--game <id>] [-- <args>...]
tool add-executable
Save a named executable launch target for a game. Equivalent to
modde exec add.
modde tool add-executable <name> <executable> --game <id> [flags...] [-- <args>...]
| Flag | Default | Description |
|---|---|---|
--game | — | Game ID (required) |
--working-dir | game install | Working directory |
--output-mod | __overwrite__ | Mod that captures files written during the run |
--wine-dll-overrides | — | Wine DLL overrides, e.g. dinput8=n,b;winmm=n,b |
--env KEY=VALUE | — | Environment variable assignment (repeatable) |
-- <args>... | — | Default arguments |
tool list-executables
List saved executable launch targets for a game.
modde tool list-executables --game <id>
tool remove-executable
Remove a saved executable launch target.
modde tool remove-executable <name> --game <id>
tool run-executable
Run a saved executable launch target with overwrite capture. Equivalent to
modde exec run.
modde tool run-executable <name> --game <id> [--profile <name>] [-- <args>...]
tool list
List detected tools for a game.
modde tool list --game <id>
tool status
Show status of all gaming tools and overlays.
modde tool status --game <id>
tool enable
Enable a tool or overlay.
modde tool enable <tool_id> --game <id>
Tool IDs: mangohud, vkbasalt, gamemode, reshade, optiscaler, proton.
tool disable
Disable a tool or overlay.
modde tool disable <tool_id> --game <id>
tool configure
Configure tool settings as key=value pairs.
modde tool configure <tool_id> --game <id> -- <key=value>...
tool apply
Apply tool patches to the game directory (DLLs, configs).
modde tool apply <tool_id> --game <id>
tool revert
Revert tool patches from the game directory.
modde tool revert <tool_id> --game <id>
tool releases
List releases for a release-backed tool (reshade, optiscaler). Set
GITHUB_TOKEN to raise the GitHub API rate limit.
modde tool releases <tool_id> --game <id>
tool install-release
Install a specific release asset for a release-backed tool, downloading it from GitHub.
modde tool install-release <tool_id> --game <id> --tag <tag> --asset <asset>
| Flag | Description |
|---|---|
--tag | Release tag (required) |
--asset | Asset filename (required) |
tool install-release-from-path
Install a specific release asset from an already-downloaded local file.
modde tool install-release-from-path <tool_id> --game <id> --tag <tag> --asset <asset> <path>
| Flag | Description |
|---|---|
--tag | Release tag the asset corresponds to |
--asset | Asset filename |
<path> | Local path to the downloaded asset |
modde tool releases optiscaler --game cyberpunk2077
modde tool install-release optiscaler --game cyberpunk2077 \
--tag v0.7.7 --asset OptiScaler_v0.7.7.7z
See the tools guide for what each overlay does.
modde fomod
FOMOD installer utilities. See the FOMOD guide.
fomod generate
Generate a declarative FOMOD config template from a mod’s ModuleConfig.xml.
modde fomod generate <mod_path> [--all] [--format <fmt>]
| Flag | Description |
|---|---|
--all | Include all plugins (not just defaults) |
--format | Output format: toml (default), json, or nix |
fomod apply
Apply a declarative FOMOD config non-interactively.
modde fomod apply <mod_path> --config <path> --dest <dir>
fomod inspect
Inspect a mod’s FOMOD steps, groups, and plugins.
modde fomod inspect <mod_path>
modde scan
Scan a game directory for installed mods. See the scanning guide.
modde scan --game <id> [--game-dir <path>] [--manifest <path>] [--import-to <profile>] [--threshold <0.0-1.0>] [--dry-run] [--prune-duplicates]
| Flag | Default | Description |
|---|---|---|
--game-dir | auto-detected | Game installation path |
--manifest | — | .wabbajack file for manifest matching |
--import-to | — | Import discovered mods into this profile |
--threshold | 0.5 | Minimum file presence fraction for a match |
--dry-run | — | Report only, don’t write to database |
--prune-duplicates | — | Remove filesystem-scanner rows covered by the manifest (requires --manifest and --import-to) |
modde collisions
Analyse mod file collisions. See the conflicts guide.
modde collisions [--profile <name>] [--game <id>] [--all] [--suggest-hides]
| Flag | Description |
|---|---|
--all | Show all collisions including cosmetic ones |
--suggest-hides | Suggest hide commands for redundant files |
modde diagnostics
Run diagnostic checks for common modding issues.
modde diagnostics --game <id> [--profile <name>]
modde export
Export a profile’s mod list to CSV.
modde export [--profile <name>] [--game <id>] [--columns <col,col,...>] [--output <path>]
Outputs to stdout if --output is omitted.
modde verify
Verify installed file integrity for a profile.
modde verify [--profile <name>] [--game <id>]
modde backup
Manage mod and plugin order backups.
backup create
Create a backup of an installed mod’s content-store directory.
modde backup create <mod_id>
backup restore
Restore a mod from its latest backup.
modde backup restore <mod_id>
backup list
List available backups for a mod.
modde backup list <mod_id>
backup plugins
Backup the real plugin order for a profile. modde reads the DB-backed order first
and falls back to native plugins.txt when needed.
modde backup plugins --profile <name> --game <id>
backup restore-plugins
Restore plugin load order from a backup, writing back to both the profile DB state
and native plugins.txt when the game uses one.
modde backup restore-plugins --profile <name> --game <id>
modde stock
Manage vanilla game snapshots.
stock snapshot
Capture a snapshot of the vanilla game installation.
modde stock snapshot <game_id>
stock verify
Verify snapshot integrity against the current installation.
modde stock verify <game_id>
modde detect
Detect installed games across Steam and Heroic launchers.
modde detect
modde instance
Manage modde instances (multiple data directories). instance switch changes the
active modde data root used by the database, store, downloads, and backups unless
--data-dir overrides it for a single command.
instance create
Create a new instance.
modde instance create <name> --data-dir <path>
instance list
List all instances.
modde instance list
instance switch
Switch to a different instance.
modde instance switch <name>
modde skill
Install and update the built-in modde-maintained agent skills (written under
~/.agents/skills/). The built-in set is modde-hm-integration,
wabbajack-readiness, and manual-archive-curation.
skill list
List built-in modde skills and their install status (missing, installed,
outdated, or newer).
modde skill list
skill path
Print the target skill directory ($HOME/.agents/skills).
modde skill path
skill install
Install or update one built-in skill, or all.
modde skill install <name|all> [--force]
| Flag | Description |
|---|---|
--force | Replace installed skills even when the installed version is newer |
modde skill install all
modde skill install wabbajack-readiness --force
modde import
Import existing TOML-format profiles into the database.
modde import
modde gui
Launch the graphical user interface (the same binary, with its own iced runtime).
modde gui
Common workflows
First install and play (Nexus collection)
modde nexus auth # store the API key once
modde detect # find installed games
modde install nexus-collection my-collection --profile main
modde play main --game skyrim-se # switch + deploy + launch + capture
Prepare and run a large Wabbajack list
modde wabbajack assess ./lotf.wabbajack --game-dir ~/Games/skyrim
modde wabbajack missing-impact ./lotf.wabbajack --nix-snippet
modde wabbajack import-archive ./lotf.wabbajack ~/Downloads/manual-archive.7z
modde install wabbajack ./lotf.wabbajack --profile lotf \
--game-dir ~/Games/skyrim --continue-on-error --diagnostics-dir ./wj-diag
modde wabbajack analyze-diagnostics ./wj-diag
Experiment safely with the rollback stack
modde profile fork main experiment --game skyrim-se --unlock
modde profile try experiment --game skyrim-se # push onto the stack
modde update apply --profile experiment --period 1m --accept-breaking
modde profile rollback --game skyrim-se # undo if it broke
modde profile commit --game skyrim-se # or keep it
Wire up a modding tool
modde exec add xEdit "~/tools/SSEEdit/SSEEdit.exe" --game skyrim-se \
--wine-dll-overrides "dinput8=n,b"
modde exec run xEdit --game skyrim-se --profile main # output captured to __overwrite__
modde collisions --profile main --game skyrim-se --suggest-hides
Back up before and capture after a session
modde backup plugins --profile main --game skyrim-se
modde save capture --game skyrim-se --profile main -m "before the dungeon"
modde play main --game skyrim-se # auto-captures saves on exit
modde save history --game skyrim-se --profile main
See also
- Installation — what install channel is live today
- Home-Manager module reference — the declarative surface
- Parity audit — what is
DonevsPartial - Supported games — the 15 built-in games
- Generic games guide — user-defined game support
- Wabbajack guide — readiness and install workflow
- Executables guide — named launch targets
- Saves guide — the git-backed save vault
Architecture
This is the system-design reference for modde — the durable “why” and “how” behind the pieces that make up the application. It is meant for contributors and for anyone who wants to understand a subsystem before changing it. Where a behaviour is settled and load-bearing, the rationale lives here so it does not have to be re-derived from the source each time.
Implementation history lives in git log; this document records the design that the
code currently encodes. When the two disagree, the source is the truth — please file
a fix.
Crate map
modde is a Cargo workspace. Five crates carry the weight, layered so that the lower crates know nothing about the higher ones.
modde-core is the engine. It owns the persistent data model (the SQLite database
and the XDG path layout), the load-order resolver, the collision engine, the VFS
symlink farm, the installer pipeline, the save vault, the Wabbajack manifest model and
its native install/extract path, hashing, and the content-addressed mod store.
modde-core deliberately contains no game-specific knowledge — every place that
would otherwise hard-code a game’s quirks instead takes a trait object or a callback so
the game layer can supply the behaviour. This is what keeps the core testable in
isolation and lets new games land without touching it.
modde-games is the game knowledge layer. It defines the GamePlugin,
ModScanner, and SaveTracker traits, the central GAME_REGISTRY, launcher detection
(Steam and Heroic), and one module per supported engine family (Bethesda, Gamebryo,
Cyberpunk RED, Larian, UE4/UE5, Witcher, Bannerlord, SMAPI) plus the generic
user-defined game loader. It depends on modde-core and supplies the trait
implementations the core calls back into. Fifteen built-in games ship in the registry.
modde-sources is the acquisition layer. It implements download backends (direct
HTTP with resume, Google Drive, MediaFire, Nexus, Wabbajack-authored CDN chunks), the
native archive decompressors (zip, 7z, BSA, BA2, optional RAR), the Wabbajack installer
and its per-archive batching, the inline-zip index, the byte LRU cache, and the
streaming verify path. It depends on modde-core for the manifest model, hashing, and
the link/store helpers.
modde-cli is the command surface (modde). Each subcommand is a thin handler that
wires user input to a core/games/sources call: install, deploy, profile, scan,
exec, tool, save, wabbajack, loot, game, and friends. It owns argument
parsing and human-readable reporting but very little logic of its own — the goal is that
anything worth testing lives below the CLI.
modde-ui is the egui desktop application (modde-ui, also reachable as
modde gui). It renders the mod list, load order, conflict views, the Nexus browse
panel, the Executables view, and the install wizard against the same core APIs the CLI
uses. UI and CLI are two front-ends over one engine; shared flows (deploy, install) are
factored into the core rather than duplicated.
┌────────────┐ ┌────────────┐
│ modde-cli │ │ modde-ui │ front-ends
└─────┬──────┘ └─────┬──────┘
│ │
├───────────────────┤
▼ ▼
┌──────────────┐ ┌──────────────┐
│ modde-games │ │ modde-sources│ knowledge + acquisition
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬─────────┘
▼
┌────────────┐
│ modde-core │ engine + data model
└────────────┘
Data model
The SQLite database
modde’s persistent state is a single SQLite database at <modde_data>/modde.db, opened
in WAL mode with foreign keys enforced. Schema evolution is handled by an idempotent
migration ladder keyed off SQLite’s user_version pragma — each step runs only if the
stored version is below it, and additive column changes go through an
add_column_if_missing guard so re-running a migration is always safe. The current
schema version is 10.
The key tables:
| Table | Holds |
|---|---|
profiles | One row per profile: name, game_id, source type/data (manual, Nexus collection, Wabbajack…), the overrides directory, and the profile-level load-order lock. UNIQUE(name, game_id). |
profile_mods | The mod list for a profile, ordered by sort_index. Carries enabled state, version, display name, FOMOD config, Nexus IDs, category/notes/tags, the per-mod load-order lock reason, and the installer-pipeline columns (install_method, source_archive_hash, install_status). |
load_order_rules | LoadAfter / LoadBefore / Incompatible constraints between mods, consumed by the resolver. |
installed_mod_files | The exact file manifest for every installed mod: rel_path, origin_rel_path, size, and a reserved merge_group. This is what makes uninstall surgical — modde removes exactly the files it staged. |
saves | Save files/directories assigned to a profile (the DB-level assignment layer, distinct from the git save vault). |
stock_snapshots | Per-game vanilla-game snapshot metadata: snapshot path, tree hash, and file count, used by Stock-Game / clean-baseline flows. |
active_profiles | The currently active profile per game (game_id primary key). |
experiment_stack | A per-game stack of profiles for the “try a change, then pop back” experiment workflow. |
hidden_files | Per-profile, per-mod file hides (the MO2 .mohidden equivalent) — excluded from the symlink farm at build time. |
plugin_order | Plugin (.esp/.esm/.esl) ordering and enable state, kept independent of mod install priority. |
mod_categories | Collapsible category separators with colour and sort index. |
game_tools | Current per-game tool/overlay state (MangoHud, vkBasalt, GameMode, OptiScaler, ReShade, Proton…): enabled flag plus a free-form JSON settings blob. |
tool_applied_files | Files a tool wrote into a game directory, tracked so tool changes can be reverted. |
executable_configs | MO2-style named launch targets: path, args, working dir, environment, Wine DLL overrides, and the configurable output (overwrite) mod. |
tool_setting_nodes / tool_setting_edges | A DAG of tool-setting history (schema V10): game_tools stays the current state, and every mutation also appends a node plus an edge from the previous current node, so restores can branch. |
Two design choices are worth calling out because they recur:
- TOML/JSON-encoded columns. Structured values that the database does not need to
query on —
InstallMethod, the load-order lock, FOMOD config, tool settings — are serialized into text columns. The type-safety contract is enforced at the Rust boundary on read/write, not by SQL. This keeps the schema stable as those structures evolve. - Install + manifest atomicity.
record_installwrites the method, archive hash, and status onto theprofile_modsrow and replaces theinstalled_mod_filesmanifest in one transaction, wiping any prior manifest first so retries never leave orphaned rows.remove_installed_modis the symmetric operation: it returns the staged files and deletes both the manifest and the mod row together.
XDG path layout
modde-core::paths is the single source of truth for where things live. It is
platform-aware (honouring XDG_DATA_HOME / XDG_CONFIG_HOME / XDG_CACHE_HOME on
Linux, with the equivalent dirs fallbacks on macOS and Windows) and supports a data-dir
override — either via --data-dir / MODDE_DATA_DIR, or via an active instance recorded
in the instance registry — so portable and multi-instance setups work without touching
the rest of the code.
<data_dir>/modde/ e.g. ~/.local/share/modde/
├── modde.db SQLite (see above)
├── store/ content-addressed mod file store
├── staging/ scratch space for installs
├── profiles/<name>/staging/ per-profile symlink farm
├── downloads/ downloaded archives (durable across restarts)
├── stock/ vanilla-game snapshots
├── saves/<game_id>/ per-game git save vault
└── wabbajack_cache/<hash>.wabbajack content-addressed .wabbajack manifests
<config_dir>/modde/ settings (app settings, game-path overrides)
<cache_dir>/modde/ non-essential cache
The store, downloads, and save vaults are all durable across restarts; staging is disposable. This separation is what lets an interrupted install resume without re-downloading and lets a botched deploy roll back without losing anything.
The GamePlugin trait and registry
A supported game is described by a GameRegistration in the static GAME_REGISTRY. The
registration is pure data — it binds a game_id to a display name, an EngineFamily,
launcher IDs (Steam app/dir, Heroic GOG/Epic IDs), Wabbajack manifest names, Nexus
domain and numeric game ID, a supports_save_profiles flag, and the trait objects that
implement behaviour:
#![allow(unused)]
fn main() {
pub struct GameRegistration {
pub game_id: &'static str,
pub display_name: &'static str,
pub engine: EngineFamily,
pub launcher: LauncherIds,
pub wabbajack_names: &'static [&'static str],
pub nexus_domain: Option<&'static str>,
pub nexus_game_id: Option<u32>,
pub supports_save_profiles: bool,
pub plugin: &'static dyn GamePlugin,
pub scanner: Option<&'static dyn ModScanner>,
pub save_tracker: Option<&'static dyn SaveTracker>,
pub collision_classifier: Option<CollisionClassifierFactory>,
pub optiscaler_profiles: &'static [OptiScalerProfile],
}
}
The GamePlugin trait is the behavioural contract. It is deliberately wide but almost
entirely default-implemented, so a new game overrides only what is genuinely
game-specific. The salient methods:
- Install layout.
mod_directory/mod_root(where mods deploy),executable_dir(where proxy DLLs live),deploy/deploy_to_install/post_deploy. - Install-method detection.
analyze_mod_archiveruns before the generic installer probes so a game can authoritatively claim a layout it recognises (Cyberpunk’s REDmod viainfo.json, for example);recognizes_bare_layoutis the last-resort fallback before the analyzer emitsUnknown. - Classification.
classify_extension/summarize_contentbucket files intoContentCategory;classify_moddecides whether a mod is save-breaking (feeding the save fingerprint). - Alternate deploy targets.
deploy_targetsadvertises named roots outside the game install dir (per-user config, saves), andresolve_deploy_targetresolves an ID to a real path at deploy time — deferred so plugins can fold in runtime context like a Wine prefix or Steamcompatdata. - Wine integration.
wine_dll_overrides/wine_dll_overrides_from_stagingreport proxy/hook DLL base names that needWINEDLLOVERRIDES=name=n,b. - Metadata accessors.
nexus_game_domain,nexus_game_id_u32,steam_app_id_u32,archive_extensions,has_plugin_system,plugins_txt_folder, and so on, which let the core and CLI stay game-agnostic.
The registry is wrapped in an OnceLock<RwLock<&'static [GameRegistration]>>. The first
access builds a snapshot from the static built-ins chained with user-defined games
loaded from disk; reload_registry rebuilds it so newly added user games appear without
a restart. resolve_game, all_games, resolve_game_by_nexus_domain, and
launcher_games are the lookups everything else uses.
Adding a built-in game
A new built-in game is a new GameRegistration appended to GAME_REGISTRY plus the
trait implementations it points at. In practice:
- Implement (or reuse) a
GamePluginfor the engine family. Most games reuse a shared data-driven plugin (the Bethesda and UE4 modules are parameterized) rather than a bespoke type. - Optionally implement a
ModScanner(filesystem discovery of already-installed mods) and aSaveTracker(save detection + capture description). Both areOptionin the registration — a game can ship without them. - Pick a
collision_classifierfactory (or one of the shared policy classifiers keyed by archive extension). - Add the
GameRegistrationwith launcher IDs and Nexus/Wabbajack identifiers.
The per-game documentation lives under Supported games.
User-defined games
Generic / user-defined game support is Partial. A user can describe a game with a
GameSpec TOML and register it via modde game add (and import/export the spec); the
generic loader turns that spec into a GameRegistration that the snapshot picks up
through the same load_user_games chain. Deployment, conflict analysis, and launcher
wiring work for these games, but there is no bespoke scanner or save tracker behind a
generic spec, and the UX is thinner than for a built-in. See
Generic & user-defined games for the spec format and its
limits.
Detection
Detection answers “where is this game installed?” without asking the user. It scans the two launchers modde understands and caches the result.
Steam. modde reads steamapps/libraryfolders.vdf to enumerate every library root
(Steam supports libraries spread across drives), and the platform-specific default
library is always included. Within each library’s steamapps/ directory it parses every
appmanifest_*.acf for its appid / name / installdir, matches the app ID against
the registry’s LauncherIds, and resolves the install to
steamapps/common/<installdir>. A common/<steam_dir> directory-name fallback catches
installs whose manifest is missing or unreadable. The VDF and ACF parsers are
intentionally lightweight line scanners rather than full KeyValues parsers — they extract
only the quoted values they need and ignore everything else.
Heroic. modde reads Heroic’s config directory and three store databases:
gog_store/installed.json, legendary_store/installed.json (Epic), and
sideload_apps/installed.json. GOG and Epic entries are matched by their store app ID
against the registry; sideloaded apps are matched heuristically by install-directory name
against each game’s known steam_dir. Each match records its LauncherSource so the
front-ends can both display provenance and launch the game (Steam via a
steam://rungameid/<id> URI, Heroic via heroic --no-gui --launch <id>).
Results flow through a process-wide detection cache so the UI can resolve many games
without rescanning every launcher per game. find_game_install consults a settings
override first, then the cache, before falling back to a fresh scan.
VFS symlink farm
Deployment is a symlink farm: rather than copying mod files into the game directory, modde builds a staging tree of symlinks pointing into the content-addressed store, then deploys that tree into the game’s mod directory as a second layer of symlinks. Nothing is copied; toggling a mod or reordering load order is cheap.
The farm uses a typestate to make misuse a compile error. SymlinkFarm<S> carries a
zero-sized PhantomData<S> marker that is Built after construction and Materialized
after it is written to disk. build() returns a Built farm; materialize() consumes
it and returns a Materialized farm; only a Materialized farm exposes deploy_to().
You cannot deploy a farm you have not materialized, and the compiler enforces it.
build() ──► SymlinkFarm<Built>
│ materialize() (writes staging symlinks to disk)
▼
SymlinkFarm<Materialized>
│ deploy_to(game_mod_dir)
▼
game directory populated with symlinks
Build is priority-aware. build() walks the resolved load order in order, inserting
each mod’s (rel_path → store_path) links into a map. Because later mods overwrite
earlier entries for the same relative path, the last mod in load order wins a file
conflict — the same rule the collision engine reports on. Profile-level overrides are
layered last and win over all mods, and hidden (mod_id, rel_path) pairs are skipped
entirely.
Store link strategy. Links point at the content-addressed store (and, for overrides, at the override directory). Materialization cleans any existing staging directory, recreates the parent directory tree for each link, and creates the symlinks; deployment recreates the target tree and replaces any pre-existing file at each destination before linking. The store is the single source of truth for bytes; the farm is a cheap, disposable view of it.
Atomic rollback. Before a risky redeploy, the previous staging tree is kept as
staging.bak. rollback() performs a rename-based swap: the current staging is moved
aside to staging.old, staging.bak is renamed into place as staging, and the old
tree is removed. Renames within a directory are atomic on POSIX filesystems, so a profile
is never left with a half-built staging tree.
Load-order resolver
The resolver turns a profile’s mod list plus its LoadOrderRules into a single
deterministic order. It is a stable Kahn topological sort with input-position
tiebreaking.
The mod and game identifiers it works with — ModId and GameId — are
#[repr(transparent)] newtypes over String, generated by a macro. They are zero-cost
at runtime but make it a type error to pass a mod ID where a game ID is expected.
Rule types. Three rules constrain the order:
LoadAfter { mod_id, after }—mod_idmust come afterafter.LoadBefore { mod_id, before }—mod_idmust come beforebefore.Incompatible { mod_a, mod_b }— error if both are enabled.
Incompatible rules are checked up front and short-circuit resolution with a conflict
error. LoadAfter / LoadBefore become directed edges; edges that reference a disabled
or unknown mod are silently dropped.
Algorithm. Enabled mods are collected in input order, each recording its input position. Adjacency and in-degree are built from the rules. A min-heap keyed on input position seeds every zero-in-degree node; the resolver repeatedly pops the lowest-input-position ready node, emits it, and decrements its successors, pushing any that reach zero in-degree.
enabled mods (input order) ──► in-degree + adjacency from rules
│
▼
min-heap of ready nodes (keyed by input position)
│ pop lowest, emit, relax successors
▼
resolved load order
(fewer emitted than enabled ⇒ cycle ⇒ DependencyCycle error)
Determinism and stability guarantees. The input-position tiebreak gives four properties the UI relies on:
- No rules → exact input order. With no rules, the resolved order is exactly the enabled subset of the mod list, unchanged.
- Reorder round-trips. Swapping two adjacent mods in the list and re-resolving produces the swapped order — so a drag-reorder in the UI is actually visible (a plain topological sort could return any valid order and silently discard the move).
- Minimal change under rules. When a rule forces movement, only the rule-involved mods shift; unrelated neighbours stay put.
- Deterministic. Identical inputs always produce identical outputs; no
HashMapiteration order leaks into the result.
Cycle detection. If fewer nodes are emitted than were enabled, some node never
reached zero in-degree — a cycle. The resolver names one surviving node in a
DependencyCycle error rather than silently truncating the order.
Collision engine
The collision engine reports which mods provide the same file, who wins, and how risky
the overlap is. It builds on the resolver’s ConflictMap (a file_path → set of provider mods index whose winner_for applies the same last-in-load-order-wins rule as
the symlink farm).
Graph of file providers. build_full_conflict_map walks each mod’s store directory
in resolved order, registering every loose file. It is archive-aware: for files whose
extension marks them as an archive (per the game’s CollisionClassifier), it indexes the
archive’s contents and registers those inner paths too, tracking each entry’s
FileOrigin — Loose or Archive { archive_rel }. This is what lets modde detect that
a loose texture shadows the same texture packed inside another mod’s BSA, which a
loose-file-only diff would miss. Mods whose store directory is missing are collected and
reported rather than silently skipped.
Severity classification. Each colliding file is classified by the game’s classifier
into Cosmetic (textures, meshes, sounds — low risk), Config (INI/config — may change
behaviour), Dangerous (scripts, plugins, DLLs — potential crashes or save corruption),
or Unknown. Severity is an ordered enum, so a mod-pair’s worst collision is just the
max.
The report. analyze_collisions produces a CollisionReport that groups collisions
by (loser, winner) mod pair (sorted most-severe-first, then by file count), flags
loose_vs_archive overrides, lists redundant_files (files a mod provides but always
loses), and identifies fully shadowed_mods (every file they provide is overridden by a
higher-priority mod). Hidden files are honoured in winner selection. The classifier is a
trait so the severity tables and archive formats stay in the game layer; the analysis is
game-agnostic.
Save vault
modde gives every game a git-backed save vault at <modde_data>/saves/<game_id>/.
Profiles map to branches, so branching, history, and stacking come for free from git.
The core SaveManager drives the vault; the game’s SaveTracker tells it what to look
for. Saves are committed via git2, and capture skips committing when the tree is
identical to HEAD so no-op captures do not litter history.
The activate flow captures the current profile’s saves (committing to its branch), parks
them in a live .modde/profiles/<name>/ directory so Steam Cloud sees a move rather than
a delete, then checks out and deploys the new profile’s branch. Steam Cloud markers and
modde’s own live-state metadata are preserved across capture/deploy/restore so the
launcher’s cloud sync is never confused.
Fingerprinting. A SaveFingerprint is a SHA-256 over the sorted, de-duplicated list
of enabled, save-breaking mod IDs. “Save-breaking” is decided by the game plugin’s
classify_mod (passed in as a callback so the core stays game-agnostic). The fingerprint
is written into the capture commit message as Mod-Fingerprint: /
Save-Breaking-Mods: trailers. Before restoring an older snapshot, modde re-derives the
current fingerprint and compares: a match is Compatible, a missing trailer is
NoFingerprint, and a difference is a Mismatch listing exactly which save-breaking
mods were added or removed — so the user is warned before loading a save whose mod set no
longer matches. Cosmetic mods do not affect the fingerprint, so freely toggling textures
never triggers a false warning.
Installer pipeline
Installing a mod is a three-stage pipeline: analyze → execute → record.
- Analyze inspects an extracted archive and produces an
InstallPlandescribing how to lay its files out (theInstallMethod), optionally stripping a wrapper directory (strip_prefix), and carrying the source archive’s xxh64 hash. - Execute moves files from the staging directory into the mod’s store directory per
the plan, producing the concrete
StagedFilemanifest. - Record persists the method, archive hash, status, and file manifest into the database (atomically, as described in the data-model section) so uninstall is precise.
InstallMethod variants. The enum is the extensibility point. It is serde-tagged
and ordered by detection specificity (game-specific layouts win over generic ones):
| Variant | Meaning |
|---|---|
BareExtract | Contents map 1:1 into the game’s mod dir. |
StripContentRoot { root } | A content root (e.g. Data/) is stripped before staging. |
DirectoryMod { directory_name } | The archive root is one directory-style mod. |
DirectoryModFromXml { marker, id_attr, fallback_name } | Directory mod whose stable name is read from an XML marker (e.g. Bannerlord SubModule.xml). |
MultiRootOverlay { roots } | Several game-root overlay directories (mods/, dlc/, bin/…). |
SingleFileSet | One or more loose files that stage straight into the mod root. |
Fomod { module_config, config_toml } | FOMOD installer; config_toml is the recorded declarative selection (None ⇒ the wizard still has to run). |
REDmod { manifest } | Cyberpunk REDmod package keyed by info.json. |
Bain { selected_subdirs } | BAIN (Wrye Bash) numbered option subdirs; empty ⇒ wizard pending. |
DllOverlay { target_dir_hint } | Proxy DLL / overlay (dxvk, ENB) into the executable dir. |
UserConfigOverlay { target_id } | Files routed to a plugin-advertised alternate root (UE config, Bethesda My Games INIs) at deploy time. |
ScriptMerge { merge_group, base } | Reserved: stages per base and tags the merge group; actual merging is future work. |
Unknown { reason } | Detection failed; a dossier is written. |
InstallMethod::is_ready() reports whether execution can proceed without user input —
FOMOD needs its config, BAIN needs its selection, Unknown never proceeds. The matching
InstallStatus (Installed, PendingUserInput, Failed, Unknown) is persisted so
the UI shows the right action (install / retry / resume).
The dossier extension mechanism. When detection cannot classify a layout, the
analyzer emits Unknown { reason } and the caller writes a dossier — a dump of the
archive’s shape and hash. The dossier is the seam by which the installer is taught a new
layout: a maintainer (or a Claude Code skill) extends the InstallMethod enum with a new
variant and the detection rule that recognises it, rather than patching ad-hoc special
cases into the analyzer. modde install dossier surfaces the path for an undetected mod.
Designing for slow-and-steady large installs
The Wabbajack apply path (in modde-sources) is the stress case: a list like Twisted
Skyrim has on the order of 6,000 archives and 680,000 install directives. The pipeline is
built around four root causes that an order-naive implementation hits, and the durable
design decisions that correct them:
- Per-archive batching. Directives are grouped by source archive and applied with
bounded concurrency over archives, not directives. Each archive’s decoder is opened
once and driven forward through its entries in order, which eliminates the
solid-7z re-decompression amplification (extracting N files from a solid archive must
not cost N full decompressions).
CreateBSAruns as a separate pass after theFromArchivework its inputs depend on. - Native, in-process decompression. zip, 7z, BSA, and BA2 are decoded by Rust crates
in-process (RAR is an optional
rarfeature; without it RAR is reported as unsupported rather than shelling out). No7zz/unrarsubprocess remains in the apply path, so there is no per-fileexecve, no re-mmap, and no subprocess stdout to buffer. - Streaming verification. Downloads are hashed as they are written (the
copy_and_hash_compatadapter computes the Wabbajack-compatible xxHash64/XXH3 while copying), instead of re-reading hundreds of gigabytes in a separate verify pass after download. Cached files keep the cheap existing-cache check. - Resumable apply. Each completed archive batch (and each
CreateBSA) writes a JSON sentinel under<staging>/_state/. A sentinel is honoured only when the pipeline version, archive hash, archive size, directive indices, and expected output files all match the current manifest; a crash mid-apply resumes at the next pending batch rather than at directive zero. Failed or interrupted batches leave no sentinel and rerun whole.
Supporting pieces — a shared inline-zip index (the .wabbajack is opened and indexed
once), a bounded byte-LRU cache for patch source bytes, and hardlink-then-reflink-then-copy
linking for Stock Game and deploy — round out the memory and disk-bounded behaviour. See
the Wabbajack guide for the operator-facing view.
Mod scanner subsystem
The scanner answers the inverse of installation: given a game directory (and optionally a Wabbajack manifest), what mods are already installed? This is what lets a profile with files on disk but no database rows be reconstructed.
The ModScanner trait. Each game implements ModScanner: scan_directories lists
the install-relative directories to inspect, and scan_filesystem returns
DiscoveredMods (each with a mod_id, display name, optional version, file list,
ModSource, and a confidence score). A third method, mod_id_footprint, is the inverse
of the scanner’s ID scheme — given an ID the scanner would produce, it returns the
filesystem footprint (directory subtree or single file) that mod owns. That inverse is
what lets the dedup pass correlate filesystem-scanner rows against a manifest.
Three pattern rules. Game scanners recognise three structural patterns when grouping files into mods:
- One subdirectory = one mod. Used where a mod loader reads a directory of named mod
folders (Cyberpunk CET / REDscript / TweakXL mods under their respective roots,
REDmods under
mods/whoseinfo.jsonsupplies name and version). - One file = one mod. Used for loose-file mod loaders (e.g. each
.archiveunderarchive/pc/mod/). - Plugin + companion archives = one mod. Used for plugin-based games, where an
.esp/.esm/.esland its sidecar.bsa/.ba2are grouped as a single mod.
Wabbajack manifest matching. match_wabbajack_manifest (in modde-core::scanner) is
the game-agnostic correlator. It groups a manifest’s FromArchive /
PatchedFromArchive directives by source archive hash, normalises the to paths
(backslash → forward slash, lowercased, MO2 mods/<name>/ prefix stripped), and for each
archive computes the fraction of its to paths that actually exist on disk. Archives
whose present fraction meets a threshold become ManifestMatches, carrying the archive’s
Nexus identity when the directive’s ArchiveState is a NexusDownloader. The crucial
invariant is that archive_mod_id derives the same canonical mod_id
(nexus_<domain>_<mod>_<file> or wj_<hash>) for both the Wabbajack installer and the
scanner, so installing a list and later re-scanning it dedup against each other instead of
producing duplicate rows.
Two more pure helpers build on this. apply_wabbajack_lock reorders a profile to follow
the manifest’s install-directive order (matched mods first, unmatched preserved after) and
stamps a Wabbajack lock — the engine behind retroactive locking. detect_stale_duplicates
classifies a profile’s filesystem-scanner rows into “leaked duplicates” (footprint covered
by the manifest, so a nexus_* row already deploys those files) and “genuine additions”
(the user’s own additions on top of the list), which powers modde scan --prune-duplicates. See the scanning guide for usage.
Release, packaging, and tooling decisions
These are the enduring choices about how modde is built, released, and distributed. They are recorded here so a contributor knows why something is the way it is before changing it.
Releases go through simit. cargo xtask release {patch|minor|major|prerelease}
delegates to simit release from the devShell. simit standardizes the semver bump,
CHANGELOG promotion, commit, and tag across the maintainer’s Rust projects, which prevents
per-project drift. Tags are bare semver with no v prefix (0.2.0, 1.0.0-rc.1); the
release workflow triggers on [0-9]*. The literal ## [Unreleased] heading in
CHANGELOG.md must stay exactly as-is because simit’s promotion logic keys off it. Major
versions are cut whenever they make sense per SemVer — there is no 1.0 milestone and no
“save up breaking changes” policy.
xtask is a thin per-project binary over a shared library. Reusable build-tooling logic
lives in harbor-xtask (in rs-harbor); modde ships a thin modde-xtask binary, invoked as
cargo xtask, that wires the project-specific bindings — the RPM spec path (modde.spec),
the COPR vendor tarball, the Zola roots (docs/site and website), the Nix package names
(modde / site / modde-windows / appimage-* / flatpak-manifest), and
cargo xtask gui. The shared rs-harbor CLI deliberately does not grow top-level
release / copr / docs subcommands, because that would couple it to downstream project
layouts. The RPM spec Version: rewrite happens in CI against the tagged tree, not
committed back to trunk.
CI and the flake stay bespoke. modde does not run simit init-ci --check or
simit init-flake --check. The Forgejo workflows and flake.nix are hand-maintained
because they model things the generic generators do not: the .#flatpak-manifest,
.#appimage-*, .#modde-windows, and .#docs builds; the Attic closure push; the
coverage gate (cargo xtask coverage --ci); cross-compilation (Windows, aarch64-linux,
macOS x86_64/aarch64 via osxcross); the Zola website / docs outputs; and the
self-hosted atlas runner. Running --check would flag all of that intentional
customization as drift. This is revisited only if simit becomes configurable enough to
express those jobs, or rs-harbor publishes a helper that preserves them.
Home-Manager tools contract. The programs.modde.profiles.<name>.tools option is
attrsOf toolSubmodule with enable, free-form settings, a reserved release, and
applyOnActivation. Only gamemode, vkbasalt, and reshade carry strict per-setting
typing; mangohud, optiscaler, and proton stay attrsOf anything, because
ToolConfig.settings is stored as a JSON blob in SQLite — the type-safety contract is at
Nix evaluation time, not at storage, and strict typing for sprawling settings (114
MangoHud knobs, 33 Proton knobs, OptiScaler’s per-game fields) buys little. Activation runs
after modde install / modde deploy: each enabled tool gets an idempotent
modde tool enable, then configure when settings are non-empty, then apply only when
applyOnActivation = true; disabled tools call disable. Every modde tool non-zero exit
warns and continues so a single tool error never fails activation. HM-managed tool
releases are eager Nix fetches with pinned hashes — activation must never call networked
modde tool install-release. See the Home-Manager module reference.
Flatpak app ID is com.tartanoglu.modde. The reverse-DNS of tartanoglu.com, a domain
owned by the project author. Flathub reserves provider-owned prefixes, and a
maintainer-owned domain keeps the application identity portable across forges (a
Codeberg-namespace ID would tie identity to the forge host). It is encoded in
dist/com.tartanoglu.modde.metainfo.xml, dist/modde-ui.desktop, and the Flatpak manifest
output in flake.nix.
Distribution channels. A single release fans out from the
.forgejo/workflows/release.yml run to every supported platform and channel:
direct Codeberg release archives for Linux (x86_64, aarch64), macOS (x86_64,
aarch64), and Windows (x86_64); Linux packages via the AUR, Fedora COPR, a signed
apt repository, Flatpak, and AppImage; macOS via a Homebrew tap; Windows via winget,
Scoop, and Chocolatey; the CLI crate to crates.io; and the Nix flake plus
home-manager module. The authoritative per-channel commands live in the
installation guide.
See also
MO2 parity & capability audit
modde does not try to be a clone of Mod Organizer 2. It targets a different platform story — Linux-first, declarative, reproducible — and ships some things MO2 has never had (git-backed save vaults, declarative FOMOD, native Wabbajack on Linux). This page is the honest accounting of how the two overlap.
It is intentionally conservative:
Donemeans the feature is shipped end to end and reachable by users.Partialmeans core logic exists, but the UX, integration, or production safety is still incomplete.Not shippedmeans the repository should not market it as available yet.
The canonical status baseline lives in docs/capability-matrix.toml.
The guardrail test crates/modde-core/tests/repo_truth_tests.rs fails the build if this page, the README, the
supported games table, and the landing-site comparison drift from that file.
Feature baseline
| Capability | Status | Notes |
|---|---|---|
| Core VFS deployment | Done | Symlink-farm deployment, per-file hiding, atomic rollback, and conflict resolution are shipped. See Deployment & VFS. |
| Profile switching & save vaults | Done | Profile activation, the experiment stack, git-backed save history, and save fingerprints ship for games with real save trackers. |
| Bethesda plugin management | Done | Plugin-order backup/restore, plugins.txt IO, LOOT parsing, Form 43 detection, and missing-master checks are shipped. |
| Nexus install pipeline | Done | API-key auth, browse/search (REST + GraphQL), update checks, nxm://, single-mod installs, and Collections are shipped. |
| FOMOD | Done | Interactive wizard plus declarative TOML/JSON/Nix config generation and non-interactive application are shipped. |
| Executable management | Done | Named executables with arguments, working directory, environment, Wine DLL overrides, and a configurable output (overwrite) mod, plus overwrite capture on run — in both the CLI (modde exec / modde tool add-executable) and the UI. See Executables & external tools. |
| Starfield save tracking | Done | Starfield .sfs files flow through the shared save-tracker path. |
| Instance switching | Partial | CLI/runtime instance selection changes the active data root, but there is no MO2-style portable/global UX yet. |
| Diagnostics | Partial | The CLI and UI share a real diagnostics engine fed by resolved conflicts and plugin order, but there is no MO2-style “problems” button with guided fixes. |
| Data tab | Partial | The UI renders real conflict rows from the resolver/collision engine, but it is not yet a full merged-VFS browser with archive/hidden filters. |
| Downloads UI | Partial | The Downloads view is wired to real queue state, but pause/resume is UI-side state rather than a durable transport-level pipeline. |
| Tool management | Partial | The Tools view loads DB-backed tool configs and can apply/revert tracked file patches for MangoHud, vkBasalt, GameMode, ReShade, OptiScaler, and Proton, but the catalog is narrower than MO2’s plugin ecosystem. |
| Mod information dialog | Partial | A right-rail panel shows Nexus metadata, the image gallery, and endorse/track toggles, but there is no MO2-style file-tree / INI editor / conflict-tab dialog. |
| Non-Nexus download backends | Partial | GitHub, Direct, Google Drive, MEGA, and MediaFire backends exist and are exercised by Wabbajack/directive workflows, but they are not yet first-class install mod UX. |
| BAIN | Partial | Detection and execution scaffolding exist, but the interactive sub-package selection flow is unfinished. |
| Generic game support | Partial | User-defined games ship through modde game add / import-export of a GameSpec TOML and a generic loader, but without a polished UI or per-game scanner/save-tracker. See Generic & user-defined games. |
Supported games
Depth varies by game. Done titles are the strongest end-to-end paths; Partial titles deploy and track saves but have not been hardened to the same degree.
| Game | Status | Notes |
|---|---|---|
| Skyrim Special Edition / Anniversary Edition | Done | Plugins, LOOT, diagnostics, VFS, saves, and Wabbajack/Nexus workflows are the strongest path today. |
| Fallout 4 | Done | Plugins, diagnostics, VFS, and save tracking are shipped. |
| Cyberpunk 2077 | Done | REDmod / CET / TweakXL-aware install and launch flows are shipped. |
| Fallout 76 | Partial | VFS, plugin handling, and BA2 scanning exist; saves are effectively server-side and only lightly represented locally. |
| Starfield | Partial | Game plugin, plugin handling, VFS, diagnostics, and .sfs save tracking exist. |
| Stellar Blade | Partial | UE4/UE5-style deployment, scanning, conflicts, OptiScaler integration, and local save tracking are present, below the Bethesda/Cyberpunk depth. |
| Baldur’s Gate 3, Stardew Valley, Fallout: New Vegas, Oblivion, Oblivion Remastered, Bannerlord, The Witcher 3, Subnautica 2 | Partial | Engine-appropriate deployment, scanning, conflict classification, and save tracking ship; depth and game-specific polish are still maturing. |
See Supported games for the full per-game table, IDs, and per-game guides.
Where MO2 is still ahead
These are the gaps that matter most for a “drop-in MO2/Vortex replacement” claim:
- Mod information dialog — file tree, text/INI editing, image preview, conflict tabs, and optional per-mod plugin management.
- Durable download pipeline — transport-level pause/resume/cancel, ETA/speed, concurrency, update-all, and resumable metadata sidecars (modde has resumable sidecars for Wabbajack/direct downloads, but not yet a unified interactive queue).
- Full merged-VFS browser — archive visibility, overwrite actions, hidden-file filters, and “go to the conflicting mod” navigation in the Data tab.
- Breadth of tool/plugin ecosystem — MO2’s plugin surface (preview, checker, and integration plugins) is much wider than modde’s curated overlay set.
Where modde is ahead
Features with no MO2 equivalent: git-backed save vaults with full history and branching, SHA-256 save fingerprinting with pre-restore compatibility warnings, the non-destructive experiment stack, declarative FOMOD configs, native Wabbajack installation on Linux, Nexus Collections, declarative home-manager profiles, profile forking that clones save history, stock-game tree-hash verification, and unified Steam + Heroic (GOG/Epic/sideload) detection. See the landing-site comparison for the side-by-side.
Glossary
This page defines the vocabulary the rest of the documentation assumes — both modde’s own concepts and the wider modding terms (FOMOD, LOOT, Wabbajack, REDmod, …) you meet when you actually install mods. Each entry is one to three sentences with a link to the guide that covers it in depth. Terms are grouped by theme; within a group they read roughly in the order you encounter them.
modde core concepts
Profile
A profile is modde’s central organizing unit: a per-game bundle of an ordered mod
set, a load order (and plugin order), a branch in the save vault, and the
deployment state that records exactly what was staged. Profiles are scoped by
(name, game_id), so a default profile for Skyrim and a default for Fallout 4
coexist without collision. See Profile management.
Instance
An instance is a named, self-contained data directory. Instances let you run
entirely separate modde states — a daily driver and a Wabbajack testbed, say —
without sharing a database, store, or save vaults; the active instance is recorded
in ~/.config/modde/instances.toml. See
Data, instances & backups.
Store
The store (<data>/store/) is modde’s content-addressed mod file repository: the
actual staged bytes of every installed mod, one directory per mod, that the VFS
symlinks point into. The database’s per-mod file manifest maps each profile’s mods
to the precise files they staged, which is what makes uninstall surgical. See
Deployment & VFS.
Staging
Staging is the per-profile scratch tree (profiles/<profile>/staging/) that
materialize() writes the symlink farm into before it is projected onto the game
directory. For Wabbajack profiles, “staging” instead means the pre-resolved
mods/<mod>/… layout the modlist install produces. See
Deployment & VFS.
Deploy
To deploy is to project the active profile’s materialized staging tree into the
game’s resolved mod directory — creating the symlinks (or, for Wabbajack, hardlinks
or copies) the game actually sees. modde deploy (and modde play, which deploys
before launching) run this step. See Deployment & VFS.
Rollback
modde rollback swaps a profile’s staging and staging.bak directories with
atomic directory renames, restoring a deliberately-preserved earlier deploy state;
it fails if no staging.bak exists, and you must re-run modde deploy afterward to
re-project the restored tree. See
Deployment & VFS.
Ordering and conflicts
Load order vs. plugin order
These are two independent ordering systems, and conflating them is the most common
source of confusion. Load order (mod install priority) decides which file
wins when two mods ship the same path — later mod in the list wins, resolved by the
VFS. Plugin order decides the sequence Bethesda .esp/.esm/.esl plugins
load in the engine — a separate axis managed by LOOT and plugins.txt. See
Conflicts & load order.
VFS / symlink farm
The VFS (virtual filesystem) is the merged, modded view of the game’s data directory that modde assembles as a symlink farm: symlinks pointing back at the content-addressed store, so the real install on disk stays pristine. Unlike MO2’s USVFS — which hooks file-system calls per process — modde’s symlinks live in the real filesystem and are globally visible to every process without API hooking. See Deployment & VFS.
Conflict / collision
A conflict (or collision) is when two or more enabled mods provide the same relative
path, so only one version can be deployed. modde resolves it by load order priority
— the highest-priority provider wins — and modde collisions reports the pairs,
per-file winners, and severities. See
Conflicts & load order.
Collision severity (cosmetic / config / dangerous)
Each colliding file is graded by extension into a severity level: Cosmetic
(visual/audio only — dds, nif, wav; low risk), Config (may change
behaviour — ini, json, toml), Dangerous (scripts/plugins/DLLs that can
crash or corrupt saves — esp, esm, dll, pex), with Unknown for
unrecognised extensions. A mod pair’s reported severity is the worst among its
colliding files, and the classifier is per-game. See
Collision severity.
Shadowed mod
A shadowed mod is one whose every file is overridden by higher-priority mods, so
it contributes nothing to the deployed VFS. The collision report lists it as
"<mod>" (N files, all overridden by <mods>) and a diagnostic suggests disabling
it to shrink the deploy. See
Shadowed mods and redundant files.
Redundant file
A redundant file is a single path from a mod that always loses the conflict — it is
never the deployed winner. modde collisions --suggest-hides turns each into an
actionable modde profile hide command. See
Shadowed mods and redundant files.
Hidden file
A hidden file is a single file you exclude from a mod without disabling the whole
mod (modde’s equivalent of MO2’s .mohidden). Hidden files are recorded per
profile and skipped during the VFS build, so the next-highest provider of that path
wins instead. See Per-file hiding.
Overwrite mod
The overwrite (output) mod is the destination that captures files a launched executable writes back into the deployed tree — for example xEdit output or generated patches — so they are not lost between deploys. It is configurable per executable and is part of the shipped executable-management feature. See Executables & external tools.
Saves
Save vault
A save vault is the git-backed history of a game’s saves: one git repository per
game under <data>/saves/<game_id>/, with one branch per profile. Every capture is
an ordinary git commit, so you get full history, branching, and the ability to roll
a profile’s saves back to any point. See Save management.
Save snapshot
A save snapshot is a single capture in the vault — a git commit on the profile’s
branch containing the live save files at that moment, plus the mod fingerprint
embedded as commit trailers. modde save history lists them and modde save restore <commit> restores one. See Save management.
Save fingerprint
A save fingerprint is a SHA-256 hash computed over only the enabled, save-breaking mods of a profile (sorted and de-duplicated), stored as a commit trailer on each snapshot. On restore, modde compares the snapshot’s fingerprint against your current profile’s and warns when the save-breaking mod set differs. See Save-breaking mods and the fingerprint.
Save-breaking mod
A save-breaking mod is one whose presence or absence can make a save game incompatible — typically anything that changes the game’s serialized data: script extenders and their plugins, mods that bake scripts or records into saves, framework mods. Cosmetic mods (textures, reshades, UI tweaks) are not save-breaking, and each game plugin classifies its own mods. See Save management.
Experiments, profiles, and instances
Experiment stack
The experiment stack lets you try profile changes non-destructively, like git
branches for your mod setup: modde profile try pushes the current profile and
activates another, rollback pops back one level, and commit accepts wherever
you landed and clears the history. Saves swap along with each step. See
Experiment stack.
Stock snapshot
A stock (vanilla) snapshot is a preserved, hardlinked copy of a clean game install,
captured with modde stock snapshot <game> before you mod anything. modde computes
a deterministic tree hash over it so modde stock verify can later tell whether the
install has drifted from vanilla. See
Stock snapshots.
Install formats and methods
FOMOD
FOMOD is the most common scripted installer format on Nexus: instead of a flat
archive, the mod ships a fomod/ModuleConfig.xml script that asks the user
questions (texture resolution, body type, optional patches) and copies a different
file set depending on the answers. modde supports it interactively (a wizard) and
declaratively (a generated config you can pin and replay). See
FOMOD installer.
FOMOD declarative config
A FOMOD declarative config is a file (TOML, JSON, or Nix) that records your FOMOD
answers so an install is reproducible and non-interactive — the path used by Home
Manager and any automation. Generate one with modde fomod generate, then pass it
as --fomod-config. See
Declarative installation.
BAIN
BAIN (BAIN Archive INstaller) is the Wrye Bash multi-option layout: an archive of
numbered subdirectories like 00 Core, 01 Option that you pick from. modde
auto-detects BAIN by the numbered-subdir convention, but the interactive
sub-package selection flow is still Partial — until a selection is made,
execution returns RequiresUserInput. See
How mod installation works and the
parity audit.
Install method
An install method is modde’s classification of an archive’s layout
(InstallMethod): BareExtract, SingleFileSet, DirectoryMod, Fomod,
REDmod, Bain, DllOverlay, and more. Detection is ordered so game-specific
layouts win before generic guesses; most methods stage automatically, while FOMOD
and BAIN may pause for input. See
How mod installation works.
Cyberpunk 2077 / REDengine
REDmod
REDmod is CD Projekt Red’s official mod packaging format for Cyberpunk 2077 — a
package keyed off an info.json manifest (plus archives/) that installs under
<install>/mods/<name>/. modde detects it, symlinks the package, and runs a
post-deploy redmod deploy pass to register it as a loadable archive. See
Cyberpunk 2077.
REDscript / CET / TweakXL
These are the major loose-file Cyberpunk modding frameworks modde deploys:
REDscript (compiled gameplay scripts under r6/scripts/), CET (Cyber Engine
Tweaks, a Lua console/UI loaded via a version.dll proxy under
bin/x64/plugins/cyber_engine_tweaks/), and TweakXL (data tweaks under
r6/tweaks/). modde deploys their files into the correct roots; each framework
sequences its own loading. See Cyberpunk 2077.
Bethesda plugins and archives
BSA / BA2
BSA (Skyrim) and BA2 (Fallout 4) are Bethesda’s packed archive container formats
for game assets. modde reads and indexes their contents so a loose
textures/sky.dds correctly collides with a sky.dds packed inside another mod’s
.bsa; the container extension itself classifies as cosmetic, with danger judged by
what is inside. Wabbajack lists can also have modde repack loose files back into a
real .bsa/.ba2. See
Archive-aware conflicts.
pak / ucas / utoc
pak, ucas, and utoc are the Unreal Engine 4/5 archive formats — the archive
extensions modde’s UE classifier recognises for titles like Stellar Blade and
Oblivion Remastered. UE games route mod paks into a ~mods/ directory at deploy
time. See Per-game classifiers and
Supported games.
Master / plugin
A plugin is a Bethesda content file (.esp, .esm, .esl); a master is a plugin
that another plugin depends on (declared in its header). modde reads the first ~1 KB
of each active plugin’s TES4 header and flags missing masters — a plugin
requiring a master not in the active load order, which crashes the game on load. See
Validating plugins.
Form 43
Form 43 (record header version 0.94) is the old Skyrim LE (“Oldrim”) plugin
format. Using a Form 43 plugin in Skyrim SE/AE (which expects Form 44, 1.70) can
crash the game; modde loot validate detects it and the fix is to resave the plugin
in the Creation Kit. See Validating plugins.
LOOT
LOOT (the Load Order Optimisation Tool) is the community standard for sorting
Bethesda plugin load order. modde loot sort parses the LOOT masterlist and derives
load-after / incompatible rules for the plugins you actually have active. See
LOOT sorting.
Masterlist
The masterlist is LOOT’s community-maintained YAML file of per-plugin after,
requires, and incompatible rules for a game. modde caches it locally and, if it
is missing, prints the exact curl command to fetch the right one; masterlists ship
for Skyrim SE/AE, Fallout 4, Fallout 76, and Starfield. See
LOOT sorting.
Wabbajack
Wabbajack modlist
A Wabbajack modlist (a .wabbajack file) is a recipe, not a bundle of mods: it
records exactly which archives to download and exactly how to reconstruct a curated
mod setup from them. modde replays that recipe natively on Linux — no Windows VM,
no Wine-hosted client — verifying every download against its hash. See
Wabbajack modlists.
Directive
A directive is one step in a Wabbajack manifest’s ordered install plan. modde
supports the common types: FromArchive (extract one file from a source archive),
PatchedFromArchive (extract then apply an OctoDiff binary delta),
InlineFile/RemappedInlineFile (write bytes stored inside the .wabbajack), and
CreateBSA/BA2 repack. See
Install directives.
Nexus
Nexus Collection
A Nexus Collection is a curated, version-pinned set of mods published on Nexus. modde resolves it by slug (discovering the game domain and latest revision, then fetching the pinned manifest) and installing it locks the profile to preserve the curator’s intended load order. See Nexus Collections.
nxm://
nxm:// is the URI scheme Nexus’s “Download with [manager]” buttons emit, carrying
the game domain, mod ID, file ID, and a short-lived key/expires pair. Run
modde nxm install to register modde as the system handler so clicking a Nexus
download link hands the URI to modde. See
The nxm:// handler.
Games and engines
GamePlugin
A GamePlugin is the per-game integration trait inside modde: each of the 15
built-in titles is a GamePlugin that knows that game’s archive layouts, mod
directory, save location, conflict classifier, and Wine/Proton DLL-override quirks.
It also supplies the analyze hook that lets a game claim a layout (REDmod, an ENB
pack) before any generic install probe runs. See
Games and Architecture.
Generic game
A generic game is a user-defined title registered at runtime with modde game add
or an imported GameSpec TOML, rather than a hand-coded GamePlugin. Generic game
support is Partial: deployment, conflict classification, launcher integration, and
executable management work, but there is no bespoke filesystem scanner and no save
tracker. See Generic & user-defined games.
Tools and runtime
OptiScaler
OptiScaler is an upscaler/frame-generation replacement (DLSS/FSR/XeSS) that hooks a game through a proxy DLL, which modde manages as a release-backed tool — including release downloading, community profiles, FSR4 payload variants, and OptiPatcher. It also subsumes modde’s old fgmod DLL-restore logic. See OptiScaler.
Proton / Wine DLL override
A Wine DLL override (WINEDLLOVERRIDES) tells Wine/Proton to load a mod’s native
proxy DLL instead of its own built-in stub — required for frameworks that ship as a
proxy DLL (CET’s version.dll, ReShade’s dxgi.dll, OptiScaler). modde detects the
proxy DLLs a deploy places and contributes the overrides automatically, surfaced
through the per-game Proton tool integration. See
Tools & overlays and
Cyberpunk Wine DLL overrides.
See also
- Conflicts & load order — severity, shadowed mods, LOOT, Form 43
- Deployment & VFS — the symlink farm, staging, deploy, rollback
- Save management — vaults, snapshots, fingerprints, save-breaking mods
- Profile management — profiles, locking, the experiment stack
- How mod installation works — install methods, FOMOD, BAIN
- Wabbajack modlists — modlists, directives, archive sources
- Nexus Mods — Collections,
nxm://, browse/search - Tools & overlays — OptiScaler, Proton, Wine DLL overrides
- Generic & user-defined games — GamePlugin vs generic games
- MO2 parity & capability audit — the
Done/Partialstatus of every feature - CLI reference — every command and flag
FAQ
Short answers to the questions that come up most. Each links to the deep-dive
page that backs the claim, so you can verify anything that matters to you. For
the conservative, test-coupled accounting of what is Done vs Partial, see the
MO2 parity & capability audit.
What is modde, and who is it for?
modde is a cross-platform game mod manager written in Rust, running natively on Linux, macOS, and Windows. It deploys mods through a virtual-filesystem symlink farm, resolves conflicts, manages Bethesda plugin order, installs FOMOD and Wabbajack lists, talks to Nexus, and keeps git-backed save vaults — with both a CLI and a GUI.
It is for players who want a real Mod Organizer 2 / Vortex workflow on their own OS, scriptable from the command line, with git-backed save history. If you use Nix, you also get an optional declarative, reproducible path via the home-manager module.
How is it different from Mod Organizer 2 / Vortex?
modde is not a clone. It runs natively on Linux, macOS, and Windows, and ships
things MO2/Vortex never had: git-backed save vaults with history, declarative
FOMOD, native Wabbajack on Linux, and — for Nix users — declarative home-manager
profiles. MO2 is still ahead on a few fronts (the rich mod-information dialog, a
fully interactive download queue, a complete merged-VFS browser). The full,
intentionally conservative side-by-side — including what is Done, Partial, and
not yet shipped — lives in the
MO2 parity & capability audit.
Do I need NixOS to run modde?
No. modde runs natively on Linux, macOS, and Windows. Install it through your
platform’s native package manager (AUR, COPR, apt, Flatpak, Homebrew, winget,
Scoop, Chocolatey), a direct download, or cargo install modde-cli. If you use
Nix, modde is also a flake — a reproducible install that, through the
home-manager module, additionally lets you declare your mod profiles as code.
That is one option among many, not a requirement. See
Installation for every channel and its commands.
Does it run on Windows or macOS?
Yes — both are first-class native targets. Every release ships native builds
for Linux (x86_64 / aarch64), macOS (x86_64 / aarch64), and Windows (x86_64),
each with the modde CLI and the modde-ui desktop app. Install them via
Homebrew, winget, Scoop, or Chocolatey, or grab a direct download. Two
platform-native steps to know: on macOS, clear the quarantine attribute once after
extracting a tarball (xattr -dr com.apple.quarantine modde modde-ui); on Windows,
the .exe artifacts are Authenticode-signed, so you can verify them with
Get-AuthenticodeSignature. Full per-platform instructions are under the
macOS and
Windows sections of the install page.
Do I need Nexus Premium?
Only for automated CDN downloads. A free Nexus account and API key give you
browse, search, update checks, metadata, endorsements, and nxm:// handling.
Premium adds the ability to fetch files directly from the Nexus CDN without the
manual browser step — which is what makes hands-off Wabbajack and Collection
installs possible. Without Premium you can still install, you just confirm some
downloads in the browser. See the Nexus integration guide.
Can I install Wabbajack lists on Linux without a Windows VM?
Yes. modde installs .wabbajack lists natively on Linux — no Windows VM,
no Wine-hosted Wabbajack client. It parses the manifest, runs the directives, and
deploys through the same VFS engine as everything else, using its own download
backends (Nexus, GitHub, Direct, Google Drive, MEGA, MediaFire) for the archives.
See the Wabbajack guide.
How do I move my saves and profiles to another machine?
modde has no cloud sync, but its on-disk layout is built for it. The reliable pattern is two parts:
- Saves: each game’s save vault under
saves/<game_id>/is already a git repository — add a remote and push it, then clone or pull on the other machine. Your save snapshots and their mod fingerprints ride along. - Profiles: declare them in home-manager (
programs.modde.profiles) and rebuild on the second machine, so the profile is reproduced from pinned inputs rather than copied. If you work imperatively, export profiles as TOML andmodde importthem.
Do not rsync modde.db or the large derivable caches. The full procedure,
including what not to sync, is in
Data, instances & backups.
What games are supported?
15 games ship with built-in support. Depth varies by title — some are Done
end-to-end, others are Partial. The canonical per-game table, with IDs and
links to each game’s guide, is on the
Supported games page.
Is executable management supported now?
Yes — this is Done. You can save named launch targets (xEdit, BodySlide,
Nemesis, the Creation Kit, LOOT, …) with arguments, a working directory,
environment variables, and Wine DLL overrides, then run them with overwrite
capture so anything the tool writes lands in a mod you control. It works from
both the CLI (modde exec ... / modde tool add-executable ...) and a GUI
Executables view. See Executables & external tools.
Can I add a game that is not built in?
Yes, with caveats — this is Partial. The generic-game path lets you
register an arbitrary title with a small GameSpec TOML via modde game add.
You get the engine-agnostic parts (VFS deployment, conflict detection, launcher
integration, executable management), but not a bespoke filesystem scanner or
save tracker. Treat it as “modde can deploy and launch mods for this game”, not
“modde fully understands this game”. See
Generic & user-defined games.
Where does modde store its data?
In a single data directory plus a small config file, following XDG on Linux —
by default ~/.local/share/modde/ (database, mod store, profiles, downloads,
stock snapshots, save vaults). You can relocate it wholesale with --data-dir /
MODDE_DATA_DIR, or run several isolated named instances. The full on-disk
layout is documented in Data, instances & backups.
Is there any telemetry?
Opt-in, and currently a no-op stub — nothing is sent. Telemetry is gated
behind a non-default remote-telemetry build feature; default builds (including
the Nix flake and cargo install modde-cli) do not include it at all. Even in a
build that does, no data is collected or transmitted unless you explicitly
configure a telemetry endpoint and token via environment variables. With no
endpoint configured, the code path is inert. There is no analytics in a normal
modde install.
See also
- Installation — every channel and its commands
- Quick Start — define and deploy your first profile
- MO2 parity & capability audit —
DonevsPartial, by feature and game - Supported games — the full per-game table
- Generic & user-defined games — register a title modde does not ship
- Executables & external tools — named launch targets with overwrite capture
- Wabbajack lists — native list installs on Linux
- Data, instances & backups — storage layout and multi-machine sync