Fabric mixins
Deep reference for Fabric/SpongePowered Mixins in Minecraft 1.20.1 modding. Use this skill whenever the user asks about Mixin injection, @Inject, @Redirect, @ModifyArg, @ModifyVariable, @Overwrite, @Accessor, @Invoker, capturing locals, debugging mixin failures, mixin conflicts, or any advanced bytecode-manipulation pattern. Triggers on: "mixin", "@Inject", "@Redirect", "CallbackInfo", "injection point", "capture locals", "accessor mixin", "invoker mixin", "@At INVOKE", "mixin not applying", "mixin crash", or any question about hooking into Minecraft internals at the bytecode level. Part of the fabric-mc-modding skill family.From its SKILL.md
npx -y skills add PSB1234/Fabric-Minecraft-Skill-1.20.1 --skill fabric-mixinsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.0 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it
Fabric Mixins — 1.20.1 Deep Reference
When to use Mixins vs Events: Always prefer a Fabric API event if one exists. Mixins are for cases where no event covers your need. See the
fabric-eventsskill for available events.
Setup
mymod.mixins.json (at src/main/resources/):
{
"required": true,
"minVersion": "0.8",
"package": "com.example.mymod.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": ["SomeMixin"],
"client": ["ClientOnlyMixin"],
"server": [],
"injectors": { "defaultRequire": 1 }
}
mixins— applied on both sidesclient— applied only on the logical clientserver— applied only on the dedicated server"defaultRequire": 1— fails at load if any injection doesn't match (recommended)
@Inject — the workhorse
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
// HEAD: top of method, before any code
@Inject(at = @At("HEAD"), method = "onDeath")
private void beforeDeath(DamageSource source, CallbackInfo ci) { }
// RETURN: fires before *each* return statement
@Inject(at = @At("RETURN"), method = "getHealth")
private void afterGetHealth(CallbackInfoReturnable<Float> cir) {
// cir.getReturnValue() = what was about to be returned
}
// TAIL: fires before the *final* return only
@Inject(at = @At("TAIL"), method = "tick")
private void atEnd(CallbackInfo ci) { }
// Cancel execution mid-method
@Inject(at = @At("HEAD"), method = "damage", cancellable = true)
private void cancelDamage(DamageSource src, float amount, CallbackInfoReturnable<Boolean> cir) {
if (src.isOf(DamageTypes.FALL)) {
cir.setReturnValue(false); // cancel + set return value
}
}
}
Targeting a Specific Overload
If a class has multiple methods with the same name, add the descriptor:
// method = "damage(Lnet/minecraft/entity/damage/DamageSource;F)Z"
// Format: methodName(ArgTypes)ReturnType
// Use L...; for objects, [ for arrays, Z=boolean I=int F=float V=void etc.
@At — Injection Points
| Value | Fires at |
|---|---|
"HEAD" | Top of method |
"RETURN" | Every return statement |
"TAIL" | The final return only |
"INVOKE" | Before a specific method call (use target=) |
"INVOKE_ASSIGN" | After a specific method call whose return is assigned |
"FIELD" | Field read or write (use target=, opcode=) |
"NEW" | Object instantiation (new Foo(...)) |
"JUMP" | A branch instruction (advanced) |
"CONSTANT" | A compile-time constant (advanced) |
INVOKE target descriptor format
@At(value = "INVOKE",
target = "Lnet/minecraft/entity/LivingEntity;heal(F)V")
// "L<class/path>;methodName(args)return"
// dots → slashes in class paths
Shifting injection point
// Fire 2 instructions AFTER the INVOKE target
@At(value = "INVOKE", target = "...", shift = At.Shift.AFTER, by = 2)
@Redirect — Replace a Method Call
Replaces one specific call site inside a method. Your method body becomes the call.
@Mixin(PlayerEntity.class)
public abstract class PlayerMixin {
@Redirect(
method = "tick",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/entity/LivingEntity;heal(F)V")
)
private void redirectHeal(LivingEntity instance, float amount) {
// replace the heal() call — do NOT call instance.heal() to cancel it,
// or call it with modified args:
instance.heal(amount * 2);
}
}
⚠️
@Redirectcompletely replaces the call — only ONE mod can redirect the same call site. Prefer@ModifyArgor@InjectatINVOKEfor better compatibility.
@ModifyArg — Change One Argument to a Method Call
Better compatibility than @Redirect when you only need to tweak one argument:
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
// Intercept the heal() call inside tick() and double the amount
@ModifyArg(
method = "tick",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/entity/LivingEntity;heal(F)V"),
index = 0 // which argument to intercept (0-based)
)
private float modifyHealAmount(float amount) {
return amount * 2;
}
}
For methods with multiple args of the same type, use index to pick the right one.
@ModifyVariable — Change a Local Variable
Changes the value of a local variable at a specific point:
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@ModifyVariable(
method = "damage",
at = @At("HEAD"),
argsOnly = true, // only look in method parameters
index = 2 // slot index in the local variable table
)
private float modifyDamageAmount(float amount) {
return amount * 0.5f; // halve all incoming damage
}
}
Use Bytecode Viewer or --printFrames to find the correct index.
Capturing Local Variables with @Inject
When you need access to a local variable inside the target method:
@Mixin(SomeMCClass.class)
public abstract class SomeMixin {
@Inject(
method = "targetMethod",
at = @At(value = "INVOKE",
target = "Lsome/Class;someCall()V"),
locals = LocalCapture.CAPTURE_FAILSOFT // or CAPTURE_FAILHARD / PRINT
)
private void onSomeCall(CallbackInfo ci,
// After ci, list ALL locals in table order at that injection point:
int localInt, float localFloat, SomeType localObj) {
// use localObj here
}
}
LocalCapture options:
CAPTURE_FAILHARD— crash if locals don't match (use during dev)CAPTURE_FAILSOFT— log warning and skip injection on mismatchPRINT— prints the local variable table to console (for debugging)
@Accessor — Read/Write Private Fields
Create a mixin interface (not class) to expose private fields:
@Mixin(PlayerEntity.class)
public interface PlayerEntityAccessor {
// Getter
@Accessor("hungerManager")
HungerManager getHungerManager();
// Setter
@Accessor("hungerManager")
void setHungerManager(HungerManager manager);
}
// Usage from anywhere (cast):
HungerManager hm = ((PlayerEntityAccessor) player).getHungerManager();
@Invoker — Call Private Methods
@Mixin(SheepEntity.class)
public interface SheepEntityInvoker {
@Invoker("setHeadRollingTimeLeft")
void invokeSetHeadRollingTimeLeft(int ticks);
}
// Usage:
((SheepEntityInvoker) sheepEntity).invokeSetHeadRollingTimeLeft(10);
@Overwrite — Full Method Replacement (Last Resort)
Completely replaces a method. Extremely fragile — breaks if Mojang changes the method.
@Mixin(SomeClass.class)
public abstract class SomeMixin {
@Overwrite
public void targetMethod(int arg) {
// your entire replacement implementation
}
}
⛔ Avoid
@Overwrite. It's incompatible with any other mod touching the same method. Use@Inject+cancellable = trueinstead when possible.
Shadow — Access Target Class Members
@Shadow lets your mixin reference fields/methods of the target class:
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@Shadow
private int stuckArrowTimer; // access a private field
@Shadow
public abstract float getHealth(); // access a method
@Shadow @Final
private static Map<EntityAttribute, EntityAttributeInstance> attributes; // final field
@Inject(at = @At("HEAD"), method = "tick")
private void onTick(CallbackInfo ci) {
// use stuckArrowTimer directly
if (stuckArrowTimer > 0) { /* ... */ }
}
}
Mixin Priorities & Compatibility
@Mixin(value = LivingEntity.class, priority = 900)
// Default priority = 1000. Lower = applied first.
// Use lower priority to run before other mods, higher to run after.
For @Inject at the same point from two mods — both fire (order by priority).
For @Redirect at the same call site — only one can exist. Use @ModifyArg instead.
Debugging Mixin Failures
Symptom: Mixin not applying at all
- Verify class name is in the correct array in
mymod.mixins.json(mixins/client/server) - Check package matches
"package"inmixins.json - Enable mixin debug output in
gradle.properties:mixin.debug=true mixin.debug.verbose=true mixin.debug.export=true # writes patched classes to .mixin.out/ folder
Symptom: @At(INVOKE) target not found
- Target descriptor uses slashes, not dots:
Lnet/minecraft/entity/LivingEntity;heal(F)V - Double-check the method signature with Bytecode Viewer / Fernflower /
genSources - If the method is overloaded, the descriptor must be exact
Symptom: Local capture mismatch crash
- Use
LocalCapture.PRINTto dump the local variable table, then fix your parameter list
Symptom: ClassCastException with Accessor
- The accessor interface must be applied via
@Mixin— make sure it's inmixins.json - Cast with
instanceofcheck if unsure:if (entity instanceof PlayerEntityAccessor acc)
Symptom: Mixin applied but wrong result
- Add
mixin.debug.export=trueand inspect the.mixin.out/folder — decompile the class to verify your injection landed where expected
Mixin + Custom Event Pattern
The canonical way to expose an injection point to other mods:
// 1. Define the event interface (in non-mixin package)
@FunctionalInterface
public interface SheepShearCallback {
Event<SheepShearCallback> EVENT = EventFactory.createArrayBacked(
SheepShearCallback.class,
listeners -> (sheep, player) -> {
for (SheepShearCallback l : listeners) l.onShear(sheep, player);
}
);
void onShear(SheepEntity sheep, PlayerEntity player);
}
// 2. Fire it from a mixin
@Mixin(SheepEntity.class)
public abstract class SheepEntityMixin {
@Inject(method = "interactMob",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/entity/passive/SheepEntity;sheared(Lnet/minecraft/sound/SoundCategory;)V"))
private void onShear(PlayerEntity player, Hand hand, CallbackInfoReturnable<ActionResult> cir) {
SheepShearCallback.EVENT.invoker().onShear((SheepEntity)(Object)this, player);
}
}
Scripts
| Script | What it does |
|---|---|
scripts/add_mixin.py | Scaffold a mixin class + auto-update mixins.json |
python scripts/add_mixin.py \
--mod-id mymod --group com.example \
--target net.minecraft.entity.LivingEntity \
--name LivingEntityMixin \
--side both \
--inject-method onDeath --inject-at HEAD
References
Load these when needed — they are not in the main SKILL.md to keep it concise:
| File | When to load |
|---|---|
references/jvm-descriptors.md | Writing @At(target=...) descriptors; full type table + common MC class paths |
references/mixin-extras.md | Using @ModifyReturnValue, @WrapOperation, @WrapWithCondition, @Local, @Share |
Further Reading
- Mixin docs (SpongePowered): https://github.com/SpongePowered/Mixin/wiki
- Fabric Mixin guide: https://docs.fabricmc.net/develop/mixin/basics
- Mixin cheatsheet: https://fabricmc.net/wiki/tutorial:mixin_cheatsheet
- Mixin extras (more annotations): https://github.com/LlamaLad7/MixinExtras
Related Skills
fabric-mc-modding— project setup, gradle, general APIfabric-events— prefer events over mixins when available
What ships with it: 3 files
14.9 KB alongside SKILL.md, 1 of them executable
references/
- jvm-descriptors.md4.0 KB
- mixin-extras.md6.3 KB
scripts/
- add_mixin.pyruns4.7 KB