New: see how many SpigotMC listing viewers end up running your plugin →

Guides · Engineering

Obfuscating a Minecraft plugin with ProGuard and Gradle (without breaking stack traces)

A working ProGuard setup for Spigot and Paper plugins with Gradle: the task, the keep rules Bukkit needs, and readable stack traces with mappings.

Updated · by Skilled · 3 min read

Premium plugins get leaked, decompiled and re-uploaded. Obfuscation doesn't make that impossible, but it makes reading and modifying your code much harder, and it shrinks the jar as a bonus. The cost: stack traces from servers become at a.b.c(SourceFile:516), unless you plan for it.

This is the setup we use for our own plugins: Gradle with the shadow plugin, then ProGuard on the shaded jar.

The Gradle task

ProGuard runs on the jar shadow builds, with your libraries already inside and relocated. Add the ProGuard Gradle plugin to the build script's classpath:

// build.gradle.kts
buildscript {
    repositories { mavenCentral() }
    dependencies { classpath("com.guardsquare:proguard-gradle:7.10.0") }
}

Then a task that reads the shadow jar and writes the obfuscated one:

val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) {
    dependsOn(tasks.shadowJar)
    configuration("proguard.pro")
    injars(tasks.shadowJar.flatMap { it.archiveFile })
    outjars(layout.buildDirectory.file("libs/${project.name}-${project.version}-obfuscated.jar"))
    // The JDK and the server API: ProGuard needs them to understand your code, and their names stay.
    val jdk = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }.get()
        .metadata.installationPath.dir("jmods").asFile
    for (module in listOf("java.base", "java.logging", "java.sql", "java.desktop")) {
        libraryjars(mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), File(jdk, "$module.jmod"))
    }
    libraryjars(configurations.compileClasspath)
    printmapping(layout.projectDirectory.file("mappings/${project.version}.txt"))
}

libraryjars(configurations.compileClasspath) adds the Spigot or Paper API (your compileOnly dependencies) as a library: it's on the server, not in your jar, so ProGuard must not rename calls into it. Add any other JDK module your code uses to the list.

The rules Bukkit needs

Bukkit finds parts of your plugin by name or by reflection. Those must survive obfuscation:

# proguard.pro
-dontoptimize
-dontnote
-dontwarn

# Annotations (event handlers), generics, inner classes; line numbers for stack traces.
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,Record,Exceptions,SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

# The main class named in plugin.yml.
-keep class com.example.myplugin.MyPlugin

# Bukkit calls @EventHandler methods by reflection: keep them (their names can change).
-keepclassmembers,allowobfuscation class * {
    @org.bukkit.event.EventHandler <methods>;
}

# Enum.valueOf needs values() and valueOf().
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

Why -dontoptimize? Optimisation rewrites your code, and the code that runs on servers is then not quite the code you tested. Renaming and shrinking give most of the protection with none of that risk.

Other things to keep, if you use them:

  • ConfigurationSerializable classes: Bukkit calls their static deserialize or valueOf and the constructor that takes a Map. Keep those members.
  • Anything you load by name: Class.forName("..."), reflection on your own classes, classes listed in config files.
  • Shaded libraries that use reflection, like bStats or Gson models: keep their packages, or the classes Gson serialises.
  • Your analytics SDK: libraries that report your plugin's own class names (like PluginAnalytics) should stay readable, so keep their relocated package: -keep class com.example.myplugin.libs.analytics.** { *; }.

Test the obfuscated jar, not the normal one

Run the obfuscated jar on a real server before every release. Most ProGuard problems show up at startup (NoSuchMethodError, ClassNotFoundException) or the first time a feature uses reflection. A quick checklist: the plugin enables, commands work, a listener fires, config loads, data saves and loads.

Keep stack traces readable

printmapping writes a file mapping every new name back to the original, a different one for every build. Keep the mapping of every version you release: commit it, or store it next to the release. Without it, a stack trace from that version can't be read.

To read a stack trace by hand, ProGuard ships retrace:

retrace mappings/1.4.0.txt stacktrace.txt

That works when a server owner sends you a trace. For errors you never hear about, PluginAnalytics applies the mapping for you: upload each version's mapping from your build, and the errors, performance data and bug reports from every server show your real class and method names, including data that arrived before you uploaded it. It's one HTTP request after ProGuard:

val uploadMapping by tasks.registering {
    dependsOn(proguardJar)
    doLast {
        val token = providers.gradleProperty("pluginanalytics.token").orNull ?: return@doLast
        val c = java.net.URI("https://pluginanalytics.dev/api/v1/mappings?version=${project.version}").toURL()
            .openConnection() as java.net.HttpURLConnection
        c.requestMethod = "POST"
        c.doOutput = true
        c.setRequestProperty("Authorization", "Bearer $token")
        c.outputStream.use { it.write(file("mappings/${project.version}.txt").readBytes()) }
        check(c.responseCode == 200) { "Mapping upload failed: ${c.responseCode}" }
    }
}

The version must match the version in your plugin.yml, which is what servers report.

Summary

  • Shadow first, ProGuard on the shaded jar, the server API as a library jar.
  • Keep the main class, @EventHandler methods, enum methods and anything reached by reflection.
  • Rename and shrink; skip optimisation.
  • Keep line numbers and the mapping of every release.
  • Test the obfuscated jar on a real server before you ship it.
Skilled

Makes Minecraft plugins (including The Sift on SpigotMC) and PluginAnalytics, so other plugin developers can see how their plugins are found, installed and used.

More guides

Engineering

Error tracking for Minecraft plugins: find the bug before the 1-star review

How Spigot and Paper plugin exceptions reach the console, why waiting for reports fails, and how to collect, group and fix errors from every server.

4 min read
Engineering

Which part of your Minecraft plugin causes lag (and how to fix it)

Find the part of your Spigot or Paper plugin that causes lag: MSPT, spark profiles, the usual suspects and how to see its cost across all servers.

4 min read

See how your own plugin is found and used

Free for your first plugin. One line in onEnable.