rymga ← Back to Lock Master

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.

Names are "internal" names. Everywhere you name a class, use JVM internal form — slashes, not dots: com/you/Main, not com.you.Main. ** matches any depth, * a single segment.
Two ways to spare a class: per-technique 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: the encrypt list (+ nested antiDump).
  • 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, keep rules, and the five package strategies.
  • String encryption — remove every readable literal from the class file. Covers the PER_METHOD, INLINE and POOL modes.
  • Control-flow obfuscation — rewrite how methods branch, loop and compute so no clean Java reproduces them. Covers presets and all nineteen flow techniques.
  • Class encryption — ship your classes as encrypted blobs with no bytecode to decompile. Covers classGuard, antiDebug and watermark.
  • 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:

FieldWhat it does
stringEncryptionEncrypt string literals — NONE / PER_METHOD / INLINE / POOL. See String encryption.
stringEncryptionExcludeClasses/packages to leave with readable strings — the per-technique exclusion for string encryption (on top of keep).
renameRename classes/methods/fields (uses rename + keep). See Renaming & layout.
stripDebugRemove debug info — line numbers and local variable names. Recommended.
stripInnerClasses / stripNestInfoDrop inner-class / nest-mate metadata.
syntheticMark members synthetic, hiding them from some tools and IDEs.
cacheModeString cache: FAST (cache after first decrypt) or SAFE (re-decrypt and wipe on every access).
callerBindingKey strings to their runtime caller: STACKWALKER (default) or STACKTRACE (cheaper, spoofable).
assetPoolWhere encrypted string data lives: INLINE (helper class) or EXTERNAL (META-INF/strings.dat).
stripKotlinMetadataDrop Kotlin @Metadata (for Kotlin code).
stripAnnotationsDrop 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 -->
Libraries aren't charged. Pricing is flat — one build is one token, and reference libraries cost nothing. They do count toward the 350 MB job cap and add server-side work, so export only what your hierarchy needs and trim test-only or runtime-only jars you don't actually extend.

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.

For stable names across releases, use incremental obfuscation. Obfuscated names are assigned in order, so a seed alone doesn't hold them when your code changes. Pass the CLI --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"]
  }
}
Async tasks are fine; watch one edge with self-loading shaded libs. 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 }
}
Anti-dump is fail-closed against any agent — profilers included. If the environment this runs in ever attaches a monitoring/APM -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 }
}
Always test the output. The heavier options (classGuard, the max flow preset, reflectionObfuscation) change a lot — run the protected build once before you ship it, and add keep rules for anything reflection touches.