Back to blog

How to Obfuscate a Minecraft Plugin with ProGuard (Gradle Guide)

Stephen, co-founder

A plain plugin jar is easy to read. Drop it into any Java decompiler and you get back something very close to your original source: class names, method names, your license check and all. Obfuscation makes that output much harder to follow, which raises the effort needed to crack or copy your plugin.

Most guides stop at a list of tools. This one gives you a working ProGuard setup for a Paper or Spigot plugin, explains every rule, and covers the things that quietly break a Minecraft plugin after obfuscation.

What obfuscation does (and doesn't) do

ProGuard renames your classes, methods and fields to short meaningless names like a, b and c, and can remove unused code. A decompiled jar becomes a maze of single letters.

It's worth being honest about the limits:

  • It doesn't encrypt strings. Your messages, URLs and config keys stay readable. Commercial tools like Zelix KlassMaster or Allatori, or the open-source Skidfuscator, add string encryption and control-flow obfuscation.
  • It doesn't stop a determined cracker. It slows them down. That's the point: most leakers give up on anything that takes real work.
  • It works best alongside a license check. Obfuscation hides where the check is. A signed license check with server limits makes a shared copy stop working. Neither is much use alone. (How licensing fits in.)

Step 1: Add ProGuard to your Gradle build

This assumes you already build a shaded jar with the Shadow plugin. If not, see adding a license check for the Shadow setup. Add ProGuard to the top of build.gradle.kts:

import proguard.gradle.ProGuardTask

buildscript {
    repositories { mavenCentral() }
    dependencies { classpath("com.guardsquare:proguard-gradle:7.7.0") }
}

Then register a task that takes the shaded jar and writes an obfuscated one:

tasks.register<ProGuardTask>("proguard") {
    dependsOn(tasks.shadowJar)

    injars(tasks.shadowJar.flatMap { it.archiveFile })
    outjars(layout.buildDirectory.file("libs/${project.name}-${project.version}-obf.jar"))

    // The JDK and the server API exist at runtime, so ProGuard reads them but doesn't touch them
    libraryjars(
        mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"),
        "${System.getProperty("java.home")}/jmods/java.base.jmod"
    )
    libraryjars(configurations.compileClasspath)

    printmapping(layout.buildDirectory.file("proguard/mapping.txt"))
    configuration("proguard.pro")
}

Run ./gradlew proguard and ship the -obf.jar.

Two notes. java.base.jmod comes from the JDK running Gradle; if your JDK was built without jmods, point this at one that has them. And because compileClasspath includes the libraries you've shaded, ProGuard will print notes about duplicate classes. They're harmless.

Using Maven? The proguard-maven-plugin takes the same rules file.

Step 2: The rules file

Create proguard.pro next to your build file. This is the part that matters. Bukkit finds several things in your plugin by name or by annotation, and if ProGuard renames or removes them, the plugin breaks at runtime even though it compiled fine.

# Rename and shrink, but don't optimise (see "Marketplace placeholders" below)
-dontoptimize

# Keep annotations and generics; keep line numbers so stack traces can be decoded
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

# Bukkit loads your main class by the name in plugin.yml
-keep public class * extends org.bukkit.plugin.java.JavaPlugin

# Paper plugins: bootstrapper and loader classes named in paper-plugin.yml
-keep class * implements io.papermc.paper.plugin.bootstrap.PluginBootstrap
-keep class * implements io.papermc.paper.plugin.loader.PluginLoader

# Event handlers are found by their annotation and never called directly,
# so stop ProGuard removing them (they can still be renamed)
-keepclassmembers,allowobfuscation class * implements org.bukkit.event.Listener {
    @org.bukkit.event.EventHandler <methods>;
}

# Config-serializable classes are saved and loaded by class name
-keep class * implements org.bukkit.configuration.serialization.ConfigurationSerializable {
    public static ** deserialize(java.util.Map);
    public static ** valueOf(java.util.Map);
    public <init>(java.util.Map);
}

# Enum constants are often looked up by name from config files
-keepclassmembers enum * {
    <fields>;
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# Only obfuscate your own code; leave shaded libraries' names alone
-keep class !com.example.myplugin.** { *; }

Replace com.example.myplugin with your plugin's package.

Why each rule is there

  • The main class is listed by name in plugin.yml. Rename it and the server can't load the plugin.
  • Event handlers are discovered by Bukkit through the @EventHandler annotation. Your code never calls them, so without this rule ProGuard decides they're unused and deletes them. The plugin loads fine, and then nothing happens. This is the most common ProGuard mistake with Minecraft plugins.
  • ConfigurationSerializable classes are written to YAML by class name and rebuilt through deserialize, valueOf or a Map constructor.
  • Enums often come from config: Material-style lookups like MyMode.valueOf("SURVIVAL") fail if the constant was renamed.
  • Libraries you shade, such as Gson-serialised data classes, Kotlin or database drivers, often rely on reflection. Keeping everything outside your own package avoids a whole class of surprises. If you serialise your own classes with Gson or Jackson, keep their field names too.

Step 3: Watch out for marketplace placeholders

If you sell on BuiltByBit or Voxel Shop, your jar probably contains download placeholders like %%__USER__%%, which the marketplace replaces when a buyer downloads it. MC License's library uses one for the license key itself.

Two things can break them:

  • String encryption. If an obfuscator encrypts string constants, the marketplace can't find the placeholder to replace it. ProGuard doesn't encrypt strings, but if you add a tool that does, exclude the classes that hold placeholders.
  • Optimisation. A placeholder field looks to the optimiser like a constant that never changes. It can treat it as one and work out the "was it injected?" check at build time, before the marketplace has done anything. That's why the rules above use -dontoptimize. It's the safe default for plugins.

Step 4: Check the result

Open the -obf.jar in a decompiler (Vineflower, Recaf or JD-GUI all work) and look through it. Your own classes should be single letters, apart from the main class and anything you deliberately kept.

Then test it on a real server: start up, run your commands, trigger your events, save and reload config. Obfuscation bugs show up at runtime, not at compile time.

Decoding stack traces

When a buyer sends you an error from an obfuscated build, the stack trace will be full of a.b.c. The mapping.txt file ProGuard wrote turns it back into real names. Run the trace through ProGuard's ReTrace tool with that file.

Keep a copy of the mapping file for every release you ship, and never publish it. It's the key to undoing the obfuscation.

Where this leaves you

With a license check, sensible server limits and an obfuscated jar, you've covered the easy ways people get round paying for a plugin. If you haven't added the license check yet, here's how. It takes about ten minutes.

Ready to protect your plugins?

Create a free account and get started in minutes.

Get started free