A deterministic mod manager for Linux.
Tip
NOTE: this is the developer documentation, if you just want to install and get going pre-built packages are available for most distributions: https://pkg.modctl.org and the usage documentation is available at: https://modctl.org/docs/overview/.
modctl installs mods by extracting archives directly into game directories while tracking every installed file. It supports per-profile priority ordering, conflict detection, and safe rollback via automatic backups.
Designed for Steam first, with a store-agnostic architecture ready for Heroic, Lutris, and future GOG clients.
Metadata is stored in SQLite. Binary artifacts (archives and backups) are stored in content-addressed blob stores on disk.
mattn/go-sqlite3)archives/, backups/,
overrides/)bsdtar with staging directory and safe move into
game directoryGaming on Linux is a real thing now, at least for the kinds of games that I play. But modding seems to still be stuck in Windows-only land. In theory you can run Vortex or Mod Organizer 2 on Linux using Wine, but in my experience it's clunky; I was able to get the tools running but performance was abysmal even just starting the game through the mod launcher with no mods enabled. There are a few attempts to make real Linux ports of these tools but none have made meaningful progress.
For the games I tried, just dumping files into the game directory worked without issue. But I missed the useful parts of a mod manager: conflict detection, load order management, update checking against Nexus. I realised that at the end of the day all I really need is a package manager for mods: something that tracks which archives have been extracted and which files they created, so that install and uninstall are possible. Keep everything in SQLite and you're most of the way there.
So a few conversations with your friendly, neighborhood LLM to think through the design and edge cases, and here we are.
I used AI assistance while planning and building this project, primarily for design discussions, thinking through edge cases, and writing documentation. I did not use an autonomous agent. Every line of code was reviewed, tested, and where necessary modified before being committed. This is not a vibe-coded project.
nxm:// protocol handlerbsdtar)go.mod for the minimum version; currently 1.25)gcc is sufficient; CGO is required for mattn/go-sqlite3, and
klauspost/compress/zstd)bsdtar at compile time and at runtime (provided by libarchive-tools on
Debian/Ubuntu, bsdtar on Fedora/RHEL, libarchive on Arch Linux)Clone the repository and run make:
git clone https://github.com/mfinelli/modctl
cd modctl
make
This produces a modctl binary in the project root. CGO_ENABLED=1 is set
automatically by the Makefile.
To run the tests:
go test ./...
To format the code:
go fmt ./...
.github/ CI workflows
cmd/ Cobra command definitions
docs/ User-facing documentation (also published to the website)
internal/ Internal Go packages
migrations/ Goose database migration files
pkg/ Package build scripts, specs, PKGBUILDs, signing keys
scripts/ Development scripts
www/ Public website (Zola)
modctl uses goose for database migrations.
Migration files live in migrations/. To create a new migration:
goose create MIGRATION_NAME sql -dir migrations -s
modctl uses sqlc to generate type-safe Go code from SQL queries. To regenerate after modifying queries or schema:
sqlc generate
This is also run automatically by make when rebuilding the binary.
Commands are defined in cmd/ using Cobra.
Each command or subcommand group has its own file following the naming
convention already established in the package (e.g. profiles_create.go,
mods_nexus_link.go).
Tests must not touch the machine that they run on, and the packages in
internal/ are written so that they can't. CI runs the tests through
scripts/test.bash, which points HOME, the XDG directories and TMPDIR at a
temporary directory (the ones that hold state are read-only) and fails if
anything is left in it or in the repository.
Respect the following rules when writing tests:
internal/ are given what they work on. They don't read the
environment, $HOME, the working directory or the system temporary directory,
and they don't import xdg or viper (internal/config is the only
exception: it reads the config file).cmd/ is the only place that knows where things are. cmd/env.go turns
the configuration and the XDG directories into those arguments. When a
function in internal/ needs another path or setting, add a parameter instead
of reading it from its surroundings.internal/ apart from read-only values
(tables, styles, embedded files) and sentinel errors. State that is shared is
what makes tests depend on each other and keeps them from running in parallel.t.TempDir() for files,
testbuilder.SetupDB or SetupDBFile for a database, testbuilder.Schema()
for the migrations, and a fake http.RoundTripper or a local server for the
network. Nothing in a test talks to the real Nexus Mods.t.Setenv, os.Setenv or os.Chdir, and no TestMain
that sets something up for the others. Tests should use t.Parallel() where
possible, and go test -race -shuffle=on should always pass. The tests of
internal/style are the only exception: they check how the output behaves on
a terminal and with NO_COLOR, which is the environment, so they run one at a
time.Contributions are welcome. For bug reports and questions please use the issue tracker and GitHub Discussions.
For code contributions, please open an issue first to discuss what you would like to change. There are no formal contribution guidelines yet: use common sense, follow the existing code style, and please make sure the tests pass (I would love it if all new new functionality came with supporting tests even if I've been pretty sparse so far myself).
I am focused on Linux for this tool. Patches for macOS or Windows support may be considered if they do not raise overall complexity significantly.
mod control (modctl): command-line mod manager
Copyright (C) 2026 Mario Finelli
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Content type
Image
Digest
sha256:37d9be439…
Size
14.1 MB
Last updated
9 days ago
docker pull mfinelli/modctl