Configuration
One JSON file tells Lock Master what to protect and how hard. Every technique is opt-in — nothing happens unless you ask for it. This page is the map; each layer has its own page with real before/after code for every transform.
com/you/Main, not com.you.Main.
** matches any depth, * a single segment.exclude vs.
keep. Reach for the per-technique exclusion first
(rename.exclude, flow.exclude,
techniques.stringEncryptionExclude, and classGuard.encrypt as an
include-list). rename.exclude keeps a class's name and its method/field names
exactly — so anything found by name (an entry point, a public API, reflection) still resolves —
while flow and string encryption still protect the bodies. That's the everyday tool.
keep is the
absolute last resort: a kept class is touched by nothing (no rename, flow, strings or
sealing) and ships fully readable — use it only when the bytecode must not change at all (serialized forms,
byte-stable protocols, a third-party lib you'd rather not obfuscate).Structure
The top level is a set of optional blocks. Include only the ones you need:
{
"configVersion": 2,
"techniques": { },
"keep": { },
"rename": { },
"flow": { },
"classGuard": { },
"typeDecoupling":{ },
"antiDebug": { },
"libraries": { },
"seed": 123456
}
configVersion— optional: marks the current config contract.techniques— class-file protections (strings, rename, stripping).keep— ABSOLUTE: classes nothing touches at all (last resort).rename— how renaming names things, incl.rename.exclude(+ scopes).flow— control-flow obfuscation, incl.flow.exclude.classGuard— whole-class encryption: theencryptlist (+ nestedantiDump).typeDecoupling— enabler for class encryption: decouple concrete types.antiDebug— lightweight: refuse to run under a debugger.libraries— dependency jars so the class hierarchy resolves.seed— optional: reproducible builds.
jar and mapping exist in the engine but are
handled by the platform in the cloud (your input and output come from the CLI's
--in/--out). mapping is handled by
the platform too — every renaming build emits a name-map file the CLI downloads for
retrace; you don't set its paths.
libraries you do set — see
libraries below.
The layers
Protection stacks in layers, from cheapest to strongest — renaming, string encryption and control-flow harden the class files; class encryption takes them off disk, with type decoupling to reach most of your code and anti-dump to defend them at runtime. Each has a dedicated page that walks through every option with real engine output — one small method or jar, transformed, then decompiled so you see exactly what ships:
- Renaming & layout — strip the meaning
out of class, method, field and package names. Covers
rename,keeprules, and the five package strategies. - String encryption — remove every
readable literal from the class file. Covers the
PER_METHOD,INLINEandPOOLmodes. - Control-flow obfuscation — rewrite how
methods branch, loop and compute so no clean Java reproduces them. Covers presets and all nineteen
flowtechniques. - Class encryption — ship your classes
as encrypted blobs with no bytecode to decompile. Covers
classGuard,antiDebugandwatermark. - Type decoupling (CHF) — the
enabler for the layer above: rewrites your code through auto-generated cleartext interfaces so class encryption
can seal classes you use by concrete type — most of your codebase, not just a few. Covers
typeDecoupling. - Anti-dump — defends the
sealed classes at runtime: layered, fail-closed detection of debuggers and agents, folded into the
decryption key so nothing unseals under observation. Covers
classGuard.antiDump.
techniques
The techniques block holds the class-file-level switches. The two headline ones,
stringEncryption and rename, have their own pages
(strings, renaming).
The rest are stripping and metadata options:
| Field | What it does |
|---|---|
| stringEncryption | Encrypt string literals — NONE / PER_METHOD / INLINE / POOL. See String encryption. |
| stringEncryptionExclude | Classes/packages to leave with readable strings — the per-technique exclusion for string encryption (on top of keep). |
| rename | Rename classes/methods/fields (uses rename + keep). See Renaming & layout. |
| stripDebug | Remove debug info — line numbers and local variable names. Recommended. |
| stripInnerClasses / stripNestInfo | Drop inner-class / nest-mate metadata. |
| synthetic | Mark members synthetic, hiding them from some tools and IDEs. |
| cacheMode | String cache: FAST (cache after first decrypt) or SAFE (re-decrypt and wipe on every access). |
| callerBinding | Key strings to their runtime caller: STACKWALKER (default) or STACKTRACE (cheaper, spoofable). |
| assetPool | Where encrypted string data lives: INLINE (helper class) or EXTERNAL (META-INF/strings.dat). |
| stripKotlinMetadata | Drop Kotlin @Metadata (for Kotlin code). |
| stripAnnotations | Drop invisible (CLASS-retention) annotations; runtime-visible ones are always kept. Use keepAnnotations for exceptions. |
libraries
Library jars are the dependencies your project imports but doesn't bundle — the ones you
declare in build.gradle or pom.xml (the Paper/Spigot API, a
database driver, any compileOnly or provided library). They
sit on your classpath at build time but aren't inside the JAR you hand us.
The obfuscator needs to see them to protect your code safely: to rename, restructure and encrypt, it walks the full class hierarchy — superclasses, interfaces, method overrides. If a type your code extends or implements lives in one of those libraries and we can't see it, we can't tell an override from a brand-new method, and the protected JAR could break at runtime. Declaring them lets us resolve every type.
Point folder at the directory that holds them and the CLI bundles and uploads
every .jar inside it (searched recursively), alongside your JAR —
no need to list them one by one. Paths are relative to where you run the CLI:
{
"libraries": {
"folder": "./libs"
}
}
If that folder also holds jars you don't want uploaded, list the specific names in
jars to take only those — folder then acts as the base path
and nothing else in it is picked up. You only need what's required to resolve your hierarchy — not your whole
classpath, and never the JDK itself:
{
"libraries": {
"folder": "./libs",
"jars": ["paper-api.jar", "PlaceholderAPI.jar"]
}
}
For large folders, set mode: "DEMAND_SAFE". The CLI uploads the
deterministic manifest first; while the worker resolves the hierarchy it requests only the JARs it
needs. The mode has an explicit fallback to ALL when a dependency cannot be
resolved statically.
Get them automatically
Don't collect these by hand. Add one task to your build and it copies every dependency into
./libs/ — then point libraries.folder at that folder.
Gradle — add this to build.gradle, then run
./gradlew exportLibs:
// build.gradle — export your dependencies into ./libs
tasks.register('exportLibs', Copy) {
// runtimeClasspath covers normal deps; add compileClasspath
// if your API is compileOnly / provided (Paper, Spigot, …)
from configurations.runtimeClasspath
from configurations.compileClasspath
into 'libs'
}
// run it: ./gradlew exportLibs
Maven — add the plugin to pom.xml; it runs on
mvn package, or on demand with the command in the comment:
<!-- pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>export-libs</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration>
<outputDirectory>${project.basedir}/libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- run it: mvn package
or: mvn dependency:copy-dependencies -DoutputDirectory=libs -->
seed & reproducibility
Set "seed": <number> to make builds reproducible: the same
input plus the same config produces byte-identical output — handy for debugging and for verifying a build.
Leave it out for a fresh random build each time.
--mapping rymga.map and it reuses the previous build's names for unchanged symbols —
see the mapping file. For an API that must
never move, keep it unrenamed.classGuard uses a secure random key by design, so turning it on
makes the build non-reproducible regardless of the seed.Full reference
Every field, with valid values, in one file — copy it and delete what you don't need. It's valid JSON as-is
(no comments), so it runs, though reflectionObfuscation and
classGuard are off by default because they change the most:
{
"techniques": {
"stringEncryption": "PER_METHOD",
"stringEncryptionExclude": [],
"rename": true,
"overloadMethods": false,
"synthetic": false,
"stripDebug": true,
"stripInnerClasses": false,
"stripNestInfo": false,
"cacheMode": "FAST",
"callerBinding": "STACKWALKER",
"assetPool": "INLINE",
"stripKotlinMetadata": false,
"stripAnnotations": false,
"keepAnnotations": []
},
"keep": {
"packages": ["com/you/model"],
"classes": ["com/you/Protocol"],
"members": [
{ "classPattern": "com/you/**", "methodPattern": "on*", "fieldPattern": null, "descriptor": null }
]
},
"rename": {
"deep": 3,
"chars": ["a", "b", "c", "d"],
"packageStrategy": "WRAPPER",
"packageDepth": 2,
"packagePool": 8,
"exclude": ["com/you/Main"],
"preservePackages": [],
"rewritePluginDescriptors": true
},
"flow": {
"enabled": true,
"preset": "custom",
"intensity": 6,
"exclude": [],
"timingSafeIndy": true,
"seedThreading": false,
"cfgFlattening": false,
"bogusControlFlow": { "enabled": true, "weight": 5 },
"realisticBogus": { "enabled": true, "weight": 5 },
"nullInvariantPredicate":{ "enabled": true, "weight": 5 },
"deadCode": { "enabled": true, "weight": 5 },
"tryCatchAbuse": { "enabled": true, "weight": 5 },
"exceptionFlow": { "enabled": true, "weight": 5 },
"controlFlowFlattening": { "enabled": true, "weight": 5 },
"loopTransforms": { "enabled": true, "weight": 5 },
"gotoSpaghetti": { "enabled": true, "weight": 5 },
"methodOutlining": { "enabled": true, "weight": 5 },
"irreducibleFlow": { "enabled": true, "weight": 5 },
"switchBogusCases": { "enabled": true, "weight": 5 },
"invokeDynamic": { "enabled": true, "weight": 5 },
"seedMutation": { "enabled": true, "weight": 5 },
"integerSplitting": { "enabled": true, "weight": 5 },
"fakeLoops": { "enabled": true, "weight": 5 },
"branchAugmentation": { "enabled": true, "weight": 5 },
"arithmeticMBA": { "enabled": true, "weight": 5 },
"exceptionOverlap": { "enabled": true, "weight": 5 },
"constantExpression": { "enabled": true, "weight": 5 },
"reflectionObfuscation": { "enabled": false, "weight": 5 }
},
"classGuard": {
"enabled": false,
"encrypt": ["com/you/internal/"],
"antiDump": {
"enabled": false,
"sites": 4,
"argsDetection": true,
"jdwpProperty": true,
"nativeDetection": true,
"nativeIntegrity": true,
"nativePrevention": true,
"periodicChecks": true,
"noRetainPlaintext": true,
"scatterChecks": false,
"scatterDensity": 20,
"attachListenerCheck": false
}
},
"typeDecoupling": {
"enabled": false,
"exclude": [],
"protectHotPaths": true
},
"antiDebug": {
"enabled": false,
"action": "throw",
"checkAgents": false
},
"libraries": {
"folder": "./libs",
"jars": []
},
"seed": null
}
Full examples
A Minecraft plugin, light and safe
Strings gone, renamed, cheap flow obfuscation. The one thing to handle: the plugin main is what the server
looks up by name, so it goes in rename.exclude — it keeps its name (the
plugin.yml reference stays valid) but still gets flow and string
encryption. That's the difference from keep,
which would leave it fully readable. A great starting point for a first Bukkit/Spigot/Paper plugin:
{
"techniques": {
"stringEncryption": "POOL",
"rename": true,
"stripDebug": true
},
"flow": { "enabled": true, "preset": "light" },
"rename": { "exclude": ["com/you/MyPlugin"] }
}
A real plugin: shaded libraries & background tasks
The common real-world case. Most plugins shade libraries (bStats, a license API…) into the
jar and run async scheduler tasks. Two different tools for two different needs: your own main
goes in rename.exclude (kept loadable, but still renamed-around, flowed and string
encrypted), while a third-party shaded library that spins up its own thread or reads itself by
name goes in keep — it's not
your code, so leave it completely untouched rather than risk obfuscating it:
{
"techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "balanced" },
"rename": { "exclude": ["com/you/MyPlugin"] },
"keep": {
"packages": ["com/you/libs/bstats", "com/vendor/licenseapi"]
}
}
classGuard loads its encrypted classes child-first, so async scheduler tasks and
threads your own code starts resolve them correctly, and two plugins with colliding obfuscated names no longer
clash on a shared server. The one case that can still miss: a shaded library that loads a class by name
off a thread it started, via that thread's context class loader (some bStats builds) —
leave such a library's package off encrypt (or add it to
keep), or use the config above (no
classGuard) for that jar. Either way, seal only after you've tested it loads on a real
server.A plugin, sealed (strongest — test it first)
Adds class encryption, type decoupling and
anti-dump — the recommended shape for real
code. encrypt: ["*"] seals everything it can,
and type decoupling makes almost all of it sealable by routing your concrete references through auto-generated
interfaces (no hand-written interfaces needed). antiDump then defends the sealed classes
at runtime — fail-closed against debuggers and agents. The main keeps just its
class name (#name) so the server can load it — its own fields and
methods are obfuscated, its onEnable preserved automatically — and it's kept cleartext
for you (the engine detects the descriptor main). Best after you've tested it loads on a
real server:
{
"techniques": {
"stringEncryption": "PER_METHOD",
"rename": true,
"stripDebug": true
},
"flow": { "enabled": true, "preset": "balanced", "timingSafeIndy": true },
"rename": { "exclude": ["com/you/MyPlugin#name"] },
"classGuard": {
"enabled": true,
"encrypt": ["*"],
"antiDump": { "enabled": true }
},
"typeDecoupling": { "enabled": true }
}
-javaagent, a profiler, or a
remote debugger, the sealed pool won't decrypt there. Only enable antiDump for builds
that run where no legitimate agent is attached, and test on that exact environment. See
Anti-dump.A library or API JAR
A jar other code compiles against. Its public api package goes in
rename.exclude: every class, method and field name stays exactly as callers link
against it — and the method bodies are still flow-obfuscated and string-encrypted. This is the
case where the old advice to "keep" the API was wrong: keep
would freeze the names and leave the bodies readable. Everything outside api is
fully obfuscated; MIRROR keeps the package shape so reflection over your own packages
still resolves:
{
"techniques": { "stringEncryption": "POOL", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "balanced" },
"rename": {
"packageStrategy": "MIRROR",
"exclude": ["com/you/api/"]
}
}
A Kotlin plugin
Same as a plugin, plus stripKotlinMetadata so the @Metadata
annotations that let tools rebuild your Kotlin classes, names and signatures are removed:
{
"techniques": {
"stringEncryption": "PER_METHOD",
"rename": true,
"stripDebug": true,
"stripKotlinMetadata": true
},
"flow": { "enabled": true, "preset": "balanced" },
"rename": { "exclude": ["com/you/MyKotlinPlugin"] }
}
A standalone app, everything on
An executable jar (not a plugin): the heaviest flow preset, sealed internal classes, and anti-debug that
refuses to unseal under a debugger. The Main-Class is what java -jar
resolves by name, so it stays cleartext and unrenamed (off encrypt, in
rename.exclude) while still getting flow and string encryption:
{
"techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "max" },
"rename": { "exclude": ["com/you/Main#name"] },
"classGuard": { "enabled": true, "encrypt": ["*"] },
"typeDecoupling": { "enabled": true },
"antiDebug": { "enabled": true, "action": "throw", "checkAgents": false }
}
max
flow preset, reflectionObfuscation) change a lot — run the protected build once before you ship it, and add
keep rules for anything reflection touches.