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:
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;
}
}
// 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:
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;
}
}
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
classPatternplus amethodPatternand/orfieldPattern, with an optionaldescriptor. Keeps just those members' names (the class is otherwise renamed). Patterns use*(one segment) and**(any depth).
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"
]
}
}
| Suffix | Keeps | Use for |
|---|---|---|
#name | class name only | plugin main, executable-jar Main-Class, reflection-loaded class — loadable by name, everything inside obfuscated |
#name,methods | class + method names | a public API other code calls by method name |
#fields | field names only | a class whose fields are bound by name (config/serialization) but whose methods are yours to hide |
| (none) | everything | backward compatible — a bare entry keeps name + fields + methods |
#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):
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:
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:
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:
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:
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:
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:
demo/aa.class
demo/ab.class
demo/ac.class
shop/App.class
shop/model/ad.class
shop/util/ae.class
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.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 / Paperpaper-plugin.yml— modern Paperbungee.yml— BungeeCord / Waterfallvelocity-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.
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.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:
| Field | What it does |
|---|---|
| exclude | A 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. |
| chars | The character set for new names (default a–z). Swap in confusable or non-Latin glyphs to make names harder to read and tell apart. |
| deep | Minimum length of generated names (they grow beyond it only when a short pool runs out). |
| packageStrategy | How the package tree is rebuilt: ROOT, FLAT, WRAPPER (default), RANDOM, MIRROR, KEEP — shown above. |
| packageDepth | Depth of the generated package structure. |
| preservePackages | Prefixes (JVM form) whose classes keep their original package while the rest follow packageStrategy — a per-package KEEP. Classes are still renamed; see above. |
| packagePool | For RANDOM: how many packages to spread classes across. |
| rewritePluginDescriptors | Keep 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.