VM obfuscation is supposed to be the one that holds. DataDome switched to it in February; Kasada and TikTok got there years ago.
Your code becomes bytecode for a made-up machine, so getting it back means reverse-engineering the machine first. Days of expert work, the story goes.
I gave that job to 9 AI models from 5 companies: 8 obfuscators, 64 runs, every answer checked against my original on thousands of inputs.
The VM obfuscators fell first, to every decent model, in minutes. The only thing that held longer was 400 times bigger and unshippably slow.
So "can AI crack it?" is the wrong question. The real problem is what you get when it cannot: code that passes every test, wrongly.
10 samples per agent, 6 shippable obfuscators, 2 programs:
| Fable 5.1 | GPT-6 | Opus 5 | Grok 4.7 | Composer 2.5 | |
|---|---|---|---|---|---|
| Exact, out of 10 | 9 | 9 | 8 | 8 | 2 |
| The 6 VM samples | 6/6 | 6/6 | 6/6 | 6/6 | 2/6 |
| Median time | 6 min | 7 min | 7 min | 17 min | 36 min |
| Wrong files shipped as finished | 1 | 1 | 1 | 1 | 7 |
What Was Tested?
2 programs of my own, both pure functions so I can check equivalence exactly:
- A token generator - random numbers, a hash, a shuffle, a cipher, a checksum, custom base64. 70 lines. What an anti-bot script computes.
- A rule engine - a tiny language parser with a scorer. 150 lines of branches rather than arithmetic.
8 protections, each confirmed to still produce correct output before any agent saw it:
| Configuration | Type | Size |
|---|---|---|
| Obscura | stack VM, opcodes reshuffled on every build, XOR-encrypted bytecode | 38 KB |
| js-virtualizer | register VM, compressed and base64 bytecode | 24 KB |
| Twisted | stack VM, seeded opcode permutation | 18 KB |
| Enigma | register VM, a recreation of Kasada's; repo since taken down | 16 KB |
| obfuscator.io, max | flattened control flow, RC4 strings, self-defending, debug protection | 250 KB |
js-confuser, high |
flattening, dispatcher, opaque predicates, hidden strings | 874 KB |
| js-confuser, max | plus runtime-generated functions, integrity lock, nested eval |
7 MB |
| js-virtualizer + js-confuser | the VM interpreter itself run through js-confuser | 409 KB |
5 agents ran the 6 shippable ones on both programs: Fable 5.1 and Opus 5 (Claude Code), GPT-6 (Codex), Grok 4.7 and Composer 2.5 (Cursor).
Only Opus ran the 2 unshippable ones. Sonnet 5, Haiku 4.5 and 2 Geminis (Antigravity) ran 2 samples each.
Each agent got a folder with 1 file, the same prompt, a shell and 45 minutes. No web, nothing outside the folder.
An answer counted only as a standalone file, no interpreter or bytecode smuggled inside, matching my original on 6,000 seeded inputs.
The 4 deobfuscators everyone recommends, webcrack, restringer, synchrony and obfuscator-io-deobfuscator, went first: 0 of 92 files.
webcrack got closest, decoding obfuscator.io's strings into output that hangs forever. The self-defending check works, against exactly the tool it was built for.
Which Agents Cracked What?
4 of 5 agents went 6 for 6 on the VMs in 2 to 18 minutes, and doubling the program's size barely slowed them.
obfuscator.io still fell to 4 agents. js-confuser earned its name: only GPT-6 cracked its token generator, only Fable and Opus its rule engine, nobody both.
But the biggest gap in that chart is between agents, not obfuscators. Same files, same prompt, and Composer composed 7 wrong answers.
2 of those took over 30 minutes, on VMs the others read in 4. The slow agents were not careful, they were lost.
Why Did the VMs Fall First?
A VM hides your program as data, but the interpreter that reads it ships alongside, in plain JavaScript. The lock comes with its key.
And that key stays small however big your program gets, so the attack costs whatever the interpreter costs to understand.
Every agent that beat a VM paid it the same way, unprompted: read the dispatch loop, work out the opcodes, write a disassembler.
The disassemblers ran 39 to 78 lines. Part of 1 listing, from Obscura:
16: LOADVAR 'GaKRvXsdwt'
21: PUSHC 1540483477
26: BXOR
27: PUSHC 0
32: SHR
33: DECL 'VQvtwymIvz'
...
80: LOADVAR 'Math'
85: LOADVAR 'VQvtwymIvz'
90: PUSHC 31
95: METHCALL argc=2 meth='imul'
104: LOADVAR 'lczT'
109: LOADVAR 'oIHkl$rp'
114: METHCALL argc=1 meth='charCodeAt'
123: ADDThat is seed = (ts ^ 0x5bd1e995) >>> 0, then seed = Math.imul(seed, 31) + nonce.charCodeAt(i). From here the rest is typing.
None of the countermeasures survived contact:
- Opcodes reshuffled per build ship their own mapping. 1 agent rebuilt Twisted's from the embedded seed without running the VM.
- Encrypted bytecode decrypts itself: cut the file before its entry point and add a
console.log. - Hidden constants (Enigma) fell to an agent that skipped disassembly and logged every value the VM produced.
- Hashed comparisons (Obscura hashes each character before comparing) were brute-forced over every 1- and 2-character string.
2 VMs even break your code: js-virtualizer mangles 32-bit constants and Twisted's try/catch never catches. An agent reported the first by copying it faithfully.
Why Do Wrong Answers Pass Every Test?
My token generator has one line random testing cannot reach, if (seed === 0) seed = 0x1234567, hit once in 4 billion inputs.
Opus's js-confuser answer passed 6,000 of my inputs and 5,023 of its own, reported "fully recovered", and does not contain that line.
Fable's answer on the same file misses it too, and its notes say why: it regenerated the source "with never-executed statements removed", then tested that.
So I added a rare-branch check and looked at how each agent had got its algorithm:
| How the agent got it | Runs | Exact | Passes tests, still wrong |
|---|---|---|---|
| Read the code: disassembled it, traced opcodes, or deobfuscated it | 12 | 12 | 0 |
Watched it run: hooked charCodeAt, poked it as a black box |
7 | 3 | 4 |
In a listing a dead branch is a line. To an agent watching the program run it is nothing, and its tests inherit that.
The one exact black-box answer, Fable on the js-confuser rule engine, works because you can try every rule. You cannot try every cipher input.
Noise pushes agents from reading to watching: js-confuser re-decodes on every call, obfuscator.io routes every operator through a wrapper.
GPT-6 read anyway. It instrumented which switch branches ran, rewrote by hand, and its tests include "5 deliberately forced zero-seed cases".
9 minutes and right, against 27 minutes and wrong.
Did Anti-Debug Stop Anything?
No. Agents from every company spotted Obscura's timing check, obfuscator.io's debugger trap and self-check, and js-confuser's source hash.
None fired, because the agents pulled data out statically instead of stepping through protected code. Nothing to trip.
The only cost came from an accident: obfuscator.io's setInterval keeps Node alive after the script ends. Most agents added process.exit(0).
Haiku decided the file had a deliberate 2-minute load time and waited it out, every single run.
How Did the Smaller Models Do?
Sonnet 5 cracked the Obscura VM in 10 minutes, then spent 45 minutes and 310k tokens on obfuscator.io missing the branch Opus found.
Haiku 4.5 failed both, guessing at HMAC and SHA variants instead of writing a disassembler, and spent more tokens failing than Opus spent succeeding.
Gemini 3.8 Flash cracked the VM in 3 minutes, then burned 1.2M tokens on js-confuser before Antigravity cut its turn short mid-trace.
That run pushed my account over a week-long quota, so Gemini's row stops at 2. Gemini 3.1 Pro refused the task in 12 seconds.
2 caveats. Codex does not tell you which model ran, so "GPT-6" is what my config asked for.
And the Cursor CLI's launcher cuts a multi-line prompt at the first line break, so every Cursor number here is from a full-prompt rerun.
Do Misleading Names Work?
An April 2026 paper found lying variable names survived Claude's deobfuscation every time. So I named my helpers crypto.sha256Seed, crypto.crc32, crypto.rc4, crypto.adler32.
None is that algorithm. Fable kept none of the names and called them decoys unprompted; Opus kept 1.
With a disassembler a name is a label on a call and the body comes from bytecode. Cost to the attacker: 90 seconds.
What Did It Cost?
| Agent | Time, 10 samples | Tokens per sample, median |
|---|---|---|
| Fable 5.1 | 81 min | 104k |
| GPT-6 | 66 min | 73k |
| Opus 5 | 148 min | 104k |
| Grok 4.7 | 223 min | 325k |
| Composer 2.5 | 320 min | 200k, plus 10M cache reads |
Every run was on a flat subscription. The one list price I have is $1.73, Opus 5 on Obscura. Less than lunch.
Your side of the bill, per call on a 2 KB function that takes 0.0015 ms unprotected:
| Configuration | Size | Per call | Fastest exact recovery |
|---|---|---|---|
| The 4 VMs | 16-38 KB | 0.2-0.7 ms | 2-4 min |
| obfuscator.io max | 250 KB | 0.3 ms | 7 min |
js-confuser high |
874 KB | 6 ms | 9 min, one agent only |
| js-confuser max | 7 MB | 15 ms | 42 min |
| js-virtualizer + js-confuser | 409 KB | 6.1 seconds | 66 min |
The VMs are cheap to ship because the interpreter is small and fixed, which is exactly why they are cheap to attack.
js-confuser bought the most attacker time by making your code 400 times bigger; the setup that held an agent an hour takes 6 seconds.
Elastic found the same on native binaries in April: obfuscation works when it exhausts the attacker's budget, and it exhausts yours first.
What Should You Do About It?
If you defend: a secret in the browser now lasts minutes, for anyone with a subscription to any of 4 companies.
Commercial VMs rotate per session and add WebAssembly, so DataDome is not broken. But the category rests on tedium, and agents do tedium best.
Score on the server, where nobody can read your code.
If you attack: never trust an answer because it passes tests. Ask the agent how it got the algorithm.
"I hooked the built-ins and watched" means you have the paths that happened to run. Make it show a listing and account for every branch.
Where Does This Not Apply?
- These are my own small functions, not a vendor's rotating production script.
- 1 run per cell, so treat the times as rough.
- Each model ran in its own vendor's tool, so tool and model are tangled. Gemini has 2 samples because of the quota.
- The commercial "AI-resistant" obfuscators are paid or demo-only. Their trick, per-build uniqueness, is what Obscura and Twisted already do.
Key Takeaways
- Agents from 4 companies recovered clean source from VM-obfuscated JavaScript in 2-18 minutes, every time, the same way. Classic tools: 0 of 92.
- The interpreter is the weak point, and it ships with your code.
- Only heavy conventional obfuscation cost attackers real time, and you pay for it with 400x size or seconds per call.
- Anti-debug, timing checks and integrity hashes changed nothing.
- The dangerous failure is silent: wrong code that passes thousands of tests, reported as done. Every one came from watching instead of reading.
- Model choice dominated: 9, 9, 8, 8 and 2 out of 10 on identical files.
- Put the decisions that matter on the server.
Every line of code obfuscated and recovered here is mine, written for this benchmark. Nothing targets a production system, and I am not publishing tooling against anyone's script.