Fabric mixins
Mixins patch Minecraft's bytecode at load time. They are powerful and fragile: every mixin is a promise that a specific method, with a specific shape, still exists in the next version and that no other mod changes it incompatibly. Write as few as possible, as narrow as possible.
Decision order - try these before writing a mixin
- Fabric API event or registry. Search Fabric API first: lifecycle events
(
ServerLifecycleEvents,ClientTickEvents), player events (UseBlockCallback,AttackEntityCallback,PlayerBlockBreakEvents), HUD (HudElementRegistry), screens (ScreenEvents), loot (LootTableEvents), commands (CommandRegistrationCallback), networking, item tooltips, resource reload listeners, and many more. Browse https://github.com/FabricMC/fabric-api (each module'sapipackage) and the Fabric docs. An event is stable across versions and composes with other mods for free. - Vanilla extension points. Data-driven content (tags, loot tables, recipes, data components, registries) often needs no code patching at all.
- Accessor / Invoker mixin - when you only need to read/write a private field or call a private method.
- Class tweaker (access widener) - when you need to subclass a final class, override a final method, or name a private/package-private class.
- Injector mixin - only now. Prefer MixinExtras injectors over
@Redirect/@Overwrite.
Write down (in a comment above the mixin) why steps 1-4 did not work. Reviewers will ask.
Before writing any injector: confirm the target
Names and signatures change every Minecraft version. Never write a target from memory.
./gradlew genSources # then open the class in the IDE or search the generated sources
- 26.1+ (unobfuscated): target the Mojang names exactly as they appear in the sources.
- Pre-26.1: target the names of your project's mappings (Yarn or Mojang). Loom's mixin annotation processor remaps them to intermediary at build time; a mismatch shows as a "Cannot remap" warning at compile time - treat that warning as an error.
- Copy method descriptors from the bytecode view or a tool (MCDev IntelliJ plugin, mcsrc.dev), not by hand. Descriptor reference: https://docs.fabricmc.net/develop/mixins/bytecode
Mixin config and conventions
{
"required": true,
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
"client": [],
"server": [],
"injectors": { "defaultRequire": 1 },
"overwrites": { "requireAnnotations": true }
}
"defaultRequire": 1makes a mixin that matches nothing fail loudly instead of silently doing nothing. Keep it.- Client-only targets (
net.minecraft.client.*) go in the client config (insrc/client/resources, declared with"environment": "client"infabric.mod.json). - Mixin classes are never referenced from normal code (except accessor interfaces, via a cast).
- Prefix every added member with your mod id to avoid collisions with other mods:
examplemod$onTick,@Unique private int examplemod$counter;. - Mark helper fields/methods you add with
@Unique. - Handler methods are
private(non-static if the target is non-static).
The targets in the examples below use Mojang names and show the shape of each injector. Method names and descriptors drift between versions - re-check every one of them in your generated sources before reusing it.
@Inject - run code at a point, optionally cancel
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true)
private void examplemod$cancelJump(CallbackInfo ci) {
if (ExampleConfig.get().disableJump) {
ci.cancel();
}
}
// getJumpPower is overloaded (getJumpPower() and getJumpPower(float)): give the descriptor.
@Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(cir.getReturnValueF() * 1.5f);
}
}
CallbackInfoforvoidtargets,CallbackInfoReturnable<T>otherwise (boxed type).- Only declare
cancellable = truewhen you actually cancel. Cancelling atHEADskips the vanilla method and every other mod's injection after yours - it is the least compatible thing an@Injectcan do. Prefer modifying a value. - To read the target's parameters, add them to the handler signature before the
CallbackInfo, in order:(float amount, CallbackInfo ci). - Common
@Atvalues:HEAD,RETURN(every return),TAIL(last return only),INVOKE(before a call; addshift = At.Shift.AFTERfor after),FIELD,NEW. - Use
ordinalonly when you must; it breaks as soon as Mojang adds another matching call.
MixinExtras - use these instead of @Redirect and most @ModifyVariable
MixinExtras ships inside Fabric Loader (0.15+), so no extra dependency is needed. Imports are
under com.llamalad7.mixinextras. Docs: https://github.com/LlamaLad7/MixinExtras/wiki
@ModifyExpressionValue - change what a call/field read returns
@ModifyExpressionValue(
method = "travelInWater",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getSpeed()F")
)
private float examplemod$fasterInWater(float original) {
return original * 1.2f;
}
Multiple mods can chain @ModifyExpressionValue on the same expression; each receives the
previous mod's result. A @Redirect on the same call would conflict and crash.
@WrapOperation - wrap a call; call the original (or not)
@WrapOperation(
method = "hurtServer",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;actuallyHurt(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;F)V")
)
private void examplemod$logDamage(LivingEntity self, ServerLevel level, DamageSource source,
float amount, Operation<Void> original) {
ExampleMod.LOGGER.debug("{} took {} damage", self, amount);
original.call(self, level, source, amount); // keep the chain intact
}
- Parameters: the receiver (for non-static calls), then the call's arguments, then
Operation<R>whereRis the boxed return type (Voidforvoid). - Always call
original.call(...)unless skipping the call is the feature. Not calling it silently disables every other mod's wrapper on the same call.
@WrapWithCondition - conditionally skip a void call
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, call args... */) {
return !ExampleConfig.get().hideThing; // false = skip the call
}
@Local - read a local variable without @ModifyVariable
@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }
On 26.1+ the game ships parameter and local names, so @Local(name = ...) is reliable there.
Pre-26.1 (no local names in obfuscated jars) prefer @Local(ordinal = n) by type, and check
it with mixin.debug.export.
Other useful MixinExtras tools
@ModifyReturnValue (change a method's return value at RETURN, chains with other mods),
@ModifyReceiver, @WrapMethod (wrap a whole method), @Share (pass a value between two
injectors in the same method), @Cancellable (cancel from inside a @WrapOperation).
@Redirect and @ModifyVariable - only when nothing else fits
@Redirectreplaces a call. Only one mod can redirect a given call; a second mod's@Redirect(or a@WrapOperationapplied on top of a redirected call in some orders) fails with a hard crash. Use@WrapOperationinstead.@ModifyVariabletargets a local by type and ordinal, which shifts between versions. Use@ModifyExpressionValueon the expression that produces the value, or@Localto read it.@Overwritereplaces the whole method: incompatible with every other mod touching it. Do not use it in a public mod.
Accessors and invokers
package com.example.examplemod.mixin.accessor;
@Mixin(LivingEntity.class)
public interface LivingEntityAccessor {
@Accessor("noJumpDelay")
int examplemod$getNoJumpDelay();
@Accessor("noJumpDelay")
void examplemod$setNoJumpDelay(int value);
@Invoker("getJumpPower")
float examplemod$invokeGetJumpPower();
}
// usage
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
- Instance accessor methods get the mod-id prefix; static ones (
static T getX() { throw new AssertionError(); }) do not need it. @Mutableon a setter removesfinalfrom the target field.- Constructor invoker:
@Invoker("<init>") static Foo examplemod$new(...). - Accessors work on other mods' classes too; class tweakers only work on vanilla.
Class tweakers (formerly access wideners)
Needed to subclass a final class, override a final method, or reference a non-public class.
Requires Loader 0.18+ and Loom 1.12+; older setups use the same syntax under the name
"access widener" with an accessWidener v2 named header.
src/main/resources/examplemod.classtweaker:
classTweaker v1 official
accessible class net/minecraft/world/inventory/MenuType$MenuSupplier
accessible method net/minecraft/network/chat/TextColor formatValue ()Ljava/lang/String;
accessible field net/minecraft/world/damagesource/DamageSources damageTypes Lnet/minecraft/core/Registry;
Wire it up in both places, then refresh Gradle and re-run genSources:
loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"
Run ./gradlew validateAccessWidener - invalid entries are otherwise silently ignored. Use
transitive- directives only when mods depending on yours must see the change. Header details,
v2 features (interface injection, enum extension): https://docs.fabricmc.net/develop/class-tweakers/
Cross-mod compatibility checklist
- Inject at the narrowest point that works; never cancel at
HEADof a widely used method (tick,render,hurt,travel) unless you are sure. - Prefer injectors that chain (
@ModifyExpressionValue,@ModifyReturnValue,@WrapOperation) over injectors that replace (@Redirect,@Overwrite). - Use
@Unique+ mod-id prefix for all added state. - Avoid
ordinaland@At("TAIL")when@At("RETURN")works. - Use
@Mixin(value = X.class, priority = ...)only to resolve a known conflict; document it. - Optional mixins targeting another mod: put them in a separate config and gate them with a
mixin plugin (
IMixinConfigPlugin#shouldApplyMixincheckingFabricLoader.getInstance().isModLoaded("othermod")), or use"require": 0on that injector. - Known heavy patchers (Sodium, Iris, Lithium, ImmediatelyFast) replace large parts of the renderer and game logic. If you inject into rendering or entity ticking, test with them.
Debugging mixins
Add JVM args to the dev run configs:
loom {
runs {
configureEach {
vmArg "-Dmixin.debug.export=true" // writes transformed classes to run/.mixin.out/
vmArg "-Dmixin.debug.verbose=true"
vmArg "-Dmixin.dumpTargetOnFailure=true"
}
}
}
run/.mixin.out/class/...contains the transformed classes - decompile them to see exactly what your injector produced and in which order other mods' injectors ran.-Dmixin.checks.interfaces=trueverifies that mixins implement interface methods.- Error decoding (target not found,
InvalidInjectionException, conflicting redirects): see thefabric-crash-debuggingskill.
Common mistakes
- Writing a mixin for something Fabric API already has an event for (HUD, ticks, block use, tooltips, commands, loot tables).
- Guessing a method name or descriptor from memory or a different version/mappings set. Always check generated sources first.
- Using Yarn names (
MinecraftClient,PlayerEntity) in a Mojang-named project or vice versa. @Redirectwhere@WrapOperation/@ModifyExpressionValuewould do;@Overwritein a public mod.cancellable = true+ci.cancel()atHEADas the default way to "change" behaviour.- Forgetting
original.call(...)in@WrapOperation. - Wrong
CallbackInfoReturnablegeneric (primitive instead of boxed), or missing the target's parameters beforeciwhen you need them. - Mixin class not listed in the config, listed in the wrong section (
mixinsvsclient), or in a package different from the config'spackage. - Client targets in the common config: crashes dedicated servers at startup.
- Referencing a mixin class directly from normal code (
new LivingEntityMixin()), or putting non-mixin helper classes inside the mixin package. - Non-
@Unique, non-prefixed fields that collide with another mod's mixin. - Turning off
defaultRequireto "fix" a failing mixin - this hides the bug and ships a mod that silently does nothing. - Class tweaker declared in only one of
build.gradle/fabric.mod.json, or not validated.