Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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 .wabbajack modlists 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

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:

PlatformNative packagesAlso available
LinuxAUR · COPR · apt · FlatpakAppImage · tarball · Cargo · Nix
macOSHomebrewtarball · Cargo · Nix
Windowswinget · Scoop · Chocolateyzip · 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-sys will 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

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 .wabbajack archive. Some of those CDN links now resolve through Wabbajack’s chunked download page rather than a plain file response, so a bare prefetch (and pkgs.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.wabbajack

Then either set wabbajackList.path = ./list.wabbajack; (no hash needed 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 set nexus.apiKeyFile). CDN downloads require a Nexus Premium subscription; modde nexus status tells you whether your key is valid and Premium. See Nexus Mods.
  • gameDir / --game-dir is required for lists that read vanilla files. Skyrim SE lists with GameFileSourceDownloader entries fail without it.
  • Use the detected game id. Run modde detect and copy the exact id; a typo’d --game is the most common “nothing happens” cause.
  • The hash is 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 rollback restores the previous deployment.
  • Adopt existing saves before your first switch. If you already have saves, run modde save adopt --game skyrim-se --profile my-skyrim so 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:

  1. 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.
  2. 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.
  3. 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

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:

LevelMeaning
CriticalWill cause game issues
MajorLikely visible problems
CosmeticMinor 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 .wabbajack list 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

Supported Games

This table is intentionally conservative:

  • Done means the game path is shipped end to end.
  • Partial means some core pieces exist, but major workflows are still missing or not yet trustworthy.
  • Not shipped means the capability should not be treated as available.
  • The canonical status baseline for this page lives in docs/capability-matrix.toml in the repository.

Bethesda titles

GameIDOverall statusScannerConflict detectionSave tracking
Skyrim Special Editionskyrim-seDoneYesYesDone
Skyrim Anniversary Editionskyrim-aeDoneYesYesDone
Fallout 4fallout4DoneYesYesDone
Fallout 76fallout76PartialYesYesPartial (server-side / local cache only)
StarfieldstarfieldPartialYesYesDone

Other games

GameIDOverall statusScannerConflict detectionSave tracking
Cyberpunk 2077cyberpunk2077DoneYesYesDone
Stellar Bladestellar-bladePartialYesYesDone
Baldur’s Gate 3baldurs-gate3PartialYesYesDone
Stardew Valleystardew-valleyPartialYesYesDone
Fallout: New Vegasfallout-new-vegasPartialYesYesDone
The Elder Scrolls IV: OblivionoblivionPartialYesYesDone
The Elder Scrolls IV: Oblivion Remasteredoblivion-remasteredPartialYesYesDone
Mount & Blade II: BannerlordbannerlordPartialYesYesDone
The Witcher 3: Wild Huntwitcher3PartialYesYesDone
Subnautica 2subnautica2PartialYesYesDone

Wabbajack game mapping

When installing Wabbajack modlists, the manifest game names are mapped to modde game IDs:

Wabbajack namemodde ID
SkyrimSpecialEditionskyrim-se
Fallout4fallout4
Fallout76fallout76
Starfieldstarfield
Cyberpunk2077cyberpunk2077
FalloutNewVegasfallout-new-vegas
FalloutNVfallout-new-vegas
Oblivionoblivion
OblivionRemasteredoblivion-remastered

Per-game guides

Each shipped title has a dedicated guide with its engine, mod directory, save location, installer quirks, and Linux/Proton notes:

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

PropertyValue
EngineBethesda Creation Engine
Game idsskyrim-se, skyrim-ae (share this guide)
Steam App ID489830 (both editions)
Steam install dirSkyrim Special Edition
Mod directoryData/
Archive formats.bsa, .ba2
INI files (per-profile)Skyrim.ini, SkyrimPrefs.ini, SkyrimCustom.ini
Nexus domainskyrimspecialedition
Nexus numeric game id1704
Plugin systemYes (plugins.txt + LOOT)
ScannerDone
Conflict detectionDone (loose files + BSA/BA2 contents)
Save trackingDone

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 489830 and the Skyrim Special Edition directory 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.txt paths 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 into Data/.

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:

  1. Authoritative pass. Every plugin listed in plugins.txt that actually exists on disk is emitted as a discovered mod (plugin/<filename>, confidence 0.95). For each plugin stem, the scanner also pulls in companion archives that share the stem — both <stem>.bsa/<stem>.ba2 and the <stem> - Textures.bsa/.ba2 texture-archive convention.
  2. Unmanaged pass. Any .esp/.esm/.esl in Data/ not already seen via plugins.txt is 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.

SeverityExtensions
Dangerousesp, esm, esl, pex, dll, psc
Configini, cfg, json, toml, xml
Cosmeticdds, 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 *.ess save files (and *.bak are recognized but skipped as backup copies). The Skyrim .skse co-save written alongside each .ess is explicitly excluded from tracking — it is a SKSE side file, not a save of record.
  • For each .ess, modde parses the binary header (magic TESV_SAVEGAME) and extracts the save number and character name, producing labels like Lydia — 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 rule
  • requires → 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:

DiagnosticSeverityWhat it meansSuggested fix
Missing masterErrorA plugin lists a master file that is not in the active load order; the game crashes on loadInstall and enable the mod providing that master
Form 43 pluginWarningA plugin uses the Oldrim/LE format (Form 43) instead of SSE’s Form 44; can cause crashes in SE/AEOpen it in the SSE Creation Kit and re-save to convert to Form 44
Orphaned overridesInfoThe 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 into Data/. 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 GameFileSourceDownloader entries (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/.ba2 archives plus .esp/.esm/.esl plugins.

Gaming tools that matter for this title

  • Proton. Skyrim SE/AE run under Proton; modde derives its save and plugins.txt paths 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_profiles is 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 / gameDir is required for vanilla-referencing modlists. Many Wabbajack lists — Legends of the Frost is the canonical example — reference vanilla Skyrim Data files 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-dir error rather than silently producing a broken profile.
  • installMode = "await-game" (home-manager). You can declare a Wabbajack profile before Skyrim is installed. In await-game mode, activation prints the next step and exits successfully instead of failing; once Skyrim is installed through Steam/Heroic, set gameDir and switch installMode to "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-se to 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 wabbajackList profile cannot also set a nexusCollection — the two install sources are mutually exclusive.

See also

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

PropertyValue
Engine familyBethesda Creation Engine (EngineFamily::Bethesda)
game_idfallout4
Display nameFallout 4
Steam App ID377160
Heroic (GOG) app ID1998527297
Nexus domainfallout4 (numeric game ID 1151)
Wabbajack nameFallout4
Mod directoryData/ (relative to the install root)
Archive format.ba2 (Bethesda Archive v2)
Managed INI filesFallout4.ini, Fallout4Prefs.ini, Fallout4Custom.ini
Save format.ess (FO4_SAVEGAME magic)
Plugin systemYes (plugins.txt, ESP/ESM/ESL)
Save profilesEnabled (supports_save_profiles = true)
Overall statusDone

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:

  1. Steam — App ID 377160, install directory steamapps/common/Fallout 4. This is the primary, best-tested detection path.
  2. 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:

  1. Authoritative pass. It reads plugins.txt from the Proton prefix and, for every listed plugin that exists on disk, emits a plugin/<filename> mod at confidence 0.95. For each plugin it also pairs companion archives that share the plugin’s stem — both <stem>.ba2/<stem>.bsa and the texture-archive convention <stem> - Textures.ba2 — so a plugin and its assets are reported as one mod.
  2. Unmanaged pass. It then scans Data/ for any .esp/.esm/.esl files not present in plugins.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.espfoo.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:

SeverityExtensionsMeaning
Dangerous.esp, .esm, .esl, .pex, .dll, .pscPlugins, compiled/source Papyrus scripts, native DLLs — overrides here change game logic and can break saves
Config.ini, .cfg, .json, .toml, .xmlSettings 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, .ba2Meshes, 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.txt read/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 .esp loads first”), stored in a dedicated plugin_order table 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/fallout4 repository and turns the after/requires/incompatible rules 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_SAVEGAME magic 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 with modde fomod generate, inspect options with modde 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, set extra_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=forced with forced_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 with modde tool enable/configure.
  • Overwrite capture. Tools that rewrite files in Data/ (xEdit/FO4Edit, BodySlide, Material Editor) can be run through modde tool run … --game fallout4 so their output is captured into an __overwrite__ mod and survives redeployment.

modde ships no OptiScaler profile for Fallout 4 (optiscaler_profiles is 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/377160 prefix, plugins.txt, the My Games/Fallout4 save 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, and Fallout4Custom.ini are under the prefix’s My Games/Fallout4, not your Linux $HOME. modde patches them comment- and formatting-preserving rather than rewriting them wholesale. Creating Fallout4Custom.ini and 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 proton tool 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 gameDir and save path if you use it.

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

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

PropertyValue
EngineBethesda Creation Engine (EngineFamily::Bethesda)
modde game_idfallout76
Steam App ID1151340
Nexus domainfallout76 (numeric game id 2590)
Wabbajack nameFallout76
Overall statusPartial
ScannerYes (loose-archive scanner)
Conflict detectionYes (shared Bethesda classifier)
Save trackingPartial — server-side; local cache only
Plugin systemReported 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 under steamapps/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 .ba2 archive files (the Fallout 76 archive extension);
  • skips game-shipped archives by ignoring any file whose name starts with the SeventySix prefix (those are vanilla content, not mods);
  • assigns each discovered archive a mod id of the form archive/<stem> and a confidence of 0.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:

SeverityExtensions (representative)
Dangerousesp, esm, esl, pex, dll, psc
Configini, cfg, json, toml, xml
Cosmeticdds, 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_SAVEGAME to identify local Bethesda save files (the same binary header family as Skyrim’s TESV_SAVEGAME and Fallout 4’s FO4_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 normal capture: … 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.ini
  • Fallout76Prefs.ini
  • Fallout76Custom.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 (.ba2 or bare Data/ layouts). Drop-in mods are recognized by the Bethesda bare-layout policy and deployed into Data/. A single .ba2 is 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 with ModuleConfig.xml is 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:

ToolUse on Fallout 76
protonSelect the Proton runtime and set Wine/Proton DLL overrides for the prefix
mangohudPerformance HUD overlay
gamemodeSystem performance tuning at launch
vkbasaltVulkan post-processing (sharpening, CAS)
reshadeD3D post-processing for the Wine-backed game
optiscalerDLSS/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 Dangerous severity 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 .ba2 mod does nothing after deploy, confirm its archive name is listed in the custom INI’s archive list — there is no plugins.txt to 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

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

PropertyValue
Engine familyBethesda Creation Engine
modde game idstarfield
Display nameStarfield
Steam App ID1716740
Nexus domainstarfield
Nexus numeric game id4187
Wabbajack nameStarfield
Mod directoryData/ (under the install root)
Archive extension.ba2
Managed INI filesStarfieldPrefs.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.toml and the parity audit. Done means shipped end to end and user-reachable; Partial means 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:

  1. Reads plugins.txt for authoritative load order. modde locates plugins.txt inside 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.
  2. Emits plugins listed in plugins.txt first, at high confidence (0.95). For each plugin it pairs companion archives that share the plugin’s stem — both <stem>.ba2 and the texture-suffixed <stem> - Textures.ba2 — into the same discovered mod.
  3. Also scans for plugins not in plugins.txt (disabled or unmanaged) by walking Data/ for .esp, .esm, and .esl files, 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.

SeverityExtensions
Dangerousesp, esm, esl, pex, dll, psc
Configini, cfg, json, toml, xml
Cosmeticdds, 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: .sfs files in the Saves directory (non-recursive).

  • Slot category, derived from the filename prefix:

    PrefixCategory
    Autosaveauto
    Quicksavequick
    Exitsaveexit
    Savemanual

    Any .sfs that does not match a known prefix still falls back to manual, so no save is silently dropped.

  • Label: the file stem (the save’s filename without the .sfs extension).

  • 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-oxide installer, 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 .ba2 archives and/or .esp/.esm/.esl plugins dropped into Data/ are recognized directly by the scanner and bare-layout policy; no scripted installer is required.
  • There is no REDmod, SMAPI, or .pak step for Starfield — those belong to other engines (Cyberpunk, Stardew Valley, and Unreal/Larian titles respectively). Starfield is a plugin + .ba2 Creation 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 so tool revert can 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.txt live inside compatdata/1716740/pfx, which Proton only creates after the first launch. Until then, save detection returns empty and the scanner falls back to scanning loose Data/ plugins without an authoritative order.

  • DLL-based mods (script extenders, ASI loaders) usually need DLL overrides. Configure them through the proton tool rather than hand-editing the prefix:

    modde tool configure proton --game starfield dll_override_mode=forced forced_dll_overrides=sfse_loader
    

    Use whatever proxy/loader DLL name the specific mod requires.

  • StarfieldCustom.ini may need creating. Many loose-file and archive-invalidation workflows expect a StarfieldCustom.ini in the My Games folder; modde manages both StarfieldPrefs.ini and StarfieldCustom.ini per profile, but the game itself does not always ship a Custom INI 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

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, and plugins.txt load-order read/write all ship. What keeps New Vegas from Done is 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 is docs/capability-matrix.toml and the supported-games table.

Engine and overall status

PropertyValue
Engine familyGamebryo (EngineFamily::Gamebryo)
modde game_idfallout-new-vegas
Display nameFallout: New Vegas
Mod directory<install>/Data
Archive format.bsa (Bethesda archives)
Plugin systemYes — .esp / .esm, ordered via plugins.txt
Script extenderNVSE (.nvse) — treated as save-breaking binary content
INI filesFallout.ini, FalloutPrefs.ini, FalloutCustom.ini
Save format.ess (save), .fos (co-save)
Nexus domainnewvegas
OptiScaler profilesNone shipped for this title
Overall statusPartial

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 keyValue
steam_app_id22380
steam_dirFallout 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 .esp and .esm file as a discovered mod;
  • pairs each plugin with a same-stem .bsa archive when one exists (e.g. MyMod.esp + MyMod.bsa are reported together as one mod’s files);
  • assigns each mod the id plugin/<filename> with a fixed match confidence of 0.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:

SeverityExtensions
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 a scripts/ 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;
  • *.bak backups are excluded;
  • detection is non-recursive (the top level of Saves only);
  • every match is filed under the manual category, 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.xml are 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 the StripContentRoot / 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 into Data/ 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/FalloutNV tree 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

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

AspectStateNotes
OverallPartialDeployment, scanning, conflicts, and save tracking work; there is no bespoke INI-merge or BSA-aware extraction UX.
ScannerYesReads Data/ for .esp/.esm plugins and matching .bsa archives.
Conflict detectionYesGamebryo collision policy with a bsa-aware archive comparison.
Save trackingDone.ess/.fos saves under the Proton prefix, git-backed vault, fingerprinting.
Plugin systemYesplugins.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:

FieldValue
game_idoblivion
Display nameThe Elder Scrolls IV: Oblivion
Steam App ID22330
Steam install dirOblivion
Nexus domainoblivion
Wabbajack nameOblivion
my_games_dirOblivion
INI fileOblivion.ini
Archive extensionbsa

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_id oblivion-remastered, Steam App ID 2623190), which uses the Unreal pak/ucas/utoc pipeline — 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 .esp or .esm extension (case-insensitive). Sub-directories are skipped at this level.
  • For each plugin it also pulls in a sibling .bsa archive 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, its Data/-relative file list with sizes, and a detection confidence of 0.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:

SeverityExtensions
Dangerousesp, esm, dll, lua, ws
Configini, json, xml
Cosmeticdds, 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 *.bak backups.
  • Categorizes every match as manual and 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 by analyze_mod_archive / recognizes_bare_layout as 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/.dll plugins 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_*.dll to 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 snapshots Data/ 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-run modde save adopt if 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.ini as a config file and classifies INI collisions as Config severity, but it does not perform an MO2-style INI tweak/merge. Edit the INI under the prefix’s My Games/Oblivion/ directly when a mod requires it.
  • Partial rating — 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 with backup plugins/restore-plugins snapshots.

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 gameDir unset (or installMode = "await-game"); activation prints the next step and continues. modde never installs the base game itself.

See also

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 of Done is 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

FieldValue
Display nameThe Elder Scrolls IV: Oblivion Remastered
Game idoblivion-remastered
Engine familyUnreal4 (UE5 layout; shares modde’s UE pak handling)
Project folderOblivionRemastered/ (under the install root)
Steam App ID2623190
Steam install dirOblivion Remastered
Heroic (GOG/Epic)not registered
Nexus domainoblivionremastered
Wabbajack nameOblivionRemasteredoblivion-remastered
Save profilesenabled
Plugin systemyes (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 / .utoc extensions into a single mod (so a .pak and its sidecar .ucas/.utoc count as one mod, not three). These come from the paks-mods source location at confidence 0.9, with mod ids prefixed pak/.
  • Plugins are discovered as one mod per top-level .esp file directly in Data/ (source location Data, confidence 0.8, mod ids prefixed plugin/). 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 formFootprint 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, wsDangerous
pak, ucas, utoc (as archive members)Dangerous
ini, json, xmlConfig
dds, png, jpg, tga, nifCosmetic

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 shapeDetected method
.pak / .ucas / .utoc files at the archive rootSingleFileSet — files stage straight into the resolved mod root
A top-level Data/ directoryStripContentRoot { 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 WINEDLLOVERRIDES from the proxy DLLs it finds in Binaries/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.
  • ~mods ordering is a UE convention, not a modde trick. If a pak mod is not taking effect, confirm it landed in Content/Paks/~mods and not loose in Content/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

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

PropertyValue
Engine familyREDengine 4 (EngineFamily::CyberpunkRedEngine)
Game idcyberpunk2077
Display nameCyberpunk 2077
Overall statusDone
ScannerYes
Conflict detectionYes
Save trackingDone
Steam App ID1091500
Heroic (GOG) app id1423049311
Heroic (Epic) app idGinger
Nexus domaincyberpunk2077
Nexus numeric game id3333
Wabbajack nameCyberpunk2077

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:

  1. Heroic (GOG / sideload): ~/Games/Heroic/Prefixes/default/Cyberpunk 2077/pfx/drive_c/users/steamuser/Saved Games/CD Projekt Red/Cyberpunk 2077
  2. 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:

  1. 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.
  2. 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 mod archives.

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 (or redmod.exe on 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/redmod binary exists, or put a redmod binary on PATH.

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 rootMod kindid prefixConfidence
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>/redscriptreds/0.90
r6/tweaks/<name>/TweakXLtweak/0.90
archive/pc/mod/<file>.archiveloose .archivearchive/0.85
mods/<name>/REDmod packageredmod/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:

SeverityExtensions
Dangerousreds, lua, tweak, xl, yaml, yml, dll
Configini, cfg, json, toml
Cosmeticarchive, 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:

PrefixCategory
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 deploy pass 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.json plus an archives/ (or archive/) subdirectory. modde records the install as a REDmod install whose manifest is info.json, and the post-deploy hook later registers it with redmod deploy.
  • Bare Cyberpunk extracts (loose redscript / TweakXL / CET / .archive trees) are recognized by the top-level directory names listed under Bare-layout recognition and symlinked wholesale into mods/.
  • 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 DLLTypically used by
versionCET (Cyber Engine Tweaks), ASI loaders
winmmASI loaders, some mod frameworks
dinput8various mod frameworks
d3d11ReShade, ENB
dxgiOptiScaler, ReShade (often handled by fgmod)
winhttpsome mod loaders
xinput1_3controller-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/redmod binary (or a redmod on PATH), 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 the native,builtin override. modde detects the common ones in bin/x64; verify with modde tool status --game cyberpunk2077 and the troubleshooting guide if a framework does not load.
  • .archive internals are opaque. modde detects archive-vs-archive collisions by filename but cannot diff their contents, so two large .archive mods 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

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

PropertyValue
Engine familyREDengine (EngineFamily::Witcher)
game idwitcher3
Display nameThe Witcher 3: Wild Hunt
Overall statusPartial
ScannerYes
Conflict detectionYes
Save trackingDone

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:

FieldValue
Steam App ID292030
Steam install dirThe Witcher 3
Heroic (GOG)not wired
Heroic (Epic)not wired
Nexus domainwitcher3

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/, or content/ 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 own mods/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, or content, 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 under mods/.

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:

Directorymod id prefixConfidence
mods/mod0.90
dlc/dlc0.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
wsScript — Dangerous (overrides game scripts)
dllBinary — Dangerous
xml, csvConfig
bundle, cacheArchive
dds, png, jpg, xbmTexture / 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) returns true if a mod directory contains any .ws file anywhere in its tree (case-insensitive).
  • script_conflict_paths(mods_root) walks every installed mod under mods/, records the relative .ws paths 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

PropertyValue
Save directory~/Documents/The Witcher 3/gamesaves
Tracked extension.sav
Excluded*.png (save thumbnails)
RecursiveNo — top-level of the save dir only
Save profilesSupported

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:

  1. contain the REDengine root layout (mods/, dlc/, bin/, content/), or
  2. 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 relocates Documents, confirm modde detect/save scanning is looking where the Proton prefix actually writes saves.
  • Script merging is on you. modde flags colliding .ws files but does not merge REDengine scripts. For modlists that touch the same scripts, plan to do a manual or tool-assisted merge — and re-check modde collisions afterward.
  • Proxy DLLs need the override. A mod that relies on a dxgi/d3d11/ version/winmm proxy 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 the proton tool.
  • No Heroic auto-detect. GOG/Epic copies are not auto-discovered; point the profile’s gameDir at 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

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

PropertyValue
Engine familyUnreal4 (the shared UE4/UE5 pak plugin)
Steam App ID3489700
Steam library folderStellar Blade
UE project short nameSB
Nexus domainstellarblade
Wabbajack nameStellarBlade
Save profilesEnabled
OptiScaler profilescommunity-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 subfolder Stellar Blade. modde walks your Steam library folders (including extra libraries from libraryfolders.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 IDLabelKindResolves to
ue4-saved-configUE4 Saved/ConfigUserConfig<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/:

SubdirectorySource location tag
~modspaks-mods
LogicModslogic-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:

SeverityExtensions
Dangerouspak, ucas, utoc, dll, lua
Configini, cfg, json, toml, xml, yaml
Cosmeticdds, 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.xml can be driven through modde 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:

FieldValue
Proxy DLLdxgi.dll
Source modegithub_release, release tag official:v0.9.1
Tested OptiScaler version0.9
FSR4 variantlatest_fp8
emulate_fp8true (FP8 emulation, for RDNA3 and other cards without native FP8)
OptiPatcherenabled
spoof_dlssfalse
Companion filescopied

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-config target both live inside the Proton prefix and resolve to None until Proton has created compatdata/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 WINEDLLOVERRIDES entry is present — modde derives it from the proxy name in Binaries/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

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

AspectValue
game_idsubnautica2
Engine familyUnreal4 (covers UE4 and UE5 pak/IoStore titles)
UE project nameSubnautica2
Steam App ID1962700
Nexus domainsubnautica2
Overall statusPartial
ScannerYes (shared UE4 pak scanner)
Conflict detectionYes (shared UE4 collision policy)
Save trackingDone (Proton-prefix .sav capture)
Save profilesEnabled (with_save_profiles(true))
OptiScaler profileNone 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 fieldValue
steam_app_id1962700
steam_dirSubnautica2
heroic_gog_app_idnone
heroic_epic_app_idnone

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 subdirectorySource location label
Subnautica2/Content/Paks/~modspaks-mods
Subnautica2/Content/Paks/LogicModslogic-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:

ExtensionSeverityMeaning
pak, ucas, utocDangerousOverlapping game-content chunks — last-writer wins and the loser’s assets are masked
dll, luaDangerousCode/script overlap (proxy DLLs, UE4SS Lua)
ini, cfg, json, toml, xml, yamlConfigConfiguration overlap — usually mergeable/tunable by hand
dds, png, jpg, tgaCosmeticTexture 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, or lua — 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 IDLabelResolves to
ue4-saved-configUE4 Saved/Configcompatdata/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/.utoc and 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 proton tool selects the Proton build and can apply DLL overrides; everything below the executable runs inside the App ID 1962700 prefix.

  • 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 right WINEDLLOVERRIDES. The recognised proxy DLLs are:

    Proxy DLLTypical 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-dxgi OptiScaler preset — subnautica2 registers an empty OptiScaler profile list. You can still enable the generic optiscaler tool if you know what you are doing, but modde provides no curated, game-specific preset for this title, so there is no profile = "..." 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-config target, and prefix-based config all require the Proton prefix to exist. A fresh install has no compatdata/1962700 until you run the game once.
  • ~mods lives 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 .pak without 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 a nexusmods.com/subnautica2/mods/<id> URL or nxm:// 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

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.

CapabilityStatusNotes
Install detectionDoneSteam app id 1086940, Steam dir Baldurs Gate 3
Mod directory resolutionDoneProton-prefix Mods/ with install-relative fallback
ScannerPartialDiscovers root .pak mods under Mods/
Conflict detectionPartialpak-aware collision classifier
Save trackingDone.lsv saves, git-backed vault
Load orderPartialGenerates 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:

LauncherIdentifier
Steam app id1086940
Steam library dirsteamapps/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:

PurposePath (relative to the data root above)
Mod directoryMods/
Load order filePlayerProfiles/Public/modsettings.lsx
Save directoryPlayerProfiles/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 Mods directory (single-file .pak rule).
  • Each discovered mod is reported with a pak/ mod-id prefix and a confidence of 0.95.
  • A mod’s on-disk footprint is the lower-cased mods/<stem>.pak file, 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:

SeverityExtensionsMeaning
Cosmeticdds, png, jpg, tga, nifVisual overlap; last writer wins
Configini, json, xmlSettings overlap; review which wins
Dangerousesp, esm, dll, lua, wsCode/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 **/*.lsv recursively under the save directory (each campaign save is its own folder containing an .lsv, so recursion is required).
  • Categorises every detected save as manual and 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 fresh modsettings.lsx enabling those modules in order. If no .pak files are present, the existing file is left untouched.
  • The generated XML uses the standard ModuleSettings region with one ModuleShortDesc node per module, keyed on the Folder attribute.
  • 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 shapeWhat modde does
One or more .pak files at the archive rootInstalls as a single file set (the .paks land directly in Mods/)
A top-level Mods/ directory in the archiveStrips 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 3 does not affect the modded game. modde resolves the prefix path for you, but if you ever inspect things manually, look under compatdata/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/1086940 prefix may not have a Mods/ 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 Extender directory 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.lsx ordering 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

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

PropertyValue
game_idstardew-valley
Display nameStardew Valley
Engine familySmapi
LoaderSMAPI (Stardew Modding API)
Steam App ID413150
Nexus domainstardewvalley
Overall statusPartial
ScannerYes
Conflict detectionYes (generic policy classifier)
Save trackingDone

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.json marker file inside a candidate raises detection confidence to 0.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 as mods/<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:

ExtensionSeverity
dll, luaDangerous (critical)
json, xml, iniConfig
png, jpg, dds, tga, nifCosmetic

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):

ClassExtensions
Save-breakingdll, 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:

  1. SMAPI directory mod — if the extracted archive root contains a manifest.json, the archive is one SMAPI mod. It installs as a DirectoryMod (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.
  2. Mods/-rooted archive — if the extracted root instead contains a Mods/ directory, modde strips that content root (StripContentRoot { root: "Mods" }) so the inner mod folders stage directly into the game’s Mods/. This handles archives that bundle one or more mods already nested under a Mods/ 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 proton tool 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 standard StardewModdingAPI %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

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 each SubModule.xml for identity and dependencies.
  • Conflict detection — Yes, via the generic policy classifier plus Bannerlord-specific save-breaking and cosmetic classification.
  • Save trackingDone. 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.

CapabilityStatusNotes
Install detection (Steam)YesSteam app id 261550, install dir Mount & Blade II Bannerlord
Mod directoryYesModules/ under the install root
Filesystem scannerYesWalks Modules/, parses SubModule.xml
Dependency checkingYesReports <DependedModule> ids missing from the module set
Conflict classificationYesGeneric classifier + Bannerlord save-breaking / cosmetic policy
Save trackingDoneDocuments/Mount and Blade II Bannerlord/Game Saves/Native, *.sav
Load-order managementNot shippedUse the in-game Launcher’s Mod Options screen
OptiScaler profilesNot shippedNo 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.xml at its top level, modde treats the whole archive as a single module. It uses the module’s own Id.value attribute (from SubModule.xml) as the mod id when one is present.
  • Modules/-prefixed archives — if the archive instead contains a top-level Modules/ directory, modde strips that prefix and deploys its contents into the game’s Modules/ directory, so a mod packaged as Modules/CoolMod/... lands at Modules/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:

  1. Parses SubModule.xml for 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.
  2. Records every file under the module directory, relative to the install root, as the module’s footprint.
  3. 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:

ExtensionContent categorySave-breaking?
dllBinaryYes
xmlConfigYes
xsltConfigYes
xslConfigYes
pakArchiveYes
dds, png, tga, jpgTextureNo (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-level Modules/ 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.
  • .pak files 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’s drive_c/users/steamuser/Documents, but on a typical setup it surfaces at your real $HOME/Documents). modde’s save tracker reads the Game 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.
  • .NET module DLLs are save-breaking. Adding or removing a dll-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/modules directory case-insensitively, which matters on Linux’s case-sensitive filesystem when a mod ships an oddly-cased folder; the on-disk Modules/ 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

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 in docs/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.

CapabilityGeneric gameNotes
Mod deployment (virtual filesystem / overlay)YesMods deploy into the configured mod_dir (defaults to the install root).
Conflict detection & classificationYesUses the built-in generic_collision_classifier.
ProfilesYesStandard per-game profiles work.
Install detectionYesVia install_path_override, Steam install_dir_name, or Steam/Heroic launcher data.
Launcher integrationYessteam_app_id lets modde resolve the Steam launch target.
Executable management (modde exec / modde tool add-executable)YesNamed 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 overridesYesproxy_dlls are emitted as Wine DLL overrides, but only for DLLs that actually exist in the executable directory.
Nexus metadata side panelPartialSet 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 scannerNoscanner: None. modde scan has no game-specific heuristics to discover already-installed mods for a generic game.
Save tracking / save profilesNosave_tracker: None and supports_save_profiles: false. modde save save-profile features are unavailable.
Wabbajack list matchingNoGeneric games carry no wabbajack_names.
OptiScaler profilesOpt-inNone 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 .toml are considered;
  • files ending in .optiscaler.toml are skipped (those are OptiScaler profile sidecars, not game specs);
  • specs are loaded in sorted filename order;
  • each spec is validated, and any spec whose id collides 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
FieldTypeRequiredMeaning
idstringyesStable 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_namestringyesHuman-readable name. Must be non-empty (after trimming).
executable_dirpathyesDirectory 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_idstringnoSteam AppID (stored as a string; parsed to u32 when a launch target is resolved). Enables Steam launcher integration.
install_dir_namestringnoFolder name under steamapps/common/. modde scans every known Steam library for this folder when detecting the install.
install_path_overridepathnoAbsolute path to the install. When set and the directory exists, it short-circuits all other detection.
mod_dirpathnoDirectory 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_domainstringnoNexus game domain (the slug in a Nexus URL). Enables Nexus browse/search and the metadata side panel for this game.
proxy_dllsarray of stringsnoDLL 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:

  • id must match ^[a-z0-9][a-z0-9-]*$ (lowercase ASCII letters/digits and hyphens, starting with a letter or digit).
  • id must not collide with a built-in game ID.
  • executable_dir and mod_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_name must 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:

  1. install_path_override, if set and the directory exists.
  2. install_dir_name under any detected steamapps/common/ library.
  3. 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:

SubcommandPurpose
addCreate or overwrite a user-defined game spec.
listList configured user-defined games.
removeDelete a user-defined game spec (with confirmation).
showShow a resolved game registration (user-defined or built-in).
detectFind executable-bearing directories under an install path.
exportSerialize a game registration (any game) to a GameSpec TOML.
importInstall a GameSpec TOML from a file into the games directory.
import-profileInstall 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 / argRequiredMaps to GameSpec field
<id> (positional)yesid
--display-name <name>yesdisplay_name
--executable-dir <path>yesexecutable_dir
--steam-app-id <id>nosteam_app_id
--install-dir-name <name>noinstall_dir_name
--mod-dir <path>nomod_dir
--nexus-domain <domain>nonexus_domain
--proxy-dll <name>noone entry in proxy_dlls; repeat the flag to add more
--forcenooverwrite 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;
  • .exe matching 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.

PrioritySourceHow it is setBest for
1OAuth tokenOAuth2 PKCE flow, stored in the keyring (modde/nexus-oauth-token); used only while unexpired (60 s skew buffer)Future interactive desktop login
2modde config filemodde nexus auth writes ~/.config/modde/nexus_api_key (mode 0600)Single-user workstations
3NEXUS_API_KEYEnvironment variableCI, containers, one-off shells
4System keyringsecret-service over D-Bus, service modde, key nexus-api-keyDesktop sessions with an unlocked keyring
5NEXUS_API_KEY_FILEEnvironment variable pointing at a file whose contents are the keysops-nix / agenix / systemd credentials
6Legacy settings.tomlnexus_api_key field in modde’s settings fileBackward 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 auth config file) and priority 4 (the system keyring) are distinct. The current modde nexus auth flow writes the mode-0600 config 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 by modde nexus auth always takes precedence.

Recommendation per environment

EnvironmentRecommended sourceWhy
NixOS / home-managerNEXUS_API_KEY_FILE via programs.modde.nexus.apiKeyFileDeclarative, secret stays out of the Nix store, sops-nix/agenix compatible
Single-user desktopmodde nexus auth (config file)One command, restrictive permissions, survives reboots
CI / ephemeral containersNEXUS_API_KEY env varNo persistent state, easy to inject as a secret
Shared / multi-user hostNEXUS_API_KEY_FILE with per-user file permissionsAvoids 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.

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
CapabilityFree accountPremium account
modde nexus status / account validationYesYes
Browse / search / trending / updated feeds (REST + GraphQL)YesYes
Mod metadata, file listings, collection manifestsYesYes
Update checking (modde update check)YesYes
Endorse / track / untrack modsYesYes
Automated CDN download (install mod, update apply, nxm handle, collection installs)NoYes

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.

FeedGraphQL (preferred)REST fallback (v1)
Trendingbrowse_feed_gql(domain, Trending)GET /games/{domain}/mods/trending.json
Monthly topbrowse_feed_gql(domain, MonthlyTop)GET /games/{domain}/mods/updated.json?period=1m
Full-text searchsearch_mods_gql(domain, term, page)GET /games/{domain}/mods/search.json?search=…&page=…
Collections feed/searchcollections_feed_gql(domain, term)GET /games/{domain}/collections.json?search=…

Additional v1 REST endpoints back the rest of the integration:

PurposeEndpoint
Mod detailsGET /games/{domain}/mods/{id}.json
File listingGET /games/{domain}/mods/{id}/files.json
Recently updatedGET /games/{domain}/mods/updated.json?period={1d|1w|1m}
Collection by slugGET /collections/{slug}.json (game domain auto-discovered)
Collection revisionGET /games/{domain}/collections/{slug}/revisions/{rev}.json
Endorse / abstainPOST …/mods/{id}/endorse.json · …/abstain.json
Tracked modsGET/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_URL and MODDE_NEXUS_GRAPHQL_URL for 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 as Nexus API rate limit exceeded. Please wait before retrying. Respect the Retry-After header 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:

  1. Resolve the mod, pick the latest MAIN file, and generate a CDN download link (Premium-gated).
  2. Download the archive into the store, then extract into a temporary staging tree (not the store) for analysis.
  3. Hash the archive (xxhash64) and record it on the install plan.
  4. Analyze the staged tree to detect the install method (simple copy, FOMOD, BAIN, …).
  5. 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:

  1. DiscoverGET /collections/{slug}.json returns the collection’s game domain and its latest published revision number.
  2. Fetch the manifestGET /games/{domain}/collections/{slug}/revisions/{rev}.json returns 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 --mods checks 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. apply refuses to run on a locked profile and tells you to either pass --confirm-locked (acknowledging the drift) or run modde profile unlock <name> first. With --confirm-locked it 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 interactive y/N confirmation. --yes assumes “yes” to those per-mod prompts but still refuses any breaking update unless --accept-breaking is 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

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 source State describing 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 Statemodde directiveNotes
NexusDownloaderNexusResolved via the Nexus API (game domain + mod id + file id). Requires an API key.
WabbajackCDNDownloaderWabbajackCdnAuthored-files / generated-output archives hosted on Wabbajack’s CDN.
GitHubDownloaderGitHubRelease asset by user/repo, tag, and asset name.
GoogleDriveDownloaderGoogleDriveBy Drive file id.
MegaDownloaderMegaBy MEGA URL.
MediaFireDownloaderMediaFireBy MediaFire URL. The MediaFire download backend resolves the real file.
HttpDownloaderDirectURLPlain HTTP(S) with optional headers.
ModDBDownloaderDirectURLDirect URL plus a ModDB HTML mirror resolver that follows the /downloads/start/<id>/all mirror page.
ManualDownloaderManualA 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.

DirectiveWhat it does
FromArchiveExtract 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.
PatchedFromArchiveExtract 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.
InlineFileWrite a file whose bytes are stored inline in the .wabbajack (by SourceDataID). Used for small generated/config files.
RemappedInlineFileLike InlineFile, but with path-remapping applied by the author. modde treats it identically to InlineFile for placement and verifies its expected Hash.
CreateBSA / BA2 repackReconstruct 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).
GameFileSourceArchives 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 NexusDownloader archive (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]
FlagDefaultEffect
--profile <name>derived from listTarget profile to create/update.
--game-dir <path>auto-detectGame install to deploy into and to read GameFileSource files from.
--forceoffForce a full reinstall, skipping the preflight short-circuit (the cheap existence check that lets an already-staged list skip straight to deploy).
--no-deployoffStage 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-erroroffLog 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-stagingoffExplicitly discard existing staging before installing. By default modde resumes compatible staging and adopts incompatible-but-present staging without deleting it (see below).
--skip-validateoffSkip post-staging hash validation before deploy. Faster, but you forfeit the safety net.
--missing-archive-policy <p>failWhat to do when downloadable manual/Nexus archives are missing. One of fail, omit-files, omit-mods (see below).
--archive-retention <p>keepSource-archive retention after a batch is integrated: keep, prune-applied, or auto.
--diagnostics-dir <path>noneWrite apply diagnostics (heartbeat + per-batch JSONL) here for later analysis.
--diagnostics-interval <s>30Diagnostics heartbeat interval in seconds.
--stall-warn-seconds <s>600Warn when apply makes no batch/sentinel progress for this long.
--stall-abort-seconds <s>1800Abort when stalled this long and cgroup memory/swap are saturated.
--acquire-missingoffFrontload assisted acquisition of missing manual archives before applying.
--no-acquire-missingoffDisable the automatic frontloaded acquisition pass entirely.
--acquire-download-dir <path>data dirBrowser download directory to watch during assisted acquisition.
--acquire-include-nexusoffAlso attempt to acquire missing Nexus archives during the frontload pass.
--acquire-browser-controlleroffDrive controlled Chromium tabs (with a managed profile and auto-download prefs) for acquisition instead of just opening the system browser.
--acquire-timeout <s>900Per-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 downstream CreateBSA that 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.

CommandPurpose
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.json describing 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 (or assess’s adopt notice) is the only thing that deletes staging.
  • Per-archive and per-BSA sentinels — completed archive batches and CreateBSA outputs drop sentinel files so a resumed run skips finished work. assess reports archive sentinels: X/Y and BSA sentinels: X/Y so 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-zst suffix. 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 _state directory. The size threshold and compression level are tunable with the MODDE_ZSTD_MIN_BYTES and MODDE_ZSTD_LEVEL environment 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, and PatchedFromArchive carry expected output hashes and are fully verified; FromArchive outputs 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 .wabbajack file itself fetched declaratively, recompute the Nix hash with nix store prefetch-file <url> (or nix-prefetch-url) and paste the reported sha256-… into wabbajackList.hash. A hash mismatch during 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-impact to identify it by Wabbajack hash, obtain the exact file (correct mod/file id on Nexus, or the original manual source), and import-archive it. 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-staging for 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) use modde wabbajack analyze-diagnostics <dir> to see the last phase, max idle time, peak memory/swap, and the slowest batches. A bottleneck: archive trust/download verification wall before extraction line means the time is going into hashing, not a true hang.
  • --stall-warn-seconds surfaces a warning when no batch/sentinel progress happens for a while; --stall-abort-seconds aborts 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 (or prune-applied) to free integrated archives, and lower MODDE_ZSTD_LEVEL to 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

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:

  1. resolve — turn a download directive into a concrete DownloadHandle (a final URL, optional mirror candidates, headers, an expected hash, and a size hint).
  2. 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

BackendDirectiveAuthResumeNotable behaviour
NexusNexus { game, mod, file, hash }API key + PremiumNoCDN link via download_link.json
GitHubGitHub { user, repo, tag, asset, hash }optional GITHUB_TOKENNo (retry)Resolves a release asset by name
DirectDirectURL { url, headers, mirror_resolver, hash }per-directive headersYes (Range)Mirror fallback + range resume
Google DriveGoogleDrive { id, hash }noneNoHandles the virus-scan interstitial
MEGAMega { url, hash }noneNoClient-side AES-128-CTR decrypt
MediaFireMediaFire { url, hash }noneYes (via Direct)HTML-scrapes the direct link
ManualManual { url, prompt, … }n/an/aFails fast with guidance
Wabbajack CDNWabbajackCdn { url, hash }noneNoWabbajack-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 a 206 Partial Content it 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 (returns 200), 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-supplied User-Agent is 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=t host, 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=TOKEN query parameter, a name="confirm" value="TOKEN" hidden input, and the id="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#KEY and the legacy https://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.zipmod_file.zip.meta). The sidecar records:

  • url, expected_hash, bytes_downloaded, total_bytes, and a status string (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 next Queued task to Active, but only while the active count is below the limit — this is how concurrency is bounded.
  • pause(id) / resume(id) move a task to Paused and back to Queued; cancel(id) removes it.
  • save_sidecars() / load_from_sidecars(dir, limit) persist and rebuild the queue from .meta files (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.

VariableDefaultEffectWhere it applies
MODDE_BYTE_CACHE_MIB512Size (MiB) of the in-memory LRU cache of extracted archive entriesWabbajack extraction (Partial)
MODDE_ZSTD_MIN_BYTES1048576 (1 MiB)Minimum file size before a staged file is zstd-compressedWabbajack staging (Partial)
MODDE_ZSTD_LEVEL9zstd compression level for staged files (clamped to 1–22)Wabbajack staging (Partial)
MODDE_ARCHIVE_RETENTIONkeepWhat to do with source archives after a batch is applied: keep, prune-applied (aliases prune/delete), or autoWabbajack install (Partial)

Related authentication / transport variables documented elsewhere:

VariablePurposeSee
GITHUB_TOKENAuthenticated GitHub release downloadsGitHub Releases
NEXUS_API_KEY / NEXUS_API_KEY_FILENexus credentialsNexus 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 Done vs Partial

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 the InstallMethod variants 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_files list, filled in by the next stage.

Detection is ordered, and the order is deliberate so that authoritative, game-specific rules win before generic guesses:

  1. 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.
  2. Game pluginGamePlugin::analyze_mod_archive gets first crack. A game can claim a layout it knows (Cyberpunk’s REDmod, an ENB pack for Bethesda) before any generic probe runs.
  3. FOMOD — presence of fomod/ModuleConfig.xml.
  4. BAIN — numbered option subdirectories (00 Core, 01 Option, …).
  5. DLL overlay — a top-level .dll with no nested asset directories.
  6. Bare extract — the game plugin’s recognizes_bare_layout recognizer.
  7. User-config overlay — the plugin advertised a UserConfig deploy target and every file in the tree looks like a config file.
  8. 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:

StatusMeaning
installedFiles are staged and tracked.
pending_user_inputDetection succeeded but execution needs answers (FOMOD wizard, BAIN picker). The archive sits extracted in a staging dir.
unknownDetection failed; a dossier was written. Retry is blocked until the installer is extended.
failedExecution 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).

MethodWhat triggers it
BareExtractGame 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.espFoo.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.
SingleFileSetOne 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, or modde fomod apply). Until a config is present the mod sits pending_user_input. See the FOMOD guide.
  • BAIN — auto-detected by the numbered-subdir convention, but modde cannot know which options you want; selected_subdirs starts empty and the GUI prompts you to pick. Without a selection, execution returns RequiresUserInput.

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:

  1. Read archive_tree.txt and file_samples/ to understand the layout.
  2. Decide where the fix belongs — a generic package format gets a new InstallMethod variant plus a detection branch in analyze.rs; a game-specific quirk extends that game’s analyze_mod_archive impl in crates/modde-games/.
  3. Extend execute.rs if the new variant needs custom staging.
  4. Add a unit test built from the dossier’s file samples.
  5. Run the installer test suite.
  6. Rename the dossier directory with a .resolved suffix 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

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:

FileRequiredPurpose
fomod/ModuleConfig.xmlyesThe installer script: steps, groups, options, conditions, file operations.
fomod/info.xmlnoDisplay 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 typeSelection 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 — Required and Recommended options 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:

--formatNotes
tomlDefault. The format --fomod-config and fomod apply accept directly.
jsonEquivalent content; accepted by fomod apply when the config path ends in a non-.toml extension.
nixRequires 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 full install pipeline handle it (its analyzer strips single-directory wrappers automatically).
  • The mod is simply not a FOMOD. Run modde fomod inspect to 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 generate against 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 SelectExactlyOne group). 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

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:

  1. 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.
  2. 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

SituationUse
Imported a manual / hand-placed install, no manifestFilesystem scanmodde scan --game <id>
You have the original .wabbajack for a deployed listManifest matching — add --manifest <list>.wabbajack
Both: a Wabbajack base plus your own extra modsBoth 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:

  1. Parses the .wabbajack manifest.
  2. Groups its FromArchive / PatchedFromArchive directives by source archive and checks what fraction of each archive’s To paths exist on disk.
  3. Imports matched archives as mods, carrying their Nexus mod/file id and game domain when the archive is NexusDownloader-sourced.
  4. 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_hash so 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:

  1. Dry-run first. modde scan --game <id> [--manifest …] --dry-run and 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.
  2. Tune --threshold if 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.
  3. Import with --import-to <profile> once the proposal looks right.
  4. Dedup if you imported into a profile that had prior filesystem-scan rows: add --prune-duplicates (or run modde profile dedup … --apply).
  5. 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.
  6. 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

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:

  1. An ordered mod set — the list of EnabledMod entries (each with an enabled toggle, 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.
  2. A load order — the mod ordering above, plus an independent plugin order (.esp/.esm/.esl) for engines that distinguish the two, plus declarative load_order_rules (load_after / load_before / incompatible).
  3. Save assignments — a branch in the game’s git-backed save vault, swapped automatically whenever the active profile changes. See Save management.
  4. Deployment state — the overrides directory 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:

  1. Captures the current profile’s saves into its vault branch (with a mod fingerprint embedded for later compatibility warnings).
  2. Restores the target profile’s saves from its branch.
  3. 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:

SourceLockReasonProvenance recorded
Wabbajack modlistWabbajackmanifest_hash (verifies which manifest)
Nexus CollectionNexusCollectionslug + version
TOML import (modde import)TomlImportsource_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:

OrderConditionRefusal (ReorderError)
1The whole profile is lockedProfileLocked { reason }
2The target mod_id isn’t in the profileModNotFound { mod_id }
3The target mod itself carries a pinModPinned { mod_id, reason }
4The move would run off the top/bottom of the listAtBoundary
5The swap partner (the neighbor one step over) is pinnedAdjacentPinned { 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:

CommandStack (bottom → top)ActiveDepth
(start)[]A0
try B[A]B1
try C[A, B]C2
rollback[A]B1
rollback[]A0
try B[A]B1
commit[]B0

The two important rules:

  • rollback from depth 0 is an error (NotInExperiment). There is nothing on the stack to pop back to — you are not in an experiment.
  • commit requires depth ≥ 1. It accepts the current active profile and throws away the rollback history; it does not revert anything. In the table above, the final commit leaves 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 commit is harmless — you simply stay “in an experiment” with a non-zero depth. The only consequence is that rollback remains 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; their category_id is 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 --unlock to 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

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:

ConceptWhat it ordersWho resolves it
Mod install priorityWhich file wins when two mods ship the same pathThe VFS — later mod in the list overrides earlier
Plugin load orderThe order Bethesda .esp/.esm/.esl plugins loadLOOT + 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 detailpath -> 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:

SeverityDisplayMeaningTypical extensions
CosmeticCOSMETICVisual/audio only; low riskdds, nif, png, wav, hkx, bsa
ConfigCONFIGMay change behaviourini, cfg, json, toml, xml
DangerousDANGEROUSScripts/plugins/DLLs; crashes or save corruptionesp, esm, esl, dll, pex, psc
UnknownUNKNOWNExtension not in the game’s tableanything 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 familyArchive extensionsNotable Dangerous extensions
Bethesda (Skyrim, Fallout, Starfield)bsa, ba2 (indexed)esp, esm, esl, dll, pex, psc
Cyberpunk 2077archive (not indexed)reds, lua, tweak, xl, dll, yaml
UE4/UE5 titlespak, ucas, utocpak, 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/.ba2 are read and their contents indexed.
  • Cyberpunk .archive is 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-hides turns 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 .pex script collides, which is high-risk, so verify this is intentional.
  • [archive > loose] on clouds.dds means CustomSky won via a packed archive entry over a loose file in HiResTextures.
  • (hidden) on stars.dds means the loser was explicitly hidden by you.
  • OldTexturePack is 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:

  • after and requires both become load-after rules (a requires whose target is not in the active set is logged as a dependency gap).
  • incompatible becomes 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 (version 0.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 for skyrim-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

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:

InputMeaning
profile_nameSelects the staging directory location
resolvedThe mods in final priority order (first = lowest priority)
mod_filesPer-mod (relative_path, store_source_path) listings
overridesOptional profile-level override files (always win)
hiddenOptional (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:

  1. Profile overrides — files in the profile’s overrides directory are layered last and beat every mod.
  2. Later mods in the load order — install priority, last-wins.
  3. 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/.esl ordering is a separate concern handled by LOOT and plugins.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.

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 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:

StrategyWhen it appliesCost
HardlinkSource and destination on the same filesystemFree; shares inode/blocks
ReflinkHardlink crossed a filesystem (EXDEV) but the FS supports copy-on-write (btrfs, XFS, ZFS)Free until written; CoW
CopyNeither of the above workedFull 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 staging exists, it is renamed aside to staging.old, staging.bak is renamed into place as staging, and staging.old is removed.
  • If no current staging exists, staging.bak is 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
  • Playingmodde play and 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:

  1. Switch to the requested profile (swapping saves automatically).
  2. Deploy the profile’s mods (build the symlink farm and wire up tools).
  3. Launch the game through whichever launcher owns it (Steam or Heroic).
  4. 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.

FlagEffect
--no-switchSkip the profile switch; deploy and launch the already-active profile.
--no-deploySkip deployment; switch and launch without rebuilding the symlink farm (use when nothing changed since the last deploy).
--no-captureSkip 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:

  1. 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.

  2. 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

SourceHow it’s detected
SteamReads 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 / GOGParses gog_store/installed.json and matches GOG app IDs.
Heroic / EpicParses legendary_store/installed.json and matches Epic/Legendary app IDs.
Heroic / SideloadParses 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>.json under enviromentOptions, merging with any existing WINEDLLOVERRIDES so it doesn’t clobber your settings. It also inserts modde’s launch wrapper into Heroic’s wrapperOptions chain (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 WINEDLLOVERRIDES value 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:

  1. Confirm detection. Run modde detect. If the game isn’t listed, modde didn’t find an install for it.
  2. 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.
  3. For Steam: make sure the app appears in a Steam library folder with a valid appmanifest_*.acf and that the install directory under steamapps/common/ actually exists. modde skips manifests whose install path is missing.
  4. For Heroic: make sure the game shows as installed in Heroic — the installed.json for its store must list it with an install_path that exists on disk. Heroic must be installed as a Flatpak (com.heroicgameslauncher.hgl) or be on your PATH for modde to launch it; if neither is found you’ll see Heroic Games Launcher not found (checked flatpak and PATH).
  5. 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.
  6. Launch manually, capture manually. As a fallback you can always start the game from Steam/Heroic yourself after a modde play ... --no-capture (or just modde deploy), then capture saves with modde save auto-capture --game <id> or a running modde 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 named My Skyrim becomes the branch My-Skyrim.
  • main is the root. When a vault is first created, modde makes an empty init save vault commit on main. 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:

  1. Take every mod in the profile that is enabled and classified save-breaking. Disabled mods and cosmetic mods are ignored entirely.
  2. Collect their mod IDs, sort them, and de-duplicate.
  3. Feed each ID (followed by a \0 separator) into SHA-256.
  4. 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 switchprofile switch, profile try, and profile rollback capture 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-capture when 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:

ResultMeaning
CompatibleFingerprints match — the same save-breaking mods are present. Safe to restore.
No fingerprintThe snapshot predates fingerprinting (no trailer). Restore at your own risk.
MismatchThe 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:

  1. Captures the outgoing profile’s saves into its vault branch (the git history is the authoritative record).
  2. 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.
  3. Leaves the steam_autocloud.vdf marker (and the .modde/ directory) untouched.
  4. 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:

  1. Capture the current profile’s live saves into its vault branch, embedding the current fingerprint in the commit.
  2. Park the current profile’s root saves under .modde/profiles/<current>/, preserving Steam Cloud metadata.
  3. Ensure the new profile’s branch exists (creating it if needed).
  4. 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

CommandPurpose
save adopt --game G --profile PImport 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 PList assigned saves
save scan --game GFind unassigned saves in the game directory

See also

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:

MechanismWhat it doesTools that use it
Environment variableExported into the game’s launch environmentMangoHud, vkBasalt, OptiScaler, Proton
Wrapper commandChained before the game executableGameMode
Generated config fileWritten under ~/.local/share/modde/tools/<game_id>/MangoHud, vkBasalt
File patch (apply/revert)DLLs / shaders copied into the executable directoryReShade, OptiScaler
Wine DLL overrideForces Wine to load the native proxy DLLReShade, OptiScaler, Proton

Supported tools

Tool IDCategoryDescriptionLinux only
mangohudOverlayPerformance HUD (FPS, frame timing, CPU/GPU telemetry)Yes
vkbasaltPost-ProcessingVulkan post-processing layer (CAS sharpening, FXAA, shaders)Yes
gamemodePerformanceFeral GameMode wrapper for system performance tuningYes
reshadePost-ProcessingReShade shader injection via proxy DLLNo (Wine/Proton)
optiscalerUpscalerDLSS/FSR/XeSS upscaling and frame-generation replacementNo (Wine/Proton)
protonPerformancePer-game Proton, prefix, environment, and DLL-override settingsNo

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:

KeySectionTypeNotes
positionLayoutselecttop-leftbottom-right
fps_limit / fps_limit_methodFPStext / late|earlyframe-rate cap and limiter mode
background_alphaLayoutnumber 0–1HUD background opacity
cpu_temp, gpu_temp, gpu_junction_tempCPU / GPUboolper-sensor toggles
custom_text_centerLayouttextfree-form HUD text
gamemode, vkbasaltCompatibilityboolshow whether those layers are active
output_folder, log_duration, upload_logsLoggingpath / number / boolbenchmark 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

KeySectionTypeDefaultNotes
toggleKeyActivationtextHomekey that toggles vkBasalt at runtime
enableOnLaunchActivationbooltruestart with effects already active
effectsEffectslist["cas"]colon/comma-joined list, e.g. cas:fxaa
casSharpnessEffectsnumber 0–10.4CAS strength
reshadeTexturePathReShade Pathspathoptional ReShade texture dir
reshadeIncludePathReShade Pathspathoptional 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

KeySectionTypeNotes
source_dirSourcepathDirectory holding ReShade DLLs, ReShade.ini, and shader folders. Required for apply.
dll_nameDeploymentselectdxgi.dll (default), d3d11.dll, or dinput8.dll
derived_executable_dirDetected Gameread-onlyWhere 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:

  1. the selected proxy DLL (dll_name) from source_dir,
  2. ReShade.ini if present, and
  3. the reshade-shaders/ and reshade-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_modeUI labelMeaning
github_releaseOfficial GitHub releasesReleases from optiscaler/OptiScaler (encoded as official:<tag>)
goverlay_buildsGOverlay buildsThe external benjamimgois/OptiScaler-builds feed, with a goverlay_channel selector
goverlay_fgmodGOverlay fgmod directoryAn existing local ~/.local/share/goverlay/fgmod install (the default)
local_dirLocal OptiScaler directoryA 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

KeyTypeNotes
proxy_dllselectDLL 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_overridestextExtra Wine DLL override base names (comma/space separated)
copy_companion_filesbool (default on)Copy fakenvapi.dll, nvngx-wrapper.dll, and other DLLs found beside OptiScaler
fsr4_variantselectlatest_fp8 (“Latest (FP8)”) or int8_402 (“4.0.2c (INT8)”) — copied as amd_fidelityfx_upscaler_dx12.dll
emulate_fp8boolOnly meaningful for the FP8 variant; exports DXIL_SPIRV_CONFIG=wmma_rdna3_workaround
enable_optipatcherboolDeploy plugins/OptiPatcher.asi to unlock DLSS/DLSS-FG inputs without whole-game spoofing
spoof_dlssboolFallback 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

KeyTypeNotes
version_modeselectlauncher_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_versionselectlatest or a specific GE-Proton tag; the option list merges the GE-Proton catalog with locally installed compatibility tools
install_targetselectTarget 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

KeyTypeNotes
prefix_path_overridepathExports WINEPREFIX; blank uses launcher detection
extra_envtextExtra KEY=VALUE lines (one per line; # comments ignored) exported at launch
dll_override_modeselectauto, forced, or off — how Proton contributes forced DLL overrides
forced_dll_overridestextComma/space-separated DLL base names, e.g. dxgi, winmm
wrapper_orderselectafter-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:

SettingExports
steamdeckSteamDeck=1
proton_enable_hdrPROTON_ENABLE_HDR=1
enable_hdr_wsiENABLE_HDR_WSI=1
proton_enable_waylandPROTON_ENABLE_WAYLAND=1
proton_logPROTON_LOG=1
radv_perftest_rtRADV_PERFTEST=rt,emulate_rt
proton_enable_nvapiPROTON_ENABLE_NVAPI=1
mesa_loader_zinkMESA_LOADER_DRIVER_OVERRIDE=zink
proton_fsr4_upgradePROTON_FSR4_UPGRADE=1
proton_dlss_upgradePROTON_DLSS_UPGRADE=1
proton_xess_upgradePROTON_XESS_UPGRADE=1
proton_use_wow64PROTON_USE_WOW64=1
proton_no_ntsyncPROTON_NO_NTSYNC=1
enable_mesa_antilagENABLE_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

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 addmodde tool add-executableSave or update a named target
modde exec listmodde tool list-executablesList configured targets for a game
modde exec removemodde tool remove-executableDelete a target
modde exec runmodde tool run-executableRun 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:

FieldCLIRequiredDefaultNotes
Namepositional <name>yesDisplay name, e.g. xEdit. Unique per game. Cannot be blank.
Executablepositional <executable>yesPath to the program to run
Game--game <id>yesMust be a supported game id
Working directory--working-dir <dir>nodetected game install dirThe 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>nononeExported as WINEDLLOVERRIDES, e.g. dinput8=n,b;winmm=n,b
Environment--env KEY=VALUE (repeatable)nononeEach must be KEY=VALUE with a non-empty key
Default args-- ARGS...nononeEverything 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:

  1. Snapshot the game’s mod directory (the deployed VFS tree) before the tool runs.
  2. Run the tool with the configured working directory (defaulting to the detected install dir), saved --env variables, WINEDLLOVERRIDES (if set), and the saved arguments plus any you appended.
  3. Snapshot again after the tool exits and diff against the before-snapshot.
  4. 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

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:

PurposeLinuxmacOSWindows
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.db is 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’s installed_mod_files manifest 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>.wabbajack caches manifest files keyed by their hash, so re-installs and dedup runs 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_DIR override 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_dir for 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_data resolves to that instance’s data_dir for every command — unless you override it with MODDE_DATA_DIR / --data-dir for 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-64 content 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.toml file inside the snapshot and into the stock_snapshots table in modde.db.
  • modde stock verify recomputes the tree hash and compares it against the recorded one. A match prints OK; a mismatch prints MISMATCH and 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.db is 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-run modde stock snapshot on each machine instead.

See also

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

SymptomSectionRelated guide
modde detect doesn’t list my gameGame not detectedPlaying
Generic game registered but install not foundGeneric game not detectedGeneric games
modde game detect finds nothing / wrong dirGeneric game not detectedGeneric games
Wabbajack list wants local game filesWabbajack install issuesWabbajack
Home Manager says profile is “awaiting game install”Wabbajack install issuesWabbajack
Wabbajack URL/hash mismatchWabbajack install issuesWabbajack
modde reports unavailable authored filesWabbajack install issuesWabbajack
Nexus auth fails / CDN download refusedNexus API authentication failsNexus
Deployment fails with symlink errorsDeployment failsDeployment
Game left in a broken state after deployDeployment failsDeployment
modde tool apply says “could not detect install dir”Tool apply / revert failuresTools
modde tool apply says “No files to apply”Tool apply / revert failuresTools
modde tool revert says “No applied files to revert”Tool apply / revert failuresTools
OptiScaler: game crashes on first bootOptiScaler first-boot crashTools
Save vault wants “adoption”Save vault issuesSaves
save restore warns about fingerprint mismatchSave vault issuesSaves
Can’t reorder mods (profile locked)Profile is lockedProfiles
Mod stuck on “pending user input”FOMOD install stuckFOMOD
“Unknown install type” / dossier writtenUnknown install typeMod installation
Extraction fails (7z / unrar missing)Missing extractorsMod installation
Database looks corruptedDatabase issuesData management
cargo build/test fails in openssl-sysBuild / openssl-sys failuresInstallation

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_*.acf file exists in your Steam library (including extra libraries listed in libraryfolders.vdf).
  • For Heroic, check that the game appears in ~/.config/heroic/GamesConfig/.
  • Use --game-dir flags to specify the path manually on commands that accept it (for example modde scan --game <id> --game-dir <path>), or set gameDir in 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_overrideinstall_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:

  1. Find the real executable directory. modde game detect walks 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 your executable_dir. If it reports No executable-bearing directories found under <path>, the path is wrong — pass the actual install root.

  2. Inspect and correct the spec:

    modde game show <id>      # full resolved spec + source path
    modde game list           # all user-defined games and their files
    
  3. Re-register with the right detection fields (use --force to overwrite):

    modde game add <id> \
      --display-name "<name>" \
      --executable-dir "Binaries/Win64" \
      --steam-app-id 998877 \
      --install-dir-name "Voidrunner" \
      --force
    

    install_path_override is not settable from add. If detection still fails (non-Steam install, unusual layout), edit ~/.local/share/modde/games/<id>.toml by hand to add an absolute install_path_override = "/abs/path", or modde game import a 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, and modde save save-profile features are unavailable — generic games ship scanner: None and save_tracker: None. That is the boundary of the Partial status, 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.

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:

MessageCauseFix
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-blade
    

    This writes dxgi.dll and companion files into the game’s Binaries/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:

  1. Open the GUI (modde gui) and complete the wizard, or
  2. 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:

  1. Check if a .db-journal or .db-wal file exists (SQLite recovery files).
  2. Back up the database before attempting repairs.
  3. 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

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: null or path
  • 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: attrsOf profile 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, or str
  • 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"
ValueBehavior
"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: null or submodule
  • Default: null
OptionTypeDefaultDescription
urlnull or strnullURL to the .wabbajack modlist file
hashnull or strnullSHA-256 hash of the modlist file (paired with url)
pathnull, path, or strnullLocal or Nix store path to an already-available .wabbajack
missingArchivePolicyenum 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:

PolicyEffect
"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.

OptionTypeDefaultDescription
hashnull or strnullWabbajack xxh64 hex hash; required when the key is a readable label, not a hash
pathnull, path, or strnullPath to the exact source archive for this archive hash
optionalboolfalseAllow 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: null or submodule
  • Default: null
OptionTypeRequiredDescription
slugstryesNexus Collection slug
versionstryesCollection 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: null or per-tool submodule
  • Default: null (for each known tool ID)

Recognized tool IDs:

Tool IDDescriptionsettings shape
mangohudPerformance HUD overlayfree-form
vkbasaltVulkan post-processingtyped
gamemodeSystem performance tuningtyped
reshadeD3D / OpenGL post-processing for Wine-backed gamestyped
optiscalerDLSS / FSR / XeSS upscalingfree-form
protonProton runtime selection and DLL overridesfree-form

Common options on every tool submodule:

OptionTypeDefaultDescription
enableboolfalseWhether the tool is enabled for this profile
applyOnActivationboolfalseRe-run modde tool apply after configuration during activation
settingstyped or free-form attrsetsee belowTool-specific settings (schema depends on the tool)
releasenull or release submodulenullPinned release asset (only valid for release-backed tools)
profilenull or str/enumnullOptiScaler 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.

OptionTypeDefaultDescription
tagstrRelease tag to pin (required)
assetstrRelease asset file name (required)
urlnull or strnullDownload URL for the pinned asset
hashnull or strnullFixed-output hash for the pinned asset (paired with url)
pathnull, path, or strnullLocal / 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, or enum of 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.

AssertionRule
Exclusive sourcewabbajackList and nexusCollection are mutually exclusive — set at most one
Wabbajack sourcewabbajackList must set exactly one of: path, or url + hash together
Wabbajack url/hash pairingwabbajackList.url and wabbajackList.hash must be set together (neither alone)
Required manual archivesEach manualArchives entry must set path, or mark optional = true
Manual-archive hashesA manualArchives entry keyed by a readable label (not a 16-hex-char hash) must set hash
Duplicate manual-archive hashesmanualArchives entries must not resolve to duplicate hashes
Tool release supportrelease may only be set on a tool that supports release pinning (today: optiscaler only)
Tool release sourcerelease.path is mutually exclusive with release.url + release.hash; exactly one source must be present
Tool release url/hash pairingrelease.url and release.hash must be set together
OptiScaler profile registeredtools.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)

KeyTypeDescription
casSharpnessfloat (0 – 1, step 0.05)Contrast Adaptive Sharpening amount
effectstextColon/comma-separated effect list, such as cas or cas:fxaa
enableOnLaunchboolStart with vkBasalt effects enabled
reshadeIncludePathpathOptional ReShade shader include directory for vkBasalt
reshadeTexturePathpathOptional ReShade texture directory for vkBasalt
toggleKeytextKey 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)

KeyTypeDescription
dll_nameenum: dxgi.dll, d3d11.dll, dinput8.dllDLL name copied into the executable directory
source_dirpathDirectory 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:

KeyType / valuesDescription
copy_companion_filesboolCopy fakenvapi, nvngx wrapper, and other DLLs found next to OptiScaler
dll_overridestextComma/whitespace-separated Wine DLL override base names
emulate_fp8boolSet DXIL_SPIRV_CONFIG=wmma_rdna3_workaround for the Latest (FP8) FSR4 variant
enable_optipatcherboolUse OptiPatcher to unlock DLSS / DLSS frame-gen inputs without whole-game spoofing
fsr4_variantenum: latest_fp8, int8_402FSR4 payload copied as amd_fidelityfx_upscaler_dx12.dll
ini_overrides.FSR.FGIndexenum: auto, 0, 1OptiScaler [FSR] FGIndex override
ini_overrides.FSR.UpscalerIndexenum: auto, 0, 1, 2OptiScaler [FSR] UpscalerIndex override
ini_overrides.Menu.Scalefloat (0.5 – 2, step 0.1)OptiScaler [Menu] Scale override
ini_overrides.Menu.ShortcutKeyenum: auto, INSERT, HOME, END, DELETE, BACKQUOTE, F1F12OptiScaler [Menu] ShortcutKey override
ini_overrides.NvApi.OverrideNvapiDlltri-state boolOptiScaler OverrideNvapiDll override
ini_overrides.fakenvapi.enable_trace_logstri-state boolfakenvapi enable_trace_logs override
ini_overrides.fakenvapi.force_latencyflextri-state boolfakenvapi force_latencyflex override
ini_overrides.fakenvapi.force_reflexenum: 0, 1, 2fakenvapi force_reflex override
ini_overrides.fakenvapi.latencyflex_modeenum: 0, 1, 2fakenvapi latencyflex_mode override
proxy_dllenum: dxgi.dll, version.dll, dbghelp.dll, d3d12.dll, wininet.dll, winhttp.dll, winmm.dll, nvngx.dll, OptiScaler.asiDLL name used to load OptiScaler
source_modeenum: github_release, goverlay_builds, goverlay_fgmod, local_dirWhere modde should get OptiScaler files from
spoof_dlssboolFallback 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:

KeyType / valuesDescription
version_modeenum: launcher_default, installed_version, install_with_protonup_rsHow modde chooses the Proton runner
selected_versionenum: latestInstalled or requested GEProton version
install_targetenum: steamTarget application passed to protonup-rs
dll_override_modeenum: auto, forced, offHow Proton contributes forced DLL overrides
forced_dll_overridestextComma/whitespace-separated DLL base names (e.g. dxgi, winmm)
extra_envtextAdditional KEY=VALUE lines exported at launch
prefix_path_overridepathOptional Proton/Wine prefix override
wrapper_orderenum: after-modde, before-toolsWhere 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 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/:

OSConfig dirFull 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

KeyTypeDefaultDescription
nexus_api_keystring""Legacy inline Nexus API key (lowest-precedence key source — prefer the keyring or nexus auth)
game_pathsarray of { game_id, path }[]Configured game install paths (typically 1–4 games)
download_dirpath (optional)unsetOverride for the downloads directory
themestring""UI theme name (e.g. Nord); empty means the built-in default
selected_gamestring (optional)unsetThe game last selected in the UI
update_check.enabledbooltrueWhether 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:

OSData 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:

PathPurpose
<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.dbSQLite 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:

VariableEffect
MODDE_DATA_DIROverride the data directory. Equivalent to the global --data-dir flag (the flag wins if both set)
NEXUS_API_KEYNexus API key, read directly from the environment (see precedence)
NEXUS_API_KEY_FILEPath to a file containing the Nexus API key — sops-nix / agenix friendly; set by the home-manager module
MODDE_NO_UPDATE_CHECKWhen set to 1/true/yes/on, disables the startup update check regardless of settings.toml
MODDE_UPDATE_CHECK_URLOverride the release endpoint queried by the update check (defaults to the Codeberg releases API)
XDG_CONFIG_HOMELinux: relocates the config dir (and therefore settings.toml and instances.toml)
XDG_DATA_HOMELinux: relocates the data dir base
XDG_CACHE_HOMELinux: 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:

FieldTypeDescription
activestring (optional)Name of the currently active instance
instancesarray of { name, data_dir, is_default }Registered instances
instances[].namestringUnique instance name
instances[].data_dirpathThe instance’s self-contained data directory
instances[].is_defaultboolWhether 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):

  1. Explicit override — the global --data-dir flag or MODDE_DATA_DIR environment variable (the flag takes precedence over the env var). This also wins over an in-process override set at startup.
  2. Active instance — the data_dir of the active instance in instances.toml, if one is set.
  3. Default<data_dir>/modde/, where <data_dir> honors XDG_DATA_HOME on 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):

  1. OAuth token — a non-expired token from modde nexus auth OAuth login.
  2. modde config file<config_dir>/modde/nexus_api_key, written by modde nexus auth (created with 0600 permissions on Unix).
  3. NEXUS_API_KEY — the environment variable, read directly.
  4. System keyring — the secret-service / D-Bus keyring entry (service = "modde", key = "nexus-api-key").
  5. 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’s nexus.apiKeyFile exports.
  6. Legacy settings.toml — the nexus_api_key key, 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_FILE when you set nexus.apiKeyFile, but it does not write settings.toml keys like theme, download_dir, or selected_game.
  • settings.toml owns 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_DIR for the data dir, NEXUS_API_KEY / NEXUS_API_KEY_FILE for the key, MODDE_NO_UPDATE_CHECK for the update check. They take effect regardless of settings.toml.
  • The system keyring is the recommended interactive store for the Nexus key (set it with modde nexus auth); it outranks NEXUS_API_KEY_FILE and the legacy settings.toml key, but is itself outranked by NEXUS_API_KEY and 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

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).

FlagEnvironmentDescription
--data-dir <path>MODDE_DATA_DIROverride the data directory for a single invocation (default: ~/.local/share/modde)
--heap-profile <path>MODDE_HEAP_PROFILEFeature-gated. Write a DHAT heap profile. Requires building with --features heap-profile; errors out otherwise
--debug-panicFeature-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.

VariableDefaultPurpose
MODDE_DATA_DIR~/.local/share/moddeData directory (DB, store, downloads, backups). Equivalent to --data-dir.
NEXUS_API_KEYNexus Mods API key, used when no modde-owned config-file key is present.
NEXUS_API_KEY_FILEPath to a file containing the Nexus API key (read after the keyring is consulted).
GITHUB_TOKENBearer token for the GitHub API; raises rate limits for release-backed tools (reshade, optiscaler).
RUST_LOGtracing/env_logger-style filter controlling log verbosity (e.g. RUST_LOG=modde=debug).
MODDE_BYTE_CACHE_MIB512Size (MiB) of the in-memory LRU byte cache used by the download/source layer.
MODDE_ZSTD_MIN_BYTES1048576Minimum file size (bytes) before Wabbajack staging entries are zstd-compressed. Default 1 MiB.
MODDE_ZSTD_LEVEL9zstd compression level for staging (clamped to the 1–22 range).
MODDE_ARCHIVE_RETENTIONkeepDefault 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_PROFILESame 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:

  1. The modde-owned config file (written by modde nexus auth).
  2. NEXUS_API_KEY.
  3. The system keyring.
  4. NEXUS_API_KEY_FILE.
  5. 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>]
FlagDescription
--gameFilter 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]
FlagDescription
--unlockStrip 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]
FlagDescription
--manifestPath to a .wabbajack file as the authoritative reference
--applyActually 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]
FlagDescription
--no-deploySkip mod deployment
--no-switchSkip profile switch
--no-captureSkip 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...]
FlagDefaultDescription
--profileTarget profile (created if it doesn’t exist)
--game-dirGame installation directory to deploy mods into
--forcefalseForce full reinstall, skipping preflight checks
--no-deployfalseStage into modde’s data dir but skip the final copy into --game-dir (Stock-Game lists)
--continue-on-errorfalseLog per-archive failures instead of aborting; install proceeds as far as possible
--reset-stagingfalseExplicitly discard existing Wabbajack staging before installing
--skip-validatefalseSkip staging validation before deploy
--diagnostics-dirWrite apply diagnostics JSONL to this directory (consumed by wabbajack analyze-diagnostics)
--diagnostics-interval30Diagnostics heartbeat interval in seconds
--stall-warn-seconds600Warn when apply makes no batch/sentinel progress for this many seconds
--stall-abort-seconds1800Abort when stalled this long and cgroup memory/swap are saturated
--archive-retentionkeepSource-archive retention after successful integration: keep, prune-applied, or auto
--missing-archive-policyfailBehavior for missing optional manual/Nexus archives: fail, omit-files, or omit-mods
--acquire-missingfalseFrontload assisted manual-archive acquisition before applying
--acquire-download-dirBrowser download directory to watch during assisted acquisition
--acquire-timeout900Per-archive assisted acquisition timeout in seconds
--acquire-include-nexusfalseInclude Nexus archives in frontloaded acquisition
--acquire-browser-controllerfalseUse controlled Chromium tabs for frontloaded acquisition
--no-acquire-missingfalseDisable 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>]
FlagDescription
--versionPin a specific collection revision
--profileTarget profile (created if it doesn’t exist)

install mod

Install a single mod from Nexus.

modde install mod <url> [--profile <name>] [--fomod-config <path>]
FlagDescription
--fomod-configPath 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>]
FlagDescription
--profileProfile 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.

Search public Wabbajack modlist catalogs.

modde wabbajack search [query] [--game <id>] [--source official|authored|both] [--json]
FlagDefaultDescription
--gameFilter by game ID
--sourcebothCatalog source: official, authored, or both
--jsonEmit 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>]
FlagDescription
--outputOutput 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>]
FlagDescription
--profileProfile name to emit in the snippet (required)
--gameGame ID to emit in the snippet (required)
--game-dirGame install directory to emit
--outputWrite 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...]
FlagDefaultDescription
--download-dirBrowser download directory to watch
--data-dirOverride the data directory for this acquisition
--browser-profileBrowser profile directory to use
--include-nexusfalseInclude Nexus archives in acquisition
--browser-controllerfalseDrive controlled Chromium tabs instead of just watching downloads
--timeout900Per-archive acquisition timeout in seconds
--jsonfalseEmit 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]
FlagDescription
--data-dirOverride the data directory
--jsonEmit machine-readable JSON
--nix-snippetPrint Home Manager manualArchives entries for the missing archives

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]
FlagDescription
--profileProfile context for the assessment
--game-dirGame install directory to assess against
--jsonEmit 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...]
FlagDescription
--display-nameHuman-readable name (required)
--executable-dirDirectory containing the game executable, relative to the install (required)
--steam-app-idSteam App ID, for launcher detection
--install-dir-nameSteam steamapps/common directory name
--mod-dirMod deployment subdirectory
--nexus-domainNexus Mods domain slug for this game
--proxy-dll <name>Proxy DLL name (repeatable) used by OptiScaler/ReShade-style hooks
--forceOverwrite 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]
FlagDescription
--yesSkip 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>]
FlagDescription
--with-optiscalerAppend resolved OptiScaler profiles to the exported TOML
--outputWrite to a file instead of stdout

game import

Import a game registration from TOML.

modde game import <path> [--force]
FlagDescription
--forceOverwrite an existing destination TOML

game import-profile

Import OptiScaler profiles from TOML for a game.

modde game import-profile <path> --for <game-id> [--force]
FlagDescription
--forGame ID the profiles apply to (required)
--forceOverwrite 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>...]
FlagDefaultDescription
--gameGame ID (required)
--working-dirgame installWorking directory for the launched process
--output-mod__overwrite__Mod that captures files written during the run
--wine-dll-overridesWine DLL overrides, e.g. dinput8=n,b;winmm=n,b
--env KEY=VALUEEnvironment 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>...]
FlagDescription
--profileProfile 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>]
FlagDescription
--profileTarget profile (required)
--gameGame ID
--labelOptional 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>]
FlagDescription
-m, --messageOptional message for the snapshot

save history

Show save snapshot history.

modde save history --game <id> --profile <name> [--limit <n>]
FlagDefaultDescription
--limit20Max 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>]
FlagDefaultDescription
--interval30Poll 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]
FlagDefaultDescription
--profileCheck this profile’s tracked Nexus mods
--gameRestrict to this game
--period1wTime window: 1d, 1w, or 1m
--modsCheck 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...]
FlagDescription
--period <period>Time window to scan: 1d, 1w, or 1m (default 1w)
--dry-runPrint the mods that would be updated without downloading
--confirm-lockedAcknowledge that the profile is locked (Wabbajack / Collection / TOML import); required for any locked profile, since updating drifts it away from its authoritative source
--accept-breakingPermit applying updates that look like breaking semver bumps (major version change)
--yesSkip 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>]
FlagDescription
--data-dirPath 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>...]
FlagDefaultDescription
--gameGame ID (required)
--working-dirgame installWorking directory
--output-mod__overwrite__Mod that captures files written during the run
--wine-dll-overridesWine DLL overrides, e.g. dinput8=n,b;winmm=n,b
--env KEY=VALUEEnvironment 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>
FlagDescription
--tagRelease tag (required)
--assetAsset 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>
FlagDescription
--tagRelease tag the asset corresponds to
--assetAsset 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>]
FlagDescription
--allInclude all plugins (not just defaults)
--formatOutput 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]
FlagDefaultDescription
--game-dirauto-detectedGame installation path
--manifest.wabbajack file for manifest matching
--import-toImport discovered mods into this profile
--threshold0.5Minimum file presence fraction for a match
--dry-runReport only, don’t write to database
--prune-duplicatesRemove 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]
FlagDescription
--allShow all collisions including cosmetic ones
--suggest-hidesSuggest 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]
FlagDescription
--forceReplace 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

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:

TableHolds
profilesOne 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_modsThe 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_rulesLoadAfter / LoadBefore / Incompatible constraints between mods, consumed by the resolver.
installed_mod_filesThe 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.
savesSave files/directories assigned to a profile (the DB-level assignment layer, distinct from the git save vault).
stock_snapshotsPer-game vanilla-game snapshot metadata: snapshot path, tree hash, and file count, used by Stock-Game / clean-baseline flows.
active_profilesThe currently active profile per game (game_id primary key).
experiment_stackA per-game stack of profiles for the “try a change, then pop back” experiment workflow.
hidden_filesPer-profile, per-mod file hides (the MO2 .mohidden equivalent) — excluded from the symlink farm at build time.
plugin_orderPlugin (.esp/.esm/.esl) ordering and enable state, kept independent of mod install priority.
mod_categoriesCollapsible category separators with colour and sort index.
game_toolsCurrent per-game tool/overlay state (MangoHud, vkBasalt, GameMode, OptiScaler, ReShade, Proton…): enabled flag plus a free-form JSON settings blob.
tool_applied_filesFiles a tool wrote into a game directory, tracked so tool changes can be reverted.
executable_configsMO2-style named launch targets: path, args, working dir, environment, Wine DLL overrides, and the configurable output (overwrite) mod.
tool_setting_nodes / tool_setting_edgesA 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_install writes the method, archive hash, and status onto the profile_mods row and replaces the installed_mod_files manifest in one transaction, wiping any prior manifest first so retries never leave orphaned rows. remove_installed_mod is 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_archive runs before the generic installer probes so a game can authoritatively claim a layout it recognises (Cyberpunk’s REDmod via info.json, for example); recognizes_bare_layout is the last-resort fallback before the analyzer emits Unknown.
  • Classification. classify_extension / summarize_content bucket files into ContentCategory; classify_mod decides whether a mod is save-breaking (feeding the save fingerprint).
  • Alternate deploy targets. deploy_targets advertises named roots outside the game install dir (per-user config, saves), and resolve_deploy_target resolves an ID to a real path at deploy time — deferred so plugins can fold in runtime context like a Wine prefix or Steam compatdata.
  • Wine integration. wine_dll_overrides / wine_dll_overrides_from_staging report proxy/hook DLL base names that need WINEDLLOVERRIDES=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:

  1. Implement (or reuse) a GamePlugin for the engine family. Most games reuse a shared data-driven plugin (the Bethesda and UE4 modules are parameterized) rather than a bespoke type.
  2. Optionally implement a ModScanner (filesystem discovery of already-installed mods) and a SaveTracker (save detection + capture description). Both are Option in the registration — a game can ship without them.
  3. Pick a collision_classifier factory (or one of the shared policy classifiers keyed by archive extension).
  4. Add the GameRegistration with 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.

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_id must come after after.
  • LoadBefore { mod_id, before }mod_id must come before before.
  • 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:

  1. No rules → exact input order. With no rules, the resolved order is exactly the enabled subset of the mod list, unchanged.
  2. 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).
  3. Minimal change under rules. When a rule forces movement, only the rule-involved mods shift; unrelated neighbours stay put.
  4. Deterministic. Identical inputs always produce identical outputs; no HashMap iteration 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 FileOriginLoose 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.

  1. Analyze inspects an extracted archive and produces an InstallPlan describing how to lay its files out (the InstallMethod), optionally stripping a wrapper directory (strip_prefix), and carrying the source archive’s xxh64 hash.
  2. Execute moves files from the staging directory into the mod’s store directory per the plan, producing the concrete StagedFile manifest.
  3. 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):

VariantMeaning
BareExtractContents 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/…).
SingleFileSetOne 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). CreateBSA runs as a separate pass after the FromArchive work its inputs depend on.
  • Native, in-process decompression. zip, 7z, BSA, and BA2 are decoded by Rust crates in-process (RAR is an optional rar feature; without it RAR is reported as unsupported rather than shelling out). No 7zz / unrar subprocess remains in the apply path, so there is no per-file execve, no re-mmap, and no subprocess stdout to buffer.
  • Streaming verification. Downloads are hashed as they are written (the copy_and_hash_compat adapter 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:

  1. 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/ whose info.json supplies name and version).
  2. One file = one mod. Used for loose-file mod loaders (e.g. each .archive under archive/pc/mod/).
  3. Plugin + companion archives = one mod. Used for plugin-based games, where an .esp/.esm/.esl and its sidecar .bsa/.ba2 are 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:

  • Done means the feature is shipped end to end and reachable by users.
  • Partial means core logic exists, but the UX, integration, or production safety is still incomplete.
  • Not shipped means 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

CapabilityStatusNotes
Core VFS deploymentDoneSymlink-farm deployment, per-file hiding, atomic rollback, and conflict resolution are shipped. See Deployment & VFS.
Profile switching & save vaultsDoneProfile activation, the experiment stack, git-backed save history, and save fingerprints ship for games with real save trackers.
Bethesda plugin managementDonePlugin-order backup/restore, plugins.txt IO, LOOT parsing, Form 43 detection, and missing-master checks are shipped.
Nexus install pipelineDoneAPI-key auth, browse/search (REST + GraphQL), update checks, nxm://, single-mod installs, and Collections are shipped.
FOMODDoneInteractive wizard plus declarative TOML/JSON/Nix config generation and non-interactive application are shipped.
Executable managementDoneNamed 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 trackingDoneStarfield .sfs files flow through the shared save-tracker path.
Instance switchingPartialCLI/runtime instance selection changes the active data root, but there is no MO2-style portable/global UX yet.
DiagnosticsPartialThe 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 tabPartialThe 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 UIPartialThe Downloads view is wired to real queue state, but pause/resume is UI-side state rather than a durable transport-level pipeline.
Tool managementPartialThe 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 dialogPartialA 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 backendsPartialGitHub, 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.
BAINPartialDetection and execution scaffolding exist, but the interactive sub-package selection flow is unfinished.
Generic game supportPartialUser-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.

GameStatusNotes
Skyrim Special Edition / Anniversary EditionDonePlugins, LOOT, diagnostics, VFS, saves, and Wabbajack/Nexus workflows are the strongest path today.
Fallout 4DonePlugins, diagnostics, VFS, and save tracking are shipped.
Cyberpunk 2077DoneREDmod / CET / TweakXL-aware install and launch flows are shipped.
Fallout 76PartialVFS, plugin handling, and BA2 scanning exist; saves are effectively server-side and only lightly represented locally.
StarfieldPartialGame plugin, plugin handling, VFS, diagnostics, and .sfs save tracking exist.
Stellar BladePartialUE4/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 2PartialEngine-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:

  1. Mod information dialog — file tree, text/INI editing, image preview, conflict tabs, and optional per-mod plugin management.
  2. 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).
  3. Full merged-VFS browser — archive visibility, overwrite actions, hidden-file filters, and “go to the conflicting mod” navigation in the Data tab.
  4. 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.

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

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 and modde import them.

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