Build Minecraft mods targeting both NeoForge and Fabric simultaneously using the Architectury framework for Minecraft 1.21.x. Covers Architectury project structure (common/neoforge/fabric subprojects), ExpectPlatform annotation for platform-specific implementations, shared registry via Architectury's registration API, platform-specific entrypoints, architectury-loom Gradle plugin configuration, gradle.properties for both loaders, multi-jar publishing to Modrinth and CurseForge, and avoiding common pitfalls when sharing code. Use this skill when building a mod that must run on both NeoForge and Fabric with a single shared codebase.
npx skills add https://github.com/Jahrome907/minecraft-agent-skills --skill minecraft-multiloader
Architectury is a framework that
lets you write one mod codebase that compiles to both NeoForge and Fabric JARs.
The common subproject has a shared API; platform subprojects implement
platform-specific behavior behind the @ExpectPlatform abstraction.
Use when: one shared codebase must build and ship both NeoForge and Fabric artifacts.Do not use when: the project is single-loader only (minecraft-modding for NeoForge/Fabric, not both).Do not use when: the task is Paper/Bukkit plugin development (minecraft-plugin-dev).| Component | Purpose |
|-----------|---------|
| architectury-loom | Gradle plugin — extends Fabric Loom for multiloader support |
| architectury-api | Runtime library — abstractions over both platforms |
| @ExpectPlatform | Annotation marking methods with platform-specific implementations |
| common/ | Shared code (no loader-specific APIs) |
| fabric/ | Fabric-specific code + entrypoint |
| neoforge/ | NeoForge-specific code + entrypoint |
# gradle.properties (root)
mod_version=1.0.0
minecraft_version=1.21.11
enabled_platforms=fabric,neoforge
architectury_version=19.0.1
fabric_loader_version=0.19.3
fabric_api_version=0.141.4+1.21.11
neoforge_version=21.11.42
loom_version=1.17.11
Pin architectury_version, the Architectury plugin version, and loom_version
from the same released template line when scaffolding a new project. The values
above are for the stable 1.21.x toolchain story in this repo and avoid snapshot-only examples.
references/architectury-reference.md./scripts/check-version-sanity.sh --root <project>Run the sanity checker after editing gradle.properties. It catches the most common
multiloader drift mistakes: snapshot toolchain pins, missing fabric / neoforge
platforms, and mismatched NeoForge vs Minecraft patch lines.
my-mod/
├── build.gradle ← root build (shared config)
├── settings.gradle
├── gradle.properties
├── common/
│ ├── build.gradle
│ └── src/main/java/com/example/mymod/
│ ├── MyMod.java ← shared init
│ ├── registry/
│ │ └── ModItems.java ← shared registry declarations
│ └── platform/
│ └── PlatformHelper.java ← @ExpectPlatform methods
├── fabric/
│ ├── build.gradle
│ └── src/main/
│ ├── java/com/example/mymod/fabric/
│ │ ├── MyModFabric.java ← Fabric entrypoint
│ ├── java/com/example/mymod/platform/
│ │ └── PlatformHelperImpl.java ← Fabric @ExpectPlatform implementation
│ └── resources/
│ ├── fabric.mod.json
│ └── assets/...
└── neoforge/
├── build.gradle
└── src/main/
├── java/com/example/mymod/neoforge/
│ ├── MyModNeoForge.java ← NeoForge @Mod entry
├── java/com/example/mymod/platform/
│ └── PlatformHelperImpl.java ← NeoForge @ExpectPlatform implementation
└── resources/
├── META-INF/neoforge.mods.toml
└── assets/...
settings.gradlepluginManagement {
repositories {
maven { url "https://maven.architectury.dev/" }
maven { url "https://maven.fabricmc.net/" }
maven { url "https://maven.neoforged.net/releases" }
gradlePluginPortal()
}
}
include "common"
include "fabric"
include "neoforge"
build.gradleplugins {
id "architectury-plugin" version "3.4" apply false
id "dev.architectury.loom" version "${loom_version}" apply false
id "com.github.johnrengelman.shadow" version "8.1.1" apply false
}
architectury {
minecraft = rootProject.minecraft_version
}
subprojects {
apply plugin: "java"
apply plugin: "architectury-plugin"
group = "com.example.mymod"
version = "${mod_version}+${minecraft_version}"
archivesBaseName = "my-mod-${project.name}"
repositories {
maven { url "https://maven.architectury.dev/" }
maven { url "https://mod-buildtools.pkg.github.com/TerraformersMC/" }
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
common/build.gradleplugins {
id "dev.architectury.loom" apply true
}
architectury {
common(rootProject.enabled_platforms.split(","))
}
loom {
// common project uses mappings only
}
dependencies {
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
mappings loom.officialMojangMappings()
modImplementation "dev.architectury:architectury:${rootProject.architectury_version}"
}
fabric/build.gradleplugins {
id "com.github.johnrengelman.shadow"
id "dev.architectury.loom" apply true
}
architectury {
platformSetupLoomIde()
fabric()
}
loom {
accessWidenerPath = project(":common").loom.accessWidenerPath
}
configurations {
common
shadowCommon
compileClasspath.extendsFrom common
runtimeClasspath.extendsFrom common
developmentFabric.extendsFrom common
}
dependencies {
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
mappings loom.officialMojangMappings()
modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
modApi "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
modApi "dev.architectury:architectury-fabric:${rootProject.architectury_version}"
common(project(path: ":common", configuration: "namedElements")) { transitive false }
shadowCommon(project(path: ":common", configuration: "transformProductionFabric")) { transitive false }
}
shadowJar {
exclude "architectury.common.json"
configurations = [project.configurations.shadowCommon]
archiveClassifier = "dev-shadow"
}
remapJar {
injectAccessWidener = true
input.fileValue shadowJar.archiveFile.get().asFile
dependsOn shadowJar
archiveClassifier = ""
}
jar { archiveClassifier = "dev" }
sourcesJar { archiveClassifier = "dev-sources" }
components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { skip() }
neoforge/build.gradleplugins {
id "com.github.johnrengelman.shadow"
id "dev.architectury.loom" apply true
}
architectury {
platformSetupLoomIde()
neoForge()
}
loom {
accessWidenerPath = project(":common").loom.accessWidenerPath
}
configurations {
common
shadowCommon
compileClasspath.extendsFrom common
runtimeClasspath.extendsFrom common
developmentNeoForge.extendsFrom common
}
dependencies {
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
mappings loom.officialMojangMappings()
neoForge "net.neoforged:neoforge:${rootProject.neoforge_version}"
modApi "dev.architectury:architectury-neoforge:${rootProject.architectury_version}"
common(project(path: ":common", configuration: "namedElements")) { transitive false }
shadowCommon(project(path: ":common", configuration: "transformProductionNeoForge")) { transitive false }
}
shadowJar {
exclude "architectury.common.json"
configurations = [project.configurations.shadowCommon]
archiveClassifier = "dev-shadow"
}
remapJar {
input.fileValue shadowJar.archiveFile.get().asFile
dependsOn shadowJar
archiveClassifier = ""
}
jar { archiveClassifier = "dev" }
common/.../MyMod.javapackage com.example.mymod;
import dev.architectury.registry.registries.DeferredRegister;
import dev.architectury.registry.registries.RegistrySupplier;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
public class MyMod {
public static final String MOD_ID = "mymod";
// Architectury's DeferredRegister — works on both platforms
public static final DeferredRegister<Item> ITEMS =
DeferredRegister.create(MOD_ID, BuiltInRegistries.ITEM);
public static final RegistrySupplier<Item> MY_ITEM =
ITEMS.register("my_item", () -> new Item(new Item.Properties()));
public static void init() {
ITEMS.register(); // registers with both platforms
}
}
@ExpectPlatform — platform-specific methodsDefine the contract in common/:
package com.example.mymod.platform;
import dev.architectury.injectables.annotations.ExpectPlatform;
import net.minecraft.world.level.material.Fluid;
public class PlatformHelper {
@ExpectPlatform
public static boolean isModLoaded(String modId) {
// This body is replaced at compile time by the platform implementation
throw new AssertionError("ExpectPlatform implementation not found");
}
@ExpectPlatform
public static boolean isClient() {
throw new AssertionError();
}
}
Keep each platform implementation in the same Java package as the common
@ExpectPlatform class. Only the source set changes between common/,
fabric/, and neoforge/.
Implement in fabric/.../platform/PlatformHelperImpl.java:
package com.example.mymod.platform;
import net.fabricmc.loader.api.FabricLoader;
// Class name must match: <common class name>Impl
public class PlatformHelperImpl {
public static boolean isModLoaded(String modId) {
return FabricLoader.getInstance().isModLoaded(modId);
}
public static boolean isClient() {
return FabricLoader.getInstance().getEnvironmentType() ==
net.fabricmc.api.EnvType.CLIENT;
}
}
Implement in neoforge/.../platform/PlatformHelperImpl.java:
package com.example.mymod.platform;
import net.neoforged.fml.ModList;
import net.neoforged.fml.loading.FMLEnvironment;
public class PlatformHelperImpl {
public static boolean isModLoaded(String modId) {
return ModList.get().isLoaded(modId);
}
public static boolean isClient() {
return FMLEnvironment.dist.isClient();
}
}
fabric/.../MyModFabric.javapackage com.example.mymod.fabric;
import com.example.mymod.MyMod;
import net.fabricmc.api.ModInitializer;
public class MyModFabric implements ModInitializer {
@Override
public void onInitialize() {
MyMod.init();
}
}
fabric/.../resources/fabric.mod.json{
"schemaVersion": 1,
"id": "mymod",
"version": "${version}",
"name": "My Mod",
"description": "A multiloader example mod",
"license": "MIT",
"environment": "*",
"entrypoints": {
"main": ["com.example.mymod.fabric.MyModFabric"]
},
"depends": {
"fabricloader": ">=0.19.3",
"fabric-api": ">=0.141.4+1.21.11",
"architectury": ">=19.0.1",
"minecraft": "~1.21.11"
}
}
neoforge/.../MyModNeoForge.javapackage com.example.mymod.neoforge;
import com.example.mymod.MyMod;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
@Mod(MyMod.MOD_ID)
public class MyModNeoForge {
public MyModNeoForge(IEventBus modEventBus) {
MyMod.init();
}
}
neoforge/.../resources/META-INF/neoforge.mods.tomlmodLoader = "javafml"
loaderVersion = "[1,)"
license = "MIT"
[[mods]]
modId = "mymod"
version = "${file.jarVersion}"
displayName = "My Mod"
description = "A multiloader example mod"
[[dependencies.mymod]]
modId = "neoforge"
type = "required"
versionRange = "[21.11,)"
ordering = "NONE"
side = "BOTH"
[[dependencies.mymod]]
modId = "minecraft"
type = "required"
versionRange = "[1.21.11,1.22)"
ordering = "NONE"
side = "BOTH"
# Build both JARs simultaneously
./gradlew build
# Outputs:
# fabric/build/libs/my-mod-fabric-1.0.0+1.21.11.jar
# neoforge/build/libs/my-mod-neoforge-1.0.0+1.21.11.jar
# Run in dev environment
./gradlew :fabric:runClient
./gradlew :neoforge:runClient
./gradlew :neoforge:runServer
# Datagen (if applicable)
./gradlew :neoforge:runData
| Pitfall | Solution |
|---------|----------|
| Using net.neoforged.* / net.fabricmc.* in common/ | Only use vanilla MC and Architectury APIs in common |
| Direct field access on DeferredRegister (NeoForge style) in common | Use Architectury's DeferredRegister |
| Forgetting @ExpectPlatform throws AssertionError at runtime | Both fabric/ and neoforge/ must have matching same-package *Impl classes |
| Assets duplicated in fabric/ and neoforge/ | Keep assets in common/src/main/resources/assets/ |
| Mixins in common — not supported on NeoForge | Put Mixins in the platform subprojects only |
| Accessing world/registry on mod init thread | Use mod bus events for setup; never access world on init |
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take jahrome907/minecraft-multiloader from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.