There's a sentence every engineer in this field eventually says out loud, usually with a sigh:
"But the code is right there in their browser."
It is. That's the whole problem, and pretending otherwise is how most bot defenses quietly fail. Anything we ship to detect automation runs inside an environment the attacker owns completely: their CPU, their debugger, their clock, their unlimited patience. They can set a breakpoint anywhere. They can run our logic ten thousand times and diff the outputs. They can rip a function out and study it in a lab.
So we made peace with an uncomfortable starting assumption: the attacker has read every line of our client code. Not "might." Has. Once you actually believe that, most of the industry's playbook reveals itself as theater, and a much more interesting design space opens up.
The asymmetry nobody escapes
Server-side, you have home-field advantage. Your detection logic lives on hardware you control, behind walls you built, observable to no one. An attacker probing it works blind, through a keyhole.
Client-side, every advantage flips. The code has to be there, in the page, doing its work where the bot is, because that is the only place the signals exist. Mouse kinematics, rendering quirks, timing jitter, the thousand tiny tells of a real browser driven by a real human: none of that is visible from your server. To see it, you have to run code on the attacker's turf.
This is the asymmetry that defines the entire discipline. And it is why the classic move (obfuscate the JavaScript, ship it, hope nobody untangles the if statement that decides bot or human) is a losing trade. Someone will untangle it. Then they patch that one line to always say "human," and your defense is now actively certifying their bots.
We don't treat obfuscation as a lock. A lock implies that if you don't have the key, you can't get in. Client-side, there is no key we can withhold; the attacker holds everything. We treat obfuscation as a cost: how many hours of expert reverse-engineering does it take to understand one build? And, more importantly: does that understanding expire?
The honest reframing is this: we are not trying to make our code permanently unreadable. That is impossible, and anyone who promises it is selling you something. We are trying to make the reward for reading it approach zero. Reverse-engineering should buy the attacker a result that is already stale, or specific to a single session, or worthless without our server. Effort in, nothing durable out.
Principle 1: Be a moving target
The first thing we took away from attackers is the thing they rely on most: stability.
Reverse engineering is an investment. You spend a week understanding a binary because that understanding pays off next week, and the week after. The entire economics of attacking a defense assume the defense sits still long enough to be worth studying.
So we don't sit still.
Every build reshuffles its own internals. The mapping between what an operation means and how it is encoded on the wire is regenerated from scratch each release. A signature painstakingly extracted from one version doesn't fit the next one. And it goes deeper than per-build: a meaningful chunk of the sensitive machinery is re-randomized per session. The shape of the thing you are staring at in your debugger is specific to this one visitor's one visit. Learn it perfectly, and you have learned nothing transferable.
Static signatures that survive a rebuild: 0
The internal encoding is regenerated every build. Cross-version signatures don't carry over.For beginners: 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.
There is a subtle discipline cost to this. A defense that mutates every build can also break every build if you are not careful. Heavy obfuscation that silently corrupts the protocol is worse than no obfuscation. So a hard rule sits underneath all of it: the transformations have to be provably behavior-preserving. We pin cross-implementation test vectors, fuzz transformed logic against a plain twin across thousands of inputs, and fail the build outright on the first disagreement. The thing has to be a moving target and be byte-for-byte correct. No exceptions.
Principle 2: Don't branch, seal
This is my favorite idea in the whole system, because it inverts an instinct every programmer has.
The natural way to write a check is:
if (debuggerDetected) {
zero_all_memory();
}
This is also the single most patchable line of code you will ever write. An attacker who finds it doesn't need to understand why it fired. They just need to find the jump and flip it. One byte. Game over.
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 gets folded directly into the key material that decrypts the next stage of execution. If everything is legitimate, the math works out and the program proceeds normally. If something has been tampered with, whether a hooked function, a faked value, or a missing step, the derived key is now subtly wrong, and the output is silently garbage. There is no if to flip. There is no "you got caught" flag to find. The code simply stops producing a valid result, and it never tells you 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 is a lock that only opens if the world is genuinely in the expected state. We would rather build the second kind everywhere it counts. The attacker can step through it all day; there is no single instruction whose meaning is "decide," and so there is nothing to neutralize.
We extend the same idea across an entire 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 itself becomes part of the secret, with no comparison anywhere to short-circuit.
(There is a real engineering tax here, and we pay it carefully: you must never fold in signals that legitimately vary between honest devices, or you will lock out real users on slower hardware. The whitelist of what is safe to fold is one of the most conservative parts of the codebase.)
Principle 3: Don't ship the secret
Here is a question that reframes the problem nicely: 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 critical 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 are delivered just-in-time, used, and discarded. The attacker running our bundle in isolation has neither the table nor the key to reconstruct 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 is actually looking at.
Named host-calls, erased: ~400 → ~45
Browser-API touch points collapsed into anonymous, per-build-renumbered channels.For beginners: Imagine trying to understand a machine by watching which buttons it presses on a control panel, except every button's label has been removed, there are now ten times fewer buttons than functions, and the wiring behind them is rearranged on every shipment. You can see that it is doing something. Working out what is a different and much longer afternoon.
The principle generalizes: the best place to keep a secret is somewhere you never put it. Every byte of sensitive material we can mint live, server-side, and let expire is a byte that no amount of offline analysis can ever recover.
Principle 4: Don't trust offline
The four words that haunt this entire field are reverse once, run forever. An attacker invests one painful week understanding your client, extracts a clean offline replica of your detection, and then runs it on their own infrastructure forever, with no rate limits, no observation, no cost. That is the nightmare. Every static defense, no matter how clever, eventually collapses into it.
The structural answer is to make "forever offline" impossible by moving part of the decision off the client entirely.
In the most-protected configuration, the client is not a self-contained judge. It is coupled to our edge across a live round-trip: the logic that turns signals into a verdict cannot complete without material the server releases, per step, in real time, gated on a cheap liveness proof that the client genuinely 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.
This converts the attacker's dream, reverse once, run forever offline, into its opposite: contact our server, every step, forever. And that is a posture we like, because a server round-trip is something we can rate-limit, observe, and reason about. We have turned an invisible offline replica into a noisy, throttleable, visible dependency. The home-field advantage we lost by running on the client, we claw back by refusing to let the client finish the job alone.
Wrap the whole thing in an authenticated, AEAD-protected wire protocol with keys that roll on a schedule, a single uniform request shape so you cannot tell a handshake from telemetry from a decision, and self-healing recovery that never leaks which protection just fired, and the attacker's view from outside is a smooth, opaque, uniform stream that gives up almost nothing about the machinery underneath.
Why we're telling you any of this
A fair question: doesn't writing this hand attackers a head start?
No, and that is kind of the point. Everything above is a description of philosophy, not a blueprint. We have told you that we are a moving target, that we seal instead of branch, that we mint secrets server-side, and that we couple to our edge. None of that helps you, because none of it depends on you not knowing it. The defenses assume a fully-informed attacker. That is the bar a serious client-side system has to clear.
The mediocre defenses in this space are the ones that can't be described, because describing them is breaking them. Their entire security budget is spent on you not finding the if statement. Ours is not. We will happily 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 is the whole thesis of how we build TrustSig's client: stop trying to win a hiding game you are structurally guaranteed to lose, and start changing the economics so that even total understanding buys the attacker nothing durable. No CAPTCHAs. No puzzles for your real users. Just 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.
Glossary
AEAD — Authenticated Encryption with Associated Data. A cipher mode that encrypts a message and at the same time cryptographically proves it wasn't tampered with. Flip a single bit of the ciphertext and decryption fails loudly instead of quietly handing back wrong data.
Moving target — A defense whose shape changes on every build or every session, so knowledge gained from studying one instance doesn't transfer to the next.
Branchless — Logic that produces a result without a visible if/else decision point, so there's no fork for an attacker to find and force.
Top comments (1)
The "seal, don't branch" idea is the sharpest thing here. Folding the signal into the key so tampering yields garbage instead of a flippable boolean really does invert how everyone writes checks. The flip side I keep poking at is your own debuggability. When a legit user on some weird device produces a wrong key and gets silent garbage, you've also designed away the "why" for yourself, the same missing branch that protects you means there's no failure reason to read. How do you tell genuine breakage from an attack when both just look like a dead husk on your end? That conservative whitelist of safe-to-fold signals must be doing a lot of quiet work.