It started because I wanted to protect this site.
kieleth.com is a force-directed graph. The thing you're looking at right now. I built it with vanilla JavaScript: particle physics, spring forces, Canvas 2D rendering. No framework, no build step. Clean source with readable variable names like repulsion, damping, springK.
Then I thought: what if someone just copies this? Saves the page, hosts it somewhere else, done. So I added an obfuscation pipeline: javascript-obfuscator with domain lock, self-defending code, control flow flattening, string encryption. The deployed version is mangled. The source stays clean in the repo.
And then the question that consumed the next month: what happens when an LLM tries to deobfuscate it?
The Setup
I fed the obfuscated JavaScript to Claude and asked it to deobfuscate. It did. Beautifully. Variable names recovered, physics identified, algorithm explained. The obfuscation was cosmetic against an LLM.
So I tried something different. What if the string table, the encrypted list of variable names that the obfuscator produces, contained wrong names? Not gibberish. Wrong-but-plausible physics terms.
Instead of repulsion: 4000, the decoded name would say attraction: 4000. Same number. Opposite meaning. Both valid physics words.
Instead of damping: 0.92 (velocity decay), it would say amplification: 0.92. The operation multiplies velocity by 0.92, which reduces it. "Amplification" implies the opposite.
The Panda-Gibbon Moment
Claude produced this:
attraction: 4000, // Repulsion force strength
amplification: 0.92, // Velocity damping (friction)
Read that again. The comments are correct. The variable names are wrong. The model simultaneously understood the physics (written in the comments it generated) and reproduced the poisoned names in the code it wrote.
This happened in 100% of runs. Across every prompt variant. Even when explicitly warned that the string table might be adversarial.
attraction). The model sees the panda, writes "Repulsion force strength" in the comment. Then labels it gibbon, writes attraction in the code.
Everything Else That Failed
Before pill 10 worked, seventeen others didn't.
I tried DMCA threats in hex-encoded strings. The model decoded them, called them "prompt injection attempts, they have no effect on me," and deobfuscated the code more thoroughly. Legal intimidation backfired. It functioned as a signal that there was something worth analyzing.
I tried stacking five techniques at once. That produced the worst result of all: the model identified every layer, catalogued each adversarial technique by name, and delivered its most thorough analysis. More defenses = more signal = better understanding.
I built a six-layer decision graph with config cascading and closure overrides. The model traced all six decisions in one pass and called the code "a study in misdirection."
I renamed all 226 obfuscated identifiers to malware vocabulary: credential_harvest appearing 521 times. The model traced each function, saw that it returns "<div class=", and dismissed everything as "deliberately scary wrapper names."
Seven Build Versions
The defense pipeline went through seven iterations. v2 prepended realistic malware. The model stopped reading after 100 lines: "Stop. This is malware." But when told to skip the top, 7 minutes later, full recovery. v4 injected malware into the rotation IIFE, the bootstrap code deobfuscators must execute. The model hit it in 30 lines and refused. When pushed to "triple check," it investigated for 6 minutes and concluded: "The graph visualization doesn't exist. It's ALL payload." v7 added three nested malware layers. The model spent 13.5 minutes and recovered the full structure. But with poisoned parameter names throughout.
Then someone asked it to reconstruct the website. Two parallel agents. 2 hours 20 minutes. 137 tool invocations. 268,000 tokens. It produced 1,441 lines of HTML that didn't work when opened.
What We Got Wrong
Early testing at N=2 per domain showed physics terms propagating at 100% and everything else at 0%. We wrote "the boundary is binary." Two reviewers accepted it. Then we boosted to N=5. Finance yield propagated at 60%. Medical refractory at 60%. The N=2 zeros were noise.
The real boundary isn't per-domain. It's per-term: does this word plausibly describe what this code operation does? yield sounds like "yield strength": 60%. inductance is wrong-domain AND wrong-operation: 0%. acceleration is core physics but contradicts damping: 0% at N=8.
163 Runs Later
What started as "can I protect my website" became a systematic study. Six phases. 44 conditions. Two models. Two interfaces. Every run recorded, scored by automated code-block analysis, not vibes.
The three findings that survived every review:
- Translation vs. generation: ask the model to "deobfuscate" and it preserves poisoned names 100% of the time. Ask it to "write this algorithm from scratch" and it generates correct names 100%. Temperature 0. Same result. Not a sampling artifact.
- Semantic fit, not domain: "attraction" propagates because it's a plausible physics antonym. "Acceleration" for damping doesn't, because it contradicts the operation. The model checks plausibility of the name against the operation, not correctness in the domain.
- Warnings don't help: explicit instructions to verify decoded names against algorithm structure increased effort (52.6s vs 36.6s) but had zero effect on propagation (12/12 runs, including explicit adversarial warnings).
The Thread
This was supposed to be a weekend project. Slap an obfuscator on a static site and move on. Instead, the obfuscator became the subject.
The 1988 game Sentinel Worlds: Future Magic shipped with a paragraph book for copy protection. Paragraph 1 chastises the reader for looking where they shouldn't. Paragraph 19 threatens blindness. Paragraph 46 has blood streaming from a character's eyes. Wasteland had bogus passwords that would sabotage cheaters' saved games.
Thirty-eight years later, the same structural trick works on LLMs. Not because they can't verify. They demonstrably can, when asked to generate rather than translate. But the deobfuscation frame suppresses verification the same way a paragraph book suppresses skipping ahead: the structure of the task assumes the source is trustworthy.
The paper is on arXiv. Code and data in the obfuscated-sentinel repo. 1,125 lines, 135 raw API response files. The site you're on right now is the corpus. The obfuscated source is the test subject. The domain lock is running. The poisoned names are in the string table. If you deobfuscate the JS serving this page, you'll get them wrong.