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.Structure
The top level is a set of optional blocks. Include only the ones you need:
{
"techniques": { ... }, // class-file protections (strings, rename, stripping)
"keep": { ... }, // what to never rename / touch
"rename": { ... }, // how renaming names things
"flow": { ... }, // control-flow obfuscation
"classGuard": { ... }, // whole-class encryption (sealed classes)
"antiDebug": { ... }, // refuse to run under a debugger
"libraries": { ... }, // dependency jars so the class hierarchy resolves
"seed": 123456 // 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 four layers
Protection stacks in four layers, from cheapest to strongest. 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.
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. |
| 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.
List them by folder and name; the CLI bundles and uploads them alongside your JAR (paths relative to where you run it). 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"]
}
}
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",
"rename": true,
"synthetic": false,
"stripDebug": true,
"stripInnerClasses": false,
"stripNestInfo": false,
"cacheMode": "FAST",
"callerBinding": "STACKWALKER",
"assetPool": "INLINE",
"stripKotlinMetadata": false,
"stripAnnotations": false,
"keepAnnotations": []
},
"keep": {
"packages": ["com/you/api"],
"classes": ["com/you/Main"],
"members": [
{ "classPattern": "com/you/**", "methodPattern": "on*", "fieldPattern": null, "descriptor": null }
]
},
"rename": {
"deep": 3,
"chars": ["a", "b", "c", "d"],
"packageStrategy": "WRAPPER",
"packageDepth": 2,
"packagePool": 8
},
"flow": {
"enabled": true,
"preset": "custom",
"intensity": 6,
"timingSafeIndy": true,
"seedThreading": false,
"cfgFlattening": false,
"bogusControlFlow": { "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,
"mode": "STANDALONE",
"pluginMainClass": null,
"excludeClasses": []
},
"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, keeping the main class and public API. A great starting point for a first Bukkit/Spigot/Paper plugin:
{
"techniques": {
"stringEncryption": "POOL",
"rename": true,
"stripDebug": true
},
"flow": { "enabled": true, "preset": "light" },
"keep": {
"classes": ["com/you/MyPlugin"],
"packages": ["com/you/api"]
}
}
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. This still gives strong protection — renamed, strings
encrypted, control flow rewritten — while keeping the two things that would otherwise break: the
main class (so plugin.yml resolves) and the packages of any
shaded library that spins up its own thread or reads itself by name:
{
"techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "balanced" },
"keep": {
"classes": ["com/you/MyPlugin"],
"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) —
list such a library in excludeClasses, 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 in PLUGIN mode and anti-debug. You only name the main in
pluginMainClass — you don't keep it or exclude it; PLUGIN mode
renames and encrypts your real main and ships a cleartext stub under your plugin.yml
name. Best for a self-contained plugin without background threads or shaded self-loading libraries (see the
warning above):
{
"techniques": {
"stringEncryption": "PER_METHOD",
"rename": true,
"stripDebug": true
},
"flow": { "enabled": true, "preset": "balanced", "timingSafeIndy": true },
"classGuard": {
"enabled": true,
"mode": "PLUGIN",
"pluginMainClass": "com/you/MyPlugin"
},
"antiDebug": { "enabled": true },
"keep": { "classes": [] }
}
A library or API JAR
A jar other code compiles against: keep the whole public api package and all its
member names so callers still link, and obfuscate everything else. MIRROR keeps the
package shape so reflection over your own packages still resolves:
{
"techniques": { "stringEncryption": "POOL", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "light" },
"rename": { "packageStrategy": "MIRROR" },
"keep": {
"packages": ["com/you/api"],
"members": [
{ "classPattern": "com/you/api/**", "methodPattern": "*" }
]
}
}
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" },
"keep": { "classes": ["com/you/MyKotlinPlugin"] }
}
A standalone app, everything on
An executable jar (not a plugin): aggressive flow, sealed classes with a generated launcher, and anti-debug
that refuses to unseal under a debugger. Keep the Main-Class:
{
"techniques": { "stringEncryption": "PER_METHOD", "rename": true, "stripDebug": true },
"flow": { "enabled": true, "preset": "aggressive" },
"classGuard": { "enabled": true, "mode": "STANDALONE" },
"antiDebug": { "enabled": true, "action": "throw", "checkAgents": false },
"keep": { "classes": ["com/you/Main"] }
}