rymga ← Back to Lock Master

Renaming & layout

Names carry more meaning than any other part of a class file. getLicenceKey tells a reader everything; ad tells them nothing. Renaming strips that meaning away — from classes, methods, fields, and the package tree that holds them.

Renaming

Turn it on in techniques, and every class, method and field that you haven't explicitly kept is given a fresh, meaningless name. References are rewritten to match, so the code still links and runs — it just stops describing itself.

{ "techniques": { "rename": true } }

Before & after

Real output. A small Cart that holds a Money subtotal, run through renaming and decompiled back to Java. Watch the types, fields, methods and even the import all collapse to two-letter names — while the wiring between them stays exactly the same:

Your code
package shop.model;
import shop.util.Money;

public class Cart {
    private Money subtotal = new Money(0L);
    private int itemCount = 0;

    public void addItem(Money money) {
        this.subtotal = this.subtotal.plus(money);
        ++this.itemCount;
    }

    public Money subtotal() {
        return this.subtotal;
    }

    public int itemCount() {
        return this.itemCount;
    }
}
After — rename
// Money  ->  aa   (its own package)
package aa;
public final class aa {
    public final /* synthetic */ long aa;
    public /* synthetic */ aa(long l) {
        this.aa = l;
    }
    public /* synthetic */ aa aa(aa aa2) {
        return new aa(this.aa + aa2.aa);
    }
    public /* synthetic */ long ab() {
        return this.aa;
    }
}

// Cart   ->  ab   (references to Money become aa.aa)
package ab;
import aa.aa;
public class ab {
    public /* synthetic */ aa ab;
    public /* synthetic */ int ac;
    public /* synthetic */ ab() {
        this.ab = new aa(0L);
        this.ac = 0;
    }
    public /* synthetic */ void ac(aa aa2) {
        this.ab = this.ab.aa(aa2);
        ++this.ac;
    }
    public /* synthetic */ aa ad() {
        return this.ab;
    }
    public /* synthetic */ int ae() {
        return this.ac;
    }
}

The /* synthetic */ notes are the decompiler pointing out that even the members' metadata has been marked to throw tools off — there's no Cart, Money, subtotal or addItem left to search for.

Method overloading

Renaming gives every method a fresh name. By default those names are all distinct — but they don't have to be. The JVM tells two methods apart by their full descriptor (parameter types and return type), not just their name, so one class can legally hold many methods that share a name and differ only in signature. Turn on rename.overloadMethods and the renamer reuses one short name wherever it safely can:

{ "rename": { "overloadMethods": true } }

The result is a class packed with same-named methods — including several that differ only by return type. That last case is legal bytecode but is not legal Java source: no decompiler can turn it back into code that recompiles. Here the same Wallet as above, renamed with overloading on and decompiled:

Your code
public final class Wallet {
    private long balance;

    public long getBalance() {
        return balance;
    }

    public void credit(long amount) {
        balance += amount;
    }

    public void debit(long amount) {
        balance -= amount;
    }

    public Wallet copy() {
        Wallet w = new Wallet();
        w.balance = balance;
        return w;
    }

    public boolean isEmpty() {
        return balance == 0;
    }
}
After — rename + overloading
public final class a {
    private long a;

    public long a() {        // getBalance   ()J
        return this.a;
    }

    public void a(long var1) {   // credit    (J)V
        this.a += var1;
    }

    public void b(long var1) {   // debit     (J)V   — shares (J)V with credit, so it can't also be a(long)
        this.a -= var1;
    }

    public a a() {           // copy         ()La;
        a var1 = new a();
        var1.a = this.a;
        return var1;
    }

    public boolean a() {     // isEmpty      ()Z
        return this.a == 0L;
    }
}

//  Three methods named a() with no parameters, differing only by return type
//  (long, a, boolean). Legal in bytecode — the JVM resolves by full descriptor —
//  but ILLEGAL Java: this source will not recompile. The decompiler has no choice
//  but to emit it. credit/debit share the descriptor (J)V, so they must differ (a / b).

Three methods called a() with no parameters, differing only in whether they return long, a or boolean. A human reading the decompiler output has to hold all of that in their head; a tool that tries to recompile it simply fails. It's safe by construction — two methods only ever share a name when no class resolves both of them with the same descriptor, so overrides, real overloads and inherited signatures are never disturbed. Opt-in: overloadMethods defaults to false.

keep vs. rename.exclude

Renaming breaks anything found by its name at runtime — reflection, class loading by string, a plugin main the server looks up, config-mapped fields. There are two ways to spare a class, and picking the right one is the single most common source of confusion:

  • rename.exclude — the everyday tool. The class and its method/field names stay exactly as written, so anything that resolves it by name still works — but flow and string encryption still protect the bodies. This is what you want for entry points, public APIs, reflection targets: keep the names, protect the insides. And you can keep only part of the identity — see granular exclusions below: an entry point usually only needs its class name kept, so its fields and methods can still be obfuscated.
  • keep — absolute, last resort. A kept class is touched by nothing: no rename, no flow, no string encryption, no sealing. It ships fully readable. Use it only when the bytecode genuinely must not change — serialized forms, a byte-stable wire protocol, or a third-party library you'd rather leave alone entirely.

So a plugin main, a public API, an event listener registered by name — all rename.exclude, not keep. Reach for keep only when you also want to give up flow and string protection on that class on purpose.

{
  "rename": {
    "exclude": ["com/you/MyPlugin", "com/you/api/", "com/you/EventListener"]
  },
  "keep": {
    "packages": ["com/you/model"],
    "classes":  ["com/you/wire/Protocol"],
    "members": [
      { "classPattern": "com/you/**", "methodPattern": "on*" }
    ]
  }
}
  • rename.exclude — class + member names preserved (by internal name or package prefix), bodies still obfuscated.
  • keep.packages — freeze whole package trees by internal prefix, completely untouched.
  • keep.classes — freeze specific classes by internal name, completely untouched.
  • keep.members — fine-grained: a classPattern plus a methodPattern and/or fieldPattern, with an optional descriptor. Keeps just those members' names (the class is otherwise renamed). Patterns use * (one segment) and ** (any depth).
Names are internal names. Use JVM form — slashes, not dots: com/you/MyPlugin, never com.you.MyPlugin. A trailing slash (com/you/api/) matches a whole package; a bare name matches one class and its inner classes. The same matcher works in rename.exclude, flow.exclude, stringEncryptionExclude and classGuard.encrypt.

Granular exclusions — keep only what you must

By default, excluding a class keeps its whole identity: class name, field names and method names. That's often more than you need — and every name you keep is a name a reader can trace. A plugin main only needs its class name preserved so the server can load it; its fields (licenseService, apiClient…) and its own methods can and should still be obfuscated. Keeping them just to keep the class loadable hands the reverser a map for free.

So rename.exclude entries take an optional scope suffix after # — any combination of name, fields, methods:

{
  "rename": {
    "exclude": [
      "com/you/MyPlugin#name",
      "com/you/api/#name,methods",
      "com/you/Config#fields",
      "com/you/Legacy"
    ]
  }
}
SuffixKeepsUse for
#nameclass name onlyplugin main, executable-jar Main-Class, reflection-loaded class — loadable by name, everything inside obfuscated
#name,methodsclass + method namesa public API other code calls by method name
#fieldsfield names onlya class whose fields are bound by name (config/serialization) but whose methods are yours to hide
(none)everythingbackward compatible — a bare entry keeps name + fields + methods
Framework callbacks are safe regardless. A #name-only main still keeps onEnable, onDisable and any method that overrides a platform type — those are pinned automatically because the runtime resolves them against the library, not by your kept scope. You only need to keep the class name; the engine handles the callbacks.

Package strategies

Renaming doesn't just change class names — it can rebuild the whole package tree. That reshaping is its own layer of obfuscation: it destroys the architectural map that package structure hands to a reader. Here's the same six-class jar under each packageStrategy. This is the real jar listing each time (your shop/App entry point, in rename.exclude, stays put):

Original layout
demo/Heavy.class
demo/Report.class
demo/Sample.class
shop/App.class
shop/model/Cart.class
shop/util/Money.class

ROOT — no packages at all

Everything is hoisted into the default package. Flat, anonymous, and it makes tools that assume a package structure work harder:

packageStrategy: ROOT
aa.class
ab.class
ac.class
ad.class
ae.class
shop/App.class

FLAT — one shared package

Every class lands in a single generated package, so nothing about the grouping survives:

packageStrategy: FLAT
aa/aa.class
aa/ab.class
aa/ac.class
aa/ad.class
aa/ae.class
shop/App.class

WRAPPER — one package per class (default)

Each class gets its own single-segment package. A tidy default that keeps class-per-file tools happy while giving nothing away:

packageStrategy: WRAPPER (default)
aa/aa.class
ab/ab.class
ac/ac.class
ad/ad.class
ae/ae.class
shop/App.class

RANDOM — scattered across a pool

Classes are spread at random across a pool of generated packages (size set by packagePool), so unrelated classes share packages and related ones don't:

packageStrategy: RANDOM
aa/aa.class
aa/ab.class
aa/ac.class
ac/ae.class
ad/ad.class
shop/App.class

MIRROR — same shape, new names

Keeps the original nesting depth but renames every segment. Notice the last two classes stay two levels deep, mirroring the shop/model and shop/util they came from — useful when something expects a particular package depth:

packageStrategy: MIRROR
aa/aa.class
ab/ab.class
ac/ac.class
ad/ae/ad.class
af/ag/ae.class
shop/App.class

KEEP — rename classes, keep their package

The most conservative option: every class stays in its original package and only its simple name changes. The package tree is untouched — compare it to the original layout above, it's the same tree with renamed leaves:

packageStrategy: KEEP
demo/aa.class
demo/ab.class
demo/ac.class
shop/App.class
shop/model/ad.class
shop/util/ae.class
When you need KEEP. Java grants protected and package-private members access from within the same package. The other strategies move your classes into new (or single, or per-class) packages, so two classes that were package-mates can land in different packages — and a call that relied on that shared package now throws IllegalAccessError at runtime. If your plugin uses protected or package-private access across its own classes and you see access errors after obfuscation, switch to KEEP. FLAT (everything co-located in one package) also fixes it and hides more, but KEEP is the safest because it changes nothing about the layout. The trade-off: keeping the original packages preserves author/plugin structure (com/yourname/yourplugin/…) — exactly the map the other strategies erase — so reach for it only when access semantics force it, not as a default.
Only some packages? You rarely need KEEP for the whole jar. If just one or two packages rely on protected/package-private access, list their prefixes in preservePackages instead: those packages keep their original path while everything else still follows your packageStrategy. That fixes the access break while leaking as little structure as possible. It's matched by prefix, JVM form (slashes) — e.g. com/you/internal. Note this is different from keep.packages, which leaves names untouched entirely; here the classes are still renamed, they just don't move.

Main class & plugin descriptors

Your plugin's entry class is named twice: once as a .class, and once as a main reference inside the platform descriptor that tells the loader where to start. Renaming only the class would leave the descriptor pointing at a name that no longer exists, so the plugin wouldn't load. Lock Master keeps the two in sync: when renaming is on, it rewrites the main reference to the renamed class in every descriptor it finds —

  • plugin.yml — Bukkit / Spigot / Paper
  • paper-plugin.yml — modern Paper
  • bungee.yml — BungeeCord / Waterfall
  • velocity-plugin.json — Velocity

So a multi-platform jar can have its entry class renamed on every platform at once, and you no longer need to keep the main just to keep the jar loadable. This is on by default; set rewritePluginDescriptors: false to leave descriptors untouched.

Don't rename a class the runtime resolves by name. Descriptor rewriting covers the main reference, but a class the runtime loads by name any other way — a ServiceLoader provider, a reflectively-loaded class, the Main-Class of an executable jar — has no descriptor to sync. Leave those in rename.exclude. And never seal such a class with ClassGuard — it must stay cleartext under its real name. Flow and string encryption still protect it.
Annotation-driven Velocity plugins. Velocity resolves the entry class from the generated velocity-plugin.json, which is rewritten here. If your build wires Velocity purely through the @Plugin annotation with no JSON descriptor, verify the module still loads after obfuscation and tell us if it doesn't.

rename fields

The rename block tunes how names are generated. Defaults are fine for most people:

FieldWhat it does
excludeA list of classes and packages to leave un-renamed — mix them freely, as many as you need: a bare name (com/you/Main) is one class and its inner classes, a pkg/ prefix a whole package tree. Their class and member names are preserved, so anything resolved by name still works, while flow and string encryption still apply. The per-technique exclusion for renaming, on top of keep. Add a scope suffix (#name, #fields, #methods) to keep only part of the identity — e.g. com/you/Main#name keeps just the class name and obfuscates its members. This is how you keep an entry point or public API loadable without leaving it unprotected.
charsThe character set for new names (default a–z). Swap in confusable or non-Latin glyphs to make names harder to read and tell apart.
deepMinimum length of generated names (they grow beyond it only when a short pool runs out).
packageStrategyHow the package tree is rebuilt: ROOT, FLAT, WRAPPER (default), RANDOM, MIRROR, KEEP — shown above.
packageDepthDepth of the generated package structure.
preservePackagesPrefixes (JVM form) whose classes keep their original package while the rest follow packageStrategy — a per-package KEEP. Classes are still renamed; see above.
packagePoolFor RANDOM: how many packages to spread classes across.
rewritePluginDescriptorsKeep the main reference in plugin descriptors in sync with the renamed main class (plugin.yml, paper-plugin.yml, bungee.yml, velocity-plugin.json). Default true — see above.

Renaming is deterministic when you set a seed — the same input builds byte-for-byte identically. It does not pin names across releases, though: obfuscated names are handed out in order, so they shift when your code changes. If other code compiles against a public API, keep that API so its names never change at all.