VM Defense Telemetry & Reactions
Report VM defense detections to your backend with vmDefenseHook, and tune how each detection category reacts with vmDefenseReaction - from a fully non-breaking, telemetry-only build to one that breaks immediately on a stolen bundle.
Watch
Obfuscator.io Defense Reactions: Break, Decoy, and the VM Defense Hook
The problem
The VM defenses - vmSelfDefending, vmDebugProtection and vmDomainLock - act locally: when a debugger, automation tool, tampered environment, or unauthorized domain is detected, the protected code breaks or silently poisons its own results. That stops the attacker, but by default you never hear about it. You can't tell how often your bundle is being probed, which detector fired, or whether a defense is breaking a legitimate user.
Two options close that gap. Neither of them enables any defense - they only observe and steer the defenses you have already turned on:
vmDefenseHook- a global callback that receives a signal object every time a defense detects something. Use it to send telemetry to your backend.vmDefenseReaction- a per-category map that selects how an enabled defense reacts: break, decoy, or do nothing locally.
Both options were introduced in v7.1.0, but every example here uses the object form vmDefenseHook: { name }, which requires
v7.4.0. Earlier versions took a bare string (vmDefenseHook: '__vmDetection'); that form is rejected from v8.0.0 on, so
use the object form throughout.
Recipe 1 - report detections to your backend
Step 1 - register a global hook function, before the obfuscated bundle loads
The VM runtime and its defenses run before your protected program, so many detections fire during startup. Define the hook as a plain global in the host page, ahead of the obfuscated script tag:
Step 2 - point vmDefenseHook at it
The option is an object whose name is the global function to call (aliases is optional - see below):
In the dashboard, the VM Defense Hook field appears in the Advanced Protection section once at least one defense (vmSelfDefending, vmDebugProtection, or vmDomainLock) is enabled.
Step 3 - receive the signal on your backend
Every detection calls the hook with a single signal object:
source- the specific detector:headless,node,agent,agentBrowser,domain,debugger,sandbox,nativeHook,timing, orintegrity. As of v7.4.0 the formerenvandinspectordetectors report undersource: 'debugger'.agentBrowserreports undercategory: 'automation'and runs on browser targets withvmDebugProtection(v7.9.0+).category-automation,debugger,sandbox,domain,tamper, orintegrity. Thenodesource reports undercategory: 'debugger'(v7.4.0+).score,threshold- the detection score and the threshold it crossed
A minimal receiving endpoint (Express shown; any backend that accepts a POST works). It normalizes the body to an array so it also handles the batched form posted by the buffer pattern below:
The hook is for reporting only - its return value is ignored, and a missing or throwing hook is a silent
no-op. It can never disable a defense, so an attacker deleting or breaking your hook gains nothing. To change what a
defense does, use vmDefenseReaction (Recipe 2).
Define the hook in the host page, not inside the obfuscated source
For telemetry you want to capture every detection, and many fire at startup - a hook defined inside the obfuscated bundle is registered too late to catch those, and if it gets VM-compiled it isn't reachable until your program runs. It stays safe either way (a missing hook no-ops, and a hook that itself triggers a detection is not called again recursively), but for complete coverage register it up front in the host page.
The one exception is a hook that reacts only to a runtime detection - such as cleanup when a debugger opens during use. That hook can live inside the obfuscated bundle; see Recipe 3.
To still protect your reporting logic, keep the registered hook a one-line buffer and drain it from your obfuscated code:
Renaming the signal fields (aliases)
The default source / category values are descriptive names, so anyone instrumenting the callback (or reading the output) can recognise the protection and which detector fired. aliases renames signal fields to opaque tokens of your choice, applied inside the VM before the signal is emitted, so those names never appear in the output or reach the callback. Your app knows its own mapping and forwards the tokens to your backend.
Aliases are per field: each takes a key (the property name the callback receives); the string name-fields source and category also take a values map, while score / threshold are numbers and take only a key. Unset entries keep their default names.
In the dashboard, the Signal aliases section sits under the VM Defense Hook field.
This is fingerprint avoidance, not secrecy - the mapping can still be inferred by repeated testing, so its only benefit is not exposing stable, self-explanatory names.
Recipe 2 - adjust the default reactions
vmDefenseReaction configures how each detection category reacts. It does not enable anything - the defenses themselves are turned on by vmSelfDefending, vmDebugProtection, and vmDomainLock; this option only selects how an enabled defense reacts. The category is the unit of control: every detector in a category enacts that category's reaction, and a reaction set for a category whose option is off simply has no effect.
| Category | Enabled by | Reacts when |
|---|---|---|
automation | vmSelfDefending or vmDebugProtection | The code is being driven by software instead of a person: a headless or automated browser, a scraping / testing framework, or an AI coding-agent stepping the page. |
debugger | vmDebugProtection or vmSelfDefending | Someone has a debugger or the browser's developer-tools inspector open and is stepping through the running code to understand it. |
sandbox | vmDebugProtection | The code is not running in a real browser at all - it has been lifted into an emulated or scripted JavaScript environment to be executed and studied offline. |
domain | vmDomainLock | The code is running on a site you did not authorize: a host not in your vmDomainLock allow-list (for example, your bundle copied onto someone else's domain). |
tamper | vmSelfDefending | The JavaScript environment around the VM has been modified to watch or hijack it, such as native browser built-ins swapped out for instrumented versions. |
integrity | vmSelfDefending | The protected bundle's own code has been edited or patched since you generated it. |
Keys are these six category names, or default (a fallback for unspecified categories). Values are:
break- break immediatelydecoy- keep running on poisoned state, silently producing wrong results.decoyneedsvmDebugProtectionorvmDomainLockon a browser target; otherwise it acts asbreak.none- do nothing locally (telemetry only)
A category you don't set falls back to the built-in defaults:
default reaches every category, integrity and tamper included, so { default: 'none' } is a genuinely non-breaking, telemetry-only build:
In the dashboard, the VM Defense Reactions selects appear in the Advanced Protection section once a defense is enabled; each category is editable only while a defense that emits its detectors is on.
Recipe 3 - run your own logic before a defense breaks
The hook is not only for reporting - it is also the one reliable place to run your own response before a defense reacts. When a debugger opens on a running page, you might want to clear what is on screen, or replace the view with a 404 page, before the code breaks.
Why the hook rather than code elsewhere in your app: break stops all subsequent bytecode, so a teardown that runs after a defense fires - especially when it is itself VM-obfuscated - is exactly what the break prevents from executing. The hook fires at the detection site before the reaction is enacted, synchronously - so a synchronous function it calls finishes first, then break stops the VM.
Define the response as your vmDefenseHook. Because the debugger detection fires at runtime - after your program has loaded and defined the hook - the hook can be part of your obfuscated source and is bytecoded along with the rest of the bundle. Branch on signal.category so each condition gets the right response, keep the work synchronous, then let the reaction run:
This applies to detections that fire while your app is running - see When bytecoding the hook works below.
Keep these points in mind:
- Only synchronous work is guaranteed to finish first. The reaction runs on the statement right after the hook returns. Fire-and-forget calls that hand off immediately are fine (
navigator.sendBeacon, synchronous DOM and canvas edits); work you schedule for later - asetTimeout, a promise continuation, anawait- is not, and anything that needs more VM bytecode will not run, because that is whatbreakstops. - The hook runs before the reaction; it does not replace it. Its return value is ignored, and it cannot cancel, delay, or change what the reaction does. Use it to act before the break, not to veto it - to change the reaction itself, use
vmDefenseReaction(Recipe 2).
When bytecoding the hook works
Putting the hook inside the obfuscated bundle like this works only because the debugger detection fires at runtime. The VM fires vmDefenseHook while it is still live, after your program has loaded and defined the hook, so the bytecoded hook is decoded and run first, then break. This is what protects the hook's own source.
It does not work for detections that fire at startup - automation, sandbox, domain, or a debugger already open when the page loads - because at that point the bytecoded hook is not defined yet, so the defense finds no function to call. For those, register the hook as a plain global in the host page instead, as in Recipe 1. When in doubt, a plain host-page global covers every detection that reaches the hook; bytecoding only adds protection for the hook's own source, and only for runtime detections.
From telemetry to enforcement
Visibility and enforcement do not have to ship together. Deploy the defenses in two stages: first a build that only reports, then - once the telemetry looks clean - one that reacts.
Step 1 - ship an observe-only build
Enable every defense you plan to use, point vmDefenseHook at your endpoint, and turn all reactions off. Every detector still runs and reports each hit to your backend, but nothing breaks:
Step 2 - review the collected signals
After the build has seen real traffic, look for detections that legitimate use triggered. The two most common:
automationhits from your own end-to-end tests or uptime monitoring - build those artifacts without the defenses instead of tolerating the category in production.domainhits from a staging or preview host you forgot to include in thevmDomainLockallow-list - add the host.
Prefer fixing the cause over softening a reaction: every category left at none is a detector an attacker can safely ignore.
Step 3 - switch on the reactions
Remove the default: 'none' override so the built-in per-category reactions apply; that single line is the entire change. If a category keeps producing false positives you cannot eliminate, leave that one category at none (e.g. vmDefenseReaction: { automation: 'none' }) and enforce the rest.
Keep vmDefenseHook set after enforcement is on - the hook fires regardless of the reaction, so you keep
visibility into who is probing your bundle while the defenses act.
