Concepts
Manifest
shulker.json declares the Minecraft version, loader, sides, mods, and modpacks. shulker create writes one from flags, taking the defaults for anything not given, and shulker init asks about each choice on the terminal instead. Every field is listed in the Manifest Reference.
Lock
shulker.lock records the exact resolved versions, hashes, and modpack commits. Shulker writes it and you commit it, and nothing in it is hand-edited. Its fields are described by its JSON Schema, which editors read from the $schema line at the top of the file.
Sides
A project has a client side, a server side, or both, and each is a build output, a client instance or a server directory. A side exists when its block does. "client": {} in shulker.json declares the client, "server": {} the server, and the block holds that side's settings, such as client.options, client.memory or server.properties. Each side builds into build/<side>, or in place, with "build": ".", which is what makes the project an instance, so one project produces a client and a server from the same mod list. shulker build builds every declared side, or the one you name. To play a server-only pack someone else publishes, pass --assume-client to link, sync or export, and Shulker builds a client from what both sides share.
Every mod has a side too, client, server, or both. Shulker reads it from the provider, then from the jar: Fabric's and Quilt's metadata name one, and a NeoForge or Forge jar, whose mods.toml names none, is client when it declares dependencies and every one is side = "CLIENT", since the loader skips those on a server and the mod then fails there. Anything else is both. You can override it with side on the mod, and the lock's sideFrom records which of these decided. A side only gets mods for itself plus those marked both, so client-only mods like Iris never end up on a server. A dependency a jar declares for one side only is checked only on that side. Resource packs and shaders are always client-only, and a server build leaves them out.
A server build writes eula.txt once you have accepted the Minecraft EULA. The first shulker serve asks and records your answer in your Shulker config rather than the manifest, so no project asks again, and a manifest cannot accept it for you.
Dependencies
Shulker reads each mod's dependencies from the jar's own metadata, not from the provider's page, and checks them for every side the project declares. add, remove, update, lock and install all run the check, so a mod whose dependency is missing or the wrong version fails there rather than in the game.
- Each side is checked against the mods its own build places. A client mod that needs a server-only mod fails, since the client build leaves that mod out, and a problem only one side has names the side.
- A problem you know is harmless is accepted with
shulker ignore. The problem prints the exact command to run. The entry lands inignoreinshulker.jsonwith a note saying why, and holds only while the jar declares that same range, so a new version that declares another brings the problem back. - The loader's own dependency overrides apply first. On Fabric that is
config/fabric_loader_dependencies.json, and on NeoForge for Minecraft 1.21.1 and later thedependencyOverridestable ofconfig/fml.toml, each as the build lays it from your overrides. A dependency the file removes needs no ignore. Quilt'sconfig/quilt-loader-overrides.jsonisn't applied, and a build that places one warns. shulker suggestslists the mods your locked mods recommend that aren't installed, and--optionaladds their optional dependencies.shulker checkruns the same check without writing anything, for CI. See GitHub Actions.
Overrides
Overrides are folders of files copied into a build on top of the mods, such as configs, resource packs and scripts. The folders are fixed by convention, and each is used only if it exists. Later folders win, in this order, with features in name order, so a shared config can sit under a server-only one.
overrides/goes into both sides.client-overrides/andserver-overrides/go into one.<feature>-overrides/(or the path the feature'soverridesnames) goes into a build while that feature is on.
Some files are generated instead of copied. server.properties, options.txt, and the whitelist, ops, and ban lists come from shulker.json, and a shader mod's iris.properties from the shader you locked.
Templates
Files ending in .tmpl have ${name} replaced with the side's variables and are written without the suffix. server.properties and client.options values in the manifest expand the same variables. The built-in variables are always there as well.
| Variable | Comes From |
|---|---|
${project.name}, ${project.displayName}, ${project.version} | The manifest of the project that owns the folder, so a pulled pack's files get that pack's own. ${project.displayName} is the side's name, or the project's when the side has none. An export given --version uses it for the project's own ${project.version} |
${minecraft.version}, ${minecraft.dataVersion} | The lock. ${minecraft.dataVersion} is the number Minecraft stamps into worlds and options.txt |
${java.major}, ${loader.type}, ${loader.version} | The lock |
A built-in with nothing to fill it, like ${project.version} in a manifest with no version, is unset, and using it fails the build.
Excluded Files
Folder metadata your operating system leaves behind (.DS_Store, ._* files, Thumbs.db, desktop.ini) never goes into a build or an export, and the manifest's skipFiles leaves out more by glob.
Features
A feature is a named, optional part of a pack, like shaders, that each player switches on or off for their own machine. features in shulker.json declares each one and whether it starts on, and a requires entry joins it with feature.
"features": { "shaders": { "default": true } },
"requires": {
"iris": { "feature": "shaders" },
"sodium-extra": { "feature": "!shaders" }
}- An entry gated on a feature ships only while the feature is on, and
!nameships it only while the feature is off. A list of names is any-of, and every!namein it has to hold as well. - A dependency that only gated-out mods need is left out with them. A gated-out mod that a mod still in the build requires is kept, and the build warns.
- A feature brings its own override folder,
<feature>-overrides/unless itsoverridesnames another, laid over the base folders while it is on. See Overrides. shulker feature on|offrecords your choice inshulker.local.jsonbeside the manifest, which is per machine and kept out of git.feature resetgoes back to the declared default, andfeature listshows each feature with the mods it gates.--withand--withoutonbuild,install,syncandexportdecide a feature for one run, over both your choice and the default.- A linked instance keeps its own choices, so
shulker feature on shaders -i <id>turns a feature on for one instance and leaves the project alone.
Operating Systems
An entry can also be held to an operating system with os, which takes macos, windows or linux, or !name to leave one out. It is decided where the build runs, and it combines with feature, so both have to hold.
"requires": { "controlify": { "os": "!macos" } }Edits in the Build Directory
Each build records what it wrote in .shulker/state.json in the build directory, so the next build can tell your edits apart from its own files. That way config you change in-game survives.
- A file you edited is kept, as long as its source hasn't changed.
- If the file changed in both places, or a file Shulker didn't write is in the way, the build stops and lists it. Use
shulker build --forceto overwrite. A sync for a launch, fromshulker playor a launcher's pre-launch hook, is the one exception. It keeps your file, applies the rest and warns, so an update never holds the game back. - Generated files like
server.propertiesmerge per key, so a key you edited in-game and a key you changed inshulker.jsonboth apply. If both changed the same key,shulker.jsonwins and the build warns, unless the file is seeded. - A
.propertiesfile in your overrides merges per key too. Shulker manages only the keys it lists, and leaves any others a mod writes alone. A file the mod wrote first gets those keys merged in instead of stopping the build. A file nothing merges into, because no other layer or Shulker changes a key in it, is placed exactly as written. List a path in the manifest'swholeFilesto copy it whole instead. - A file the manifest's
seedFileslists is seeded: the pack supplies a starting copy and you own it after that. It is written when missing, deleted included, and follows the pack while you haven't changed it. Once both have changed it, every build keeps yours and warns, never stops. Delete it to take the pack's again, orshulker build --forcefor every seeded file. A file merged per key, likeoptions.txtfromclient.optionsor a.propertiesoverride, is seeded key by key instead: a key you changed keeps your value while the rest follow the pack, and a key the pack drops goes only if you left it alone. A pulled modpack's ownseedFilescovers the files it brings.
Use shulker diff to see what changed, and shulker pull to copy those edits back into your overrides or shulker.json so they're part of the project.
Modpacks
A modpack is a requires entry that brings another pack's mods and overrides into this one. Its overrides sit beneath your own layers, and a mod you list in shulker.json yourself always wins over what a modpack provides. It comes from one of three places, and shulker modpack add, or a bare shulker add, takes each.
A Source
Another Shulker project at a local path, git URL, or manifest URL. A git repository holding several packs names the folder of the one you want with path. A raw manifest URL brings only the pack's shulker.json and shulker.lock, never its overrides or local files, so every command that reads one says so, and one naming a local file fails. Use the repository's git URL for a pack that has either.
- A locked modpack ships a
shulker.lock. The exact versions it pins, dependencies included, are copied into your lock and marked with the modpack they came from, and its Minecraft and loader have to match yours exactly. When you set neither, yours are taken from it, and every locked modpack has to agree. - A floating modpack has no lock, or was added with
--unlocked. Its mods are resolved here alongside your own, and its versions only have to fit your ranges.
A Pack Archive
A local .mrpack or CurseForge modpack zip, named by file. Its mods lock as the modpack's, found by hash on Modrinth or by project and file ID on CurseForge, and its override folders are laid before your own. The lock records the archive's bytes, so changing the file makes the lock out of date until shulker lock reads it again.
A Hosted Modpack
A Modrinth or CurseForge modpack by its slug, like shulker add cozy. The newest version that fits your Minecraft and loader is locked, and its archive is read as a local one is. pin, unpin, update and outdated treat it like a mod, and sync never moves it.
Instances
An instance is a project that builds in place, so its own folder is the game directory, living in a launcher's instance folder. The instance is a project in its own right, so shulker add there puts a mod on top of the pack and keeps it through every update.
Linking
shulker link <launcher> makes one for Prism Launcher, MultiMC, ATLauncher, GDLauncher, the Minecraft Launcher, or Shulker itself. It writes a shulker.json in the launcher's game directory that follows the pack you linked as a modpack, takes its Minecraft version, loader, features and overrides from it, and builds in place before link returns. The pack can be a project of your own, or someone else's git or manifest URL.
Syncing
A linked instance syncs from its source before each launch. The launcher runs a small script Shulker keeps in the instance's .shulker/ folder, and the Minecraft Launcher runs a shim in the profile's Java slot instead. Each sync records how it went in the instance's own .shulker/instance.json, which also holds the settings that decide how Shulker sets the instance up.
shulker instanceslists every instance under a short id, and-i <id>acts on one from anywhere.shulker unlinkstops syncing one while leaving its files.- A directory you
sync --intois a detached build instead, with no registry row and no hooks.
Instances Shulker Owns
An instance Shulker owns is one it launches itself. shulker play syncs it, fetches the game, its libraries and the loader into a shared store, and starts the game as an account you signed in with shulker accounts login, an offline account, or one read from another launcher's files. Those instances share their worlds through save groups, and backup and restore zip and put back the worlds of any instance.
Instance Settings
Every instance has its own settings in .shulker/instance.json, which are per machine and never part of the pack. shulker instance set changes one, for the instance the current directory is or the one -i names.
shulker instance set memory 8G
shulker instance set window 1920x1080 -i smp
shulker instance set jvmArgs --literal '["-XX:+UseZGC"]'memory,jvmArgsandwindowapply in every launcher. Undershulker playthey are the heap size, extra JVM arguments and window size of the launch, and for an instance in another launcher Shulker writes them into that launcher's own settings at each link, sync or repair. ATLauncher keeps one window size for every instance, sowindowthere warns instead.- A setting you leave unset leaves the other launcher's own value alone. Under
shulker playit falls back to theplay.default in your config, whichshulker config setsets for every instance at once, and formemorythen to the pack'sclient.memory, and then to4G. javaandwrapperpoint the launch at a Java of your own and a command to run it through, such asgamemoderun.sandboxruns the game out of reach of the rest of your home folder, in every launcher. It is experimental. See The Sandbox.accountpicks the accountshulker playlaunches the instance as, over the default.hooks.preLaunchandhooks.postExitturn the sync before each launch, and the record of each run, on or off.
The Pack in Its Launcher
A linked instance carries the pack's name and icon into its launcher. icon in shulker.json names a PNG in the project, and each launcher shows it, fitted to the launcher's own shape. A sync replaces the launcher's image only when the pack's icon changes, so one you picked yourself stays until then. Exports carry the icon too.
The Mod List Entry
A client build includes a small marker mod, which gives the pack its own entry in the in-game mod list, with its name, description, icon and links.
- The entry shows what Shulker wants you to know about the instance. A Notices section names files gone from their provider and jars changed since Shulker placed them, and the last 30 days of syncs each get a section listing what they changed.
- The marker is also how an export of the build is recognised as this pack again.
"marker": falseinshulker.jsonleaves it out, andshulker instance set marker falsedoes the same for one instance.
History and Rollback
An instance keeps the states it was in before it changed, so an update that breaks the game can be undone.
- An entry is taken before anything is rewritten.
add,remove,updateandlocktake one before they saveshulker.jsonandshulker.lock, and an in-place build takes one before it writes over a file you changed. - An entry holds the manifest, the lock, the whole
configdirectory and every other file the build manages. Mod and pack files aren't copied, since the restored lock brings them back from the cache. shulker history listlists the entries, newest first, andhistory showsays what restoring one would bring back, remove and change.shulker rollbackrestores an entry and builds it in place. The current state is kept as an entry first, so a rollback can itself be rolled back.historyinshulker.jsonis how many entrieshistory prunekeeps, 5 by default. Nothing else deletes history, and a build over that number only warns.-1keeps every entry, and0takes none.
Only a project with a side that builds in place keeps history, which is every linked instance.
Saves and Backups
History covers the pack's files, and backups cover the worlds.
Save Groups
Every instance Shulker owns shares its worlds through a save group. Its saves/ folder is a link to the group's folder, so every instance in a group lists the same worlds, and a new instance of another pack opens the worlds you already have. Each instance joins the default group.
shulker instance set savesGroup <name>moves an instance to another group, andnonekeeps its worlds in the instance. Nothing is copied or merged, and leaving a group leaves its worlds there.- Instances in other launchers keep their own worlds.
shulker saveslists the groups, or one group's or instance's worlds and backups.
Backups
shulker backup zips the worlds of a save group, an instance in any launcher, or a server, and shulker restore puts them back.
- An
updateor asyncthat changes the mods backs the worlds up first, and keeps the newest five of those automatic backups.play.saveBackupsin your config changes the number. - A backup you take yourself is never deleted automatically.
shulker saves prunedeletes backups, and always says how many it keeps. - A restore backs up the worlds that are there first, so it can be undone, and replaces each world whole rather than merging files.
--worldrestores only the worlds you name. - A restore refuses to replace a world a running game or server has open. Quit to the title screen, or stop the server, first.
Resource Packs and Shaders
Resource packs and shaders sit in requires beside your mods, and the provider's own project type decides which is which, so shulker add fresh-animations needs no --type.
Filenames
Each is placed under its entry's filename. shulker add sets it to the provider's file name, the one other packs' options.txt and Paxi load orders already name, and then holds it. Minecraft and Iris both enable packs by literal file name, so a stable name is what keeps a pack you turned on from quietly switching off the next time it updates, even though the provider's file name changes with every version.
- An entry without
filenameis placed under itsrequireskey,resourcepacks/<key>.ziporshaderpacks/<key>.zip. - Set
filenameto a name of your own, such as theChat Reporting Helper.zipplayers of another modpack already have enabled. Change it later and the build moves the pack's entry in your enabled list along with it.
Shaders
A shader starts enabled only when client.shader names it; with none named, the build selects no shader, since a shader is a performance choice. "shader": "" clears a selection the pack's overrides ship. It is written into its shader mod's own config, config/iris.properties, or config/oculus.properties on Forge. Shulker writes only shaderPack and enableShaders there, so the rest of your shader settings survive a rebuild.
- Each build looks for Iris or Oculus among the mods it placed, so a shader mod behind a feature only enables a shader in the builds that have it.
- The named shader goes to the first of those mods that can load it, going by the shader mods the provider tagged it for. A shader added from a file loads in either.
- The other shaders are placed but off, with no warning. Only a shader nothing in the build can load gets a line in the build report, with
shulker add iristo fix it. - A shader that ships vanilla core shaders needs no shader mod at all. It goes in
resourcepacks/and is enabled like a resource pack.
Shulker knows Iris, Oculus and Canvas by their jar ids. A fork under another id is marked in integrations, which replaces the ids Shulker looks for, so list the original beside it. An empty list turns a shader mod off:
"integrations": { "iris": ["iris", "iris_fork"] }The Enabled List
The enabled list in options.txt works differently, because it is in priority order and yours to arrange. client.resourcePacks names the packs that start on, top first, by their keys, along with the game's own programmer_art and high_contrast; a placed pack it leaves out is off. Without it, Shulker seeds the list once, on the first build, with every placed pack, when the line is missing or still Minecraft's own ["vanilla"], and then leaves it alone.
- With
client.resourcePacks, the build writes the list again whenever you change it, unless the player has changed the list in game since: then theirs stays, and the build report says so. The shader named inclient.shaderworks the same way.shulker build --forcewrites yours. - Without it, a pack you add later is placed but not enabled. Turn it on in game, and Shulker won't reorder what you chose.
shulker build --forceseeds the list again. - An
options.txtin your overrides that sets its ownresourcePackslist is never seeded over, unlessclient.resourcePacksis set. That list is the pack's own, and the build names each pack entry in it that no pack is placed under, such as a pack renamed since the list was written.client.options.resourcePackssets the raw list too, for a list that names packsclient.resourcePackscan't, but not together with it. - Importing a pack moves the list its
options.txtships, and the shader itsiris.propertiesoroculus.propertiesselects, intoclient.resourcePacksandclient.shader, unless the list names a pack a mod provides. - Before Minecraft 1.13 the game names a pack by its bare file name rather than
file/<name>, and the seed and the checks follow that.
Datapacks
A datapack sits in requires too, with "type": "datapack". Modrinth files datapacks as mods, so a project whose only files are datapacks adds as one on its own, and a mod that also ships a datapack, like Terralith, needs shulker datapack add to get the datapack.
Vanilla loads datapacks only per world, so a datapack for every world needs a global datapack mod. Each build places a datapack in the folder of one it finds among the mods it placed.
config/paxi/datapacks/for Paxi.config/openloader/data/before Minecraft 1.21, orconfig/openloader/packs/from it, for Open Loader.- A server without one places it in its world's own
datapacks/folder, the worldlevel-namenames, which the game loads with no mod at all. That one folder inside the world is the build's to fill, so a datapack edited or replaced there, as by restoring an old backup, is a conflict until--force. - A client without one places it in
datapacks/, which only some global datapack mods read, and says so.
Shulker knows Paxi and Open Loader by their jar ids. Mark a fork under another id in integrations, listing the original beside it, or turn one off with an empty list:
"integrations": { "paxi": ["paxi", "paxi_fork"] }A datapack is placed on both sides unless its side says otherwise, since a singleplayer world runs its server inside the client. Load order isn't Shulker's. Ship Paxi's datapack_load_order.json as an override, naming each datapack by its file name, which is <key>.zip unless filename says otherwise.
Providers
Mods, resource packs, shaders and datapacks are resolved from Modrinth or CurseForge. By default Modrinth is tried first, then CurseForge. Set providers to change the order or use only one, or set provider on a single mod.
CurseForge needs an API key. Release builds include one, so it works without setup. To use your own, set SHULKER_CURSEFORGE_KEY or run shulker config set curseforge.key <key>. Your key always takes priority and is never replaced. If CurseForge rejects the included key, Shulker fetches a new one from shulker.sh, saves it in its cache, and retries once. If that fails too, it asks you to set your own key or report an issue.
Cache
Every file Shulker downloads is stored once, by hash, in a cache shared by all your projects, ~/Library/Caches/shulker on macOS, ~/.cache/shulker on Linux and %LOCALAPPDATA%\shulker on Windows, or wherever SHULKER_CACHE points. A build places files out of it, so a mod ten instances use is downloaded once, and installing a pack you have built before needs no network at all.
Nothing Shulker placed is deleted without its bytes reaching the cache first. That is what makes shulker rollback work offline. A history entry leaves mod and pack files out and relies on the cache to put them back.
shulker cache prune removes what nothing references. What counts as a reference is every registered instance and the project you run it in, which means each one's shulker.lock, the lock of every history entry it keeps, and the modpack checkouts and offline sync fallbacks its sources need. A registered folder that is gone is skipped, and one whose lock can't be read stops the prune instead. Files you downloaded by hand are kept unless you pass --manual, since nothing can fetch them again. Your CurseForge key and the Java runtimes Shulker manages are never touched. shulker cache info shows what it would free before you run it.
