I tried something I think is relevant to share here: handed Claude Code two obfuscated JS files (no hint about which obfuscator made which file) with a four-paragraph prompt asking it to recover the source.
Both came back with a clean source - about 10 minutes for an open-source obfuscator, about 20 minutes for a commercial one.
This is given literally 13-line programs were inflated like 200x to demonstrate obfuscation capability, and these are own’s vendor demos. In production, 200x is unrealistic (maybe, just for a small chunk of code), which makes this even simpler for Claude.
I have a longer write-up on this topic with the exact prompts, inputs and outputs - the technique below is a TL;DR version.
Target 1 - the open-source
The input was a 1,587-line, 68 KB blob from javascript-obfuscator landing page demo (the 13-line calculatePrice function inflated ~194×). Claude returned clean source in ~10 minutes.
What got me wasn’t the time. It was that there were nine distinct defense layers stacked in that file - and the agent identified and undid all nine. Not myself pointing at any of them. The prompt was four paragraphs that boiled down to “here’s a JS file, get me the source”.
It also chose the right strategy on its own. What’s interesting, the first attempt was an 883-line script statically reimplementing the pipeline (RC4 string decryption, base64, the binary deserializer); it recovered the ~500 encrypted string calls and the env-fingerprint value, then it died on the custom zigzag-varint bytecode format. Claude pivoted from reimplement to instrument - a ~48-line script that splices logging hooks into the VM and lets the obfuscator deserialize its own bytecode at runtime. Recovered from the runtime trace:
- function name, 2 params, 3 locals, constants
[0.15, 100, 1, "calculatePrice"] blockKey=54,jumpKey=9643,seKey=4168320119- the 22-instruction bytecode stream
A small AI disassembler named the opcodes (PUSH_CONST, LOAD_ARG, STORE_LOCAL, LOAD_LOCAL, MUL, SUB, GT, JMP_FALSE, RETURN, …) and reconstructed the original code:
function calculatePrice(price, quantity) {
const taxRate = 0.15;
const threshold = 100;
let total = price * quantity;
if (total > threshold) total = total * (1 - taxRate);
return total;
}
// calculatePrice(10, 20) === 170, matches the original
Names and argument-order aren’t recovered (inferred from behavior); literal numbers are (the VM needs them in the bytecode).
The nine layers Claude unwrapped, and why each came apart
- String array + RC4 - self-contained; eval
U()once, statically decrypt every call. - String array rotation - IIFE rotates until a checksum matches; runs on startup, just read the result.
- Env fingerprint
h()- XORs0x5f3759dfwith built-in.lengthvalues; identical on every JS engine, so the “binding” is cosmetic. - RC4-encrypted bytecode - key derived from
h(); onceh()is known, one line. - Custom binary serialization
B()- zigzag-varint, flag-gated fields; instrument it, don’t reimplement. - Opcode shuffling -
b3table + per-function seed (seeded PRNG); reconstructible with seed + algorithm. - Operand XOR -
blockKey/jumpKeyper function; simple XOR, one known-plaintext recovers it. - Stack-value XOR -
seKey, integers only; floats/strings/objects in plaintext. - Anti-debug timing + the ~1,500-line VM - timing checks corrupt the opcode table if you step through slowly; automated instrumentation runs full-speed and never trips them. The VM is the inverse - instrument dispatch once, get everything.
Nine layers. None of them survived a single agentic pass.
Target 2 - the commercial obfuscator
The input was a 24,620-byte js from Jscrambler’s public demo (available through their portal after registration) - the original is a 5-line BACKGROUND sprite-atlas object. Claude returned clean source in ~20 minutes, and this time even no instrumentation pivot was needed; static analysis alone was enough.
Different machinery, very similar outcome. Control-flow flattening into a switch state-machine, a URL-encoded XOR string blob with a cyclic key, a self-replacing decoder that primes for 8 calls and then degrades to a direct lookup, a 3D index table of opaque integer tags, no bytecode VM. Same six-step arc translated almost directly. Jscrambler has a public blog post arguing AI can’t reverse their obfuscation - accurate about a chatbot that can’t execute code, not about an agent that can.
What does this means for obfuscation?
Nine independent defenses on Target 1, six on Target 2 - and the agent walked through every one without me pointing at any of them. A year ago this was a week of focused human work; today it’s one prompt.
The honest question I’m sitting with: is client-side obfuscation finished as a serious defense (which I don’t believe in), or has it shifted from “secret” to “friction”? (defeating static analyzers, still) What’s left that an agent genuinely can’t unwrap - anti-tamper integrity, environment-bound execution, runtime self-checks? Something else?
And if you ever thrown an agent at obfuscated code, what didn’t it crack, especially in production apps? That’s the comparison I’d find useful.