Client-side HTML mutation engine.
Rewrites your live DOM on every load so scrapers, headless browsers, and AI
vision models see a different, hostile document each time — with zero visible
change to real users.
- Why
- Quick start
- Layers
- Compatibility & caveats — read this before shipping
- Configuration
- Runtime API
- Build outputs
- Monorepo layout
- Development
- License
The HTML your server sends and the DOM a browser renders don't have to match.
web-mutator runs once the page is live and renames, re-wraps, and noises up
the DOM — tags become custom elements, classes and IDs are re-hashed, decoy
nodes and honeypots are sprayed in, and text can be ciphered. Native DOM APIs
(getElementById, querySelector, classList, …) are transparently patched so
your own code keeps working against the original names.
The result: a scraper that keys off .price or #contact-form, a headless
crawler diffing markup across loads, or an OCR/vision model reading the page —
all get a moving target. Humans see nothing change.
Not a security boundary. Anything shipped to the browser can ultimately be read from the browser.
web-mutatoris a cost-raiser against automated collection, not DRM.
<!-- configure first (optional) … -->
<script>
window.WebMutatorConfig = {
classes: { prefixes: ['mk-', 'btn-', 'card-'] },
vision: { ss_detector: { enabled: true } },
};
</script>
<!-- … then drop in the bundle. It auto-inits on DOMContentLoaded. -->
<script src="dist/web-mutator.iife.min.js"></script>ESM:
import WebMutator from 'web-mutator';
WebMutator.init({ /* config */ });Every layer is independently toggleable. The first seven ship on; the vision and cipher layers are opt-in.
| # | Layer | What it does | Default |
|---|---|---|---|
| 1 | Tag Mutator | Renames HTML tags to random custom elements | ✅ |
| 2 | Class Mutator | Re-hashes CSS class names (DOM + stylesheets) | ✅ |
| 3 | CSS Var Mutator | Renames --custom-properties |
✅ |
| 4 | Noise Injector | Fake classes, decoy attributes, comments, honeypots | ✅ |
| 5 | ID Mutator | Re-hashes element IDs | ✅ |
| 6 | Structural Noise | Random wrapper elements & hidden decoys | ✅ |
| 7 | Attr Name Mutator | Renames class/id/src attrs; patches DOM APIs |
✅ |
| 8 | Vision Poison | Unicode marks + CSS micro-transforms vs. AI OCR | opt-in |
| 9 | SS Detector | Animation-heartbeat headless-browser detection | opt-in |
| 10 | Text Cipher | XOR-encodes text nodes, decodes in the live DOM | opt-in |
Plus a NoScript Guard that gates the page when JavaScript is disabled.
web-mutator is most effective on sites that ship their own static CSS. A
few environments need specific layers turned off — know your stack before
enabling everything:
- Utility-CSS frameworks (Tailwind, UnoCSS, …). These express layout
through classes (
flex,grid,hidden,block). The Tag Mutator forces adisplayvalue per element and will override those utilities, so flex/grid containers collapse. Turntagsoff on utility-CSS sites. - Runtime CDN JIT (
cdn.tailwindcss.com). It regenerates CSS from live class names and will fight the Class Mutator. Precompile your CSS to a static file first, or leaveclassesoff. - Heavy inline-JS / third-party widgets (lightboxes, map SDKs, Alpine, …).
The API patch keeps your selectors working, but widgets that cache elements
or use exotic lookups can still trip. If a widget misbehaves, disable
ids/attrNamesand keep the non-structural layers. observeDynamicon a non-SPA. The observer re-processes nodes the noise layers inject, which can loop. Leave it off unless you have a true SPA and have excluded injected nodes.
A safe, always-non-breaking starting point is noise + structure only:
window.WebMutatorConfig = {
observeDynamic: false,
layers: {
tags: false, classes: false, cssVars: false, ids: false, attrNames: false,
noise: true, structure: true,
},
};This mutates the DOM every load (decoys, honeypots, comments) without touching a single real tag, class, or ID — so nothing can break. Add the heavier layers once you've confirmed your stack tolerates them.
window.WebMutatorConfig = {
// ── Global ───────────────────────────────────────────────────────
enabled: true, // master switch
seed: null, // null = random per-session
observeDynamic: true, // MutationObserver for SPA-injected nodes
refreshInterval: 0, // ms; 0 = off
silent: true, // suppress console warnings
// ── Per-layer toggles ────────────────────────────────────────────
layers: {
tags: true, classes: true, cssVars: true, noise: true, ids: true,
structure: true, attrNames: true,
visionPoison: false, ssDetector: false, textCipher: false,
},
// ── Layer 1: Tag Mutator ─────────────────────────────────────────
tags: { exclude: ['table', 'form'], cleanScripts: true },
// ── Layer 2: Class Mutator ───────────────────────────────────────
classes: { prefixes: ['mk-', 'btn-'], ignore: ['js-hook'] }, // null = all
// ── Layer 8: Vision Poison ───────────────────────────────────────
visionPoison: { enabled: true, selectors: ['.price', '.phone'], mode: 'both' },
// ── Layer 9: SS Detector ─────────────────────────────────────────
vision: {
ss_detector: {
enabled: true, with_animation_stop: true,
blur_exclude: ['header', 'nav'],
on_detect: (reason, ts) => console.log('headless:', reason),
},
},
// ── Layer 10: Text Cipher ────────────────────────────────────────
textCipher: { enabled: true, key: null }, // null = random per-session
// ── NoScript Guard ───────────────────────────────────────────────
noScript: { enabled: true, title: 'JavaScript Required' },
// ── User plugins: (root, maps, engine) => void ──────────────────
plugins: [ (root, maps) => console.log('tag map:', maps.tags) ],
};const wm = window.__webMutator;
wm.findById('price-card'); // resolve by ORIGINAL (pre-mutation) name
wm.findByClass('contact-form');
wm.getMaps(); // { tags:{…}, classes:{…}, ids:{…}, … }
wm.getLayer('textCipher'); // a specific layer instance
wm.destroy(); // undo mutations & restore native DOM APIs| File | Format | Use |
|---|---|---|
dist/web-mutator.iife.min.js |
IIFE (min) | <script> drop-in, production |
dist/web-mutator.esm.min.js |
ESM (min) | import in a bundler |
dist/web-mutator.cjs.js |
CJS | require() / Node / SSR |
dist/chunks/*.js |
ESM | Standalone per-package chunks |
Non-minified .iife.js / .esm.js are emitted too for debugging.
packages/
core/ config schema, seeded RNG, shared helpers
mutators/ Layers 1–7 (tags, classes, css-vars, noise, ids, structure, attr-names)
vision/ Layers 8–9 (vision poison, headless detector)
cipher/ Layer 10 (text cipher, char substitution)
guards/ NoScript guard
web-mutator/ umbrella package: engine, loader, public API
Each package is independently importable under the @web-mutator/* scope.
npm install # Node 18+, npm workspaces
npm run build # all dist outputs
npm run build:watch # rebuild on change
npm test # jest unit tests
npm run lint # eslintMIT © qiyascc