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

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