Reverse Once, Run Forever: Defending Code You Can't Hide
Every line of client-side bot detection runs on hardware the attacker fully owns. Here's the engineering philosophy we use to defend code we can never actually hide.
Client-side bot detection has one inconvenient property: the code is right there in the browser.
That's the whole problem. Anything we ship to detect automation runs inside an environment the attacker owns: their CPU, their debugger, their clock, their time. They can set a breakpoint anywhere, run our logic ten thousand times and diff the outputs, or pull a function out and study it offline.
So we start from an assumption that sounds defeatist but isn't: the attacker has read every line of our client code. Not might have. Has. Once that's the baseline, a lot of the standard playbook stops making sense, and the design problem changes shape.
The asymmetry nobody escapes
Server-side, detection logic lives on hardware you control, behind walls you built, observable to no one. An attacker probing it works blind.
Client-side, that advantage is gone. The code has to be in the page, doing its work where the bot is, because that's the only place the signals exist. Mouse kinematics, rendering quirks, timing jitter, the small tells of a real browser driven by a real human: none of it is visible from the server. To see it, you have to run code on the attacker's machine.
This is why the usual move fails. Obfuscate the JavaScript, ship it, and hope nobody untangles the
if statement that decides bot or human. Someone untangles it, patches that one line to always say "human," and the defense is now certifying their bots for them.So we don't treat obfuscation as a lock. A lock keeps people out if they lack the key. Client-side there's no key to withhold, because the attacker holds everything. We treat obfuscation as a cost: how many hours of reverse-engineering does one build take, and does that work still pay off on the next build?
We aren't trying to make the code permanently unreadable. That's impossible. We're trying to make reading it worth as little as possible. The result should be stale, specific to a single session, or useless without our server. Effort in, nothing durable out.
Principle 1: Be a moving target
Reverse engineering is an investment. You spend a week understanding a binary because that understanding pays off next week too. The economics only work if the defense sits still long enough to be worth studying.
So we don't sit still.
Every build reshuffles its internals. The mapping between what an operation means and how it's encoded on the wire is regenerated from scratch each release, so a signature extracted from one version doesn't fit the next. It goes deeper than per-build, too: part of the sensitive machinery is re-randomized per session, so the thing in your debugger is specific to one visitor's one visit. Learn it completely and you've learned nothing that transfers.
Static signatures that survive a rebuild
0
The internal encoding is regenerated every build. Cross-version signatures don't carry over.
Think of it like a combination lock where the numbers are not just secret. The positions of the numbers physically rearrange themselves every time someone walks up to it. Memorizing the combination is useless, because next time the "3" is not where the "3" was.
This has a cost. A defense that mutates every build can also break every build if you're not careful, and obfuscation that silently corrupts the protocol is worse than none. So the transformations have to be behavior-preserving, and we treat that as non-negotiable: we pin cross-implementation test vectors, fuzz the transformed logic against a plain twin across thousands of inputs, and fail the build on the first disagreement. Moving target and byte-for-byte correct, both.
Principle 2: Don't branch, seal
This one inverts an instinct most programmers have.
The natural way to write a check is:
if (debuggerDetected) {
zero_all_memory();
}
It's also one of the most patchable lines you can write. An attacker who finds it doesn't need to understand why it fired. They find the jump and flip it. One byte, done.
So in the parts of the system that matter most, we don't make decisions with branches. We make them with cryptography, branchless by construction.
Instead of comparing a value and branching on the result, the signal is folded into the key material that decrypts the next stage of execution. If everything is legitimate, the math works out and the program continues. If something has been tampered with, a hooked function, a faked value, a missing step, the derived key is wrong and the output is garbage. There's no
if to flip and no "you got caught" flag to find. The code just stops producing a valid result, and never says why.Patchable decision points in the hardened path
None by design
Tampering corrupts the key, not a comparison. There's no branch to invert.
A branch is a question the attacker can overhear and answer for you. A key derivation only opens if the world is actually in the expected state. The attacker can step through it all day; there's no single instruction that means "decide," so there's nothing to neutralize.
The same idea extends across a sequence of steps. Each step folds its identity and result into a one-way running accumulator, and every downstream key derives from that accumulator. Run the steps out of order, skip one, or splice in a replay, and the accumulator is wrong, the keys are wrong, decryption fails. The order becomes part of the secret, with no comparison anywhere to short-circuit.
This has an obvious failure mode, and we're careful about it: you must never fold in signals that legitimately vary between honest devices, or you lock out real users on slower hardware. The list of what's safe to fold is one of the most conservative parts of the codebase.
Principle 3: Don't ship the secret
If the most valuable strings and logic are the things you most want to protect, why are they in the bundle at all?
For the highest-value material, they aren't.
A reverser who downloads our client and studies it offline is missing pieces that were never in the download. Certain lookup tables and certain pieces of executable logic are minted by our server, per session, at handshake, under a key derived from that session. They're delivered just in time, used, and discarded. An attacker running our bundle in isolation has neither the table nor the key to rebuild it, not because they failed to find it but because it isn't there to find.
Lifetime of the most sensitive material
One session
Minted server-side at handshake, used once, gone. Never written to the shipped bundle.
The names of the browser APIs our compiled core touches get similar treatment from the other direction. Hundreds of named host-call slots collapse into a few dozen anonymous numbered channels, with the original names stripped from both sides and the numbering reshuffled per build. Listing what the module calls out to no longer tells you what it's looking at.
Named host-calls, erased
~400 → ~45
Browser-API touch points collapsed into anonymous, per-build-renumbered channels.
Imagine trying to understand a machine by watching which buttons it presses on a control panel, except every label has been removed, there are ten times fewer buttons than functions, and the wiring behind them is rearranged on every shipment. You can see that it's doing something. Working out what takes a lot longer.
The general rule: the best place to keep a secret is somewhere you never put it. Every byte we can mint live, server-side, and let expire is a byte no amount of offline analysis can recover.
Principle 4: Don't trust offline
The phrase that names the worst case is "reverse once, run forever." An attacker spends a week understanding your client, extracts a clean offline replica of your detection, and runs it on their own infrastructure with no rate limits, no observation, and no cost. Every fully static defense eventually collapses into this, no matter how clever it is.
The way out is to move part of the decision off the client. In the most-protected configuration, the client isn't a self-contained judge. It's coupled to our edge across a live round-trip: the logic that turns signals into a verdict can't complete without material the server releases, per step, gated on a cheap liveness proof that the client ran the exact code we issued. Skip the round-trip and you don't get a guessable local fallback, you get a dead husk that produces nothing usable.
That turns "reverse once, run forever offline" into "contact our server every step, forever." A server round-trip is something we can rate-limit, observe, and reason about, so an invisible offline replica becomes a throttleable, visible dependency instead.
The whole thing rides on an authenticated, AEAD-protected wire protocol with keys that roll on a schedule, a single uniform request shape so you can't tell a handshake from telemetry from a decision, and recovery that never leaks which protection just fired. From the outside it's a uniform stream that gives up little about the machinery underneath.
Why we're telling you any of this
A fair question: doesn't writing this hand attackers a head start?
Not really, and that's the point. Everything above describes an approach, not a blueprint. We're a moving target, we seal instead of branch, we mint secrets server-side, and we couple to our edge. Knowing that doesn't help you, because none of it depends on you not knowing it. The whole design assumes a fully-informed attacker, which is the bar a client-side system has to clear.
The defenses that can't be described are the weak ones, because describing them is breaking them. Their entire security budget goes to you not finding the
if statement. Ours doesn't. We'll tell you the shape of the wall. Climbing it is still expensive, still expires, and still ends at a server you have to talk to on our terms.Security that survives being explained
The bar
If telling you how it works breaks it, it was never security. It was a secret waiting to be found.
That's how we build TrustSig's client: instead of trying to win a hiding game you can't win, change the economics so that even full understanding buys the attacker nothing durable. No CAPTCHAs, no puzzles for real users. A defense that assumes the worst about its own exposure and is built to be fine with it.
TrustSig is an invisible, privacy-first bot-detection and form-protection platform built in the EU. If you have reverse-engineered something of ours and disagree with anything above, we would sincerely love to hear from you. That conversation is the best part of this job. Get in touch.