# How the AfterPack JavaScript obfuscator works

What standard (Free) output does to your code, how it resists reversal, and what an attacker can actually recover.

Source: https://www.afterpack.dev/docs/concepts

AfterPack is a post-build JavaScript protector. It takes the bundle your toolchain produces and rewrites it into something a human, or an AI agent, can't read, search, or modify without disproportionate effort. It wires into [your existing build](https://www.afterpack.dev/docs/frameworks) in one line, or runs [over the output directory](https://www.afterpack.dev/docs/cli) with no plugin at all.

## No peelable obfuscation layers

Most obfuscators ship two things an attacker can exploit: a stack of layers applied one after another, and pre-baked structures a tool recognizes on sight (a string array, even an encrypted one, a custom VM interpreter, a recognizable decoder bootstrap). The reversal recipe is mechanical: identify a layer or structure by its signature, strip it, repeat until the original falls out. `webcrack` and `restringer` do exactly this, in microseconds.

AfterPack ships none of that. There is no layer to peel back and no fixed structure to fingerprint: every part of the output depends on every other part, and the whole shape changes with each build, so a tool written against one build does not carry to the next. [How it compares](https://www.afterpack.dev/docs/comparison) to those tools is its own page.

*(animated "no peelable layers" comparison — see the web page)*

## AI agents changed the threat

A frontier model doesn't just summarize minified code: agents like Claude Code now deobfuscate the output of traditional obfuscators. In one public experiment, an agent stripped both `javascript-obfuscator` and a commercial tool (JScrambler) back to readable logic in about ten minutes ([the run is written up on our blog](https://www.afterpack.dev/blog/ai-deobfuscates-javascript)).

Renaming, a string array, and a few layers no longer buy protection: they are the exact patterns these tools and models are best at. The [threat model](https://www.afterpack.dev/docs/threat-model) ranks which adversary defeats which technique. What still holds is output with no fixed structure to learn, and a new shape every build: what a tool learns from one build does not apply to the next.

## What standard obfuscation already does

AfterPack runs after your bundler, on built output, and never touches your source, through a [framework plugin](https://www.afterpack.dev/docs/frameworks) or the [post-build CLI](https://www.afterpack.dev/docs/cli#what-a-run-does). [Quickstart](https://www.afterpack.dev/docs/quickstart) is the five-minute version.

*(build flow diagram — see the web page)*

Free applies all of this:

- **It encodes your string literals.** At any positive [complexity target](https://www.afterpack.dev/docs/config#complexity) the [string floor](https://www.afterpack.dev/docs/config#strings-encode) is on, so a string literal is rewritten into a [seed](https://www.afterpack.dev/docs/config#seed)-chosen runtime decoder instead of sitting in the artifact as text. This is on by default, and it is the single biggest difference from minified code. Directive prologues, `typeof` comparands, property names the runtime needs and export names stay readable, and a few source string values can remain inside heavily-minified third-party bundles; the build reports how many survived and the [Protection Map](https://www.afterpack.dev/docs/protection-map) lists them.
- **It rewrites your logic into equivalent but information-destroying forms**: mixed-boolean arithmetic, opaque predicates, an integer-expression bytecode VM. An attacker can recover a working program, not your original; the algorithmic choice is gone. [`transforms.<kind>.enabled`](https://www.afterpack.dev/docs/config#transforms-kind-enabled) toggles the individual families.
- **It reshapes control and structure**: control-flow flattening, dead branches, scope deepening, cross-dependency entanglement.
- **It changes everything every build**, through per-build [polymorphism](#a-new-program-every-build) (below).

The zero-config default is [`light`](https://www.afterpack.dev/docs/config#preset): the [string floor](https://www.afterpack.dev/docs/config#strings-encode) plus light structural [inflation](https://www.afterpack.dev/docs/config#inflation-max). Raising the [preset](https://www.afterpack.dev/docs/presets) adds more structural work; string readability is already at its floor regardless of preset. All of this runs on every tier, Free included; [Tiers](https://www.afterpack.dev/docs/tiers) is where the Free and Pro boundary lives.

## A new program every build

AfterPack derives its output from a per-build [seed](https://www.afterpack.dev/docs/config#seed) that's random by default. Two builds of the same source come out structurally different: different names, different encodings, a different shape everywhere.

This goes further than a cosmetic reshuffle. AfterPack changes the structure of the program itself, not just the text of it: control flow is flattened and reordered, values are recomputed at each use instead of stored, and parts of the bundle that were independent come out entangled with each other. What ships is a functional equivalent of what you wrote, a different program that behaves the same way, and the seed decides which equivalent you get this time.

So there is no fixed structure to fingerprint, and a deobfuscator written against one build does not carry to the next. An attacker is left reversing one specific build by hand, and that work does not transfer: the next release is a different program again. Shipping often repeats the cost every time. [Builds & releases](https://www.afterpack.dev/docs/builds) covers how to make that a habit.

For reproducible output, for cache hits or audits, pin the seed with [`--seed=git`](https://www.afterpack.dev/docs/config#seed) on the [CLI](https://www.afterpack.dev/docs/cli#flags), or [`seed: "git"`](https://www.afterpack.dev/docs/config#seed) in a plugin. Both derive it from your commit. See [Builds & releases](https://www.afterpack.dev/docs/builds).

## What an attacker can actually recover

Not every transform raises the cost of reversal the same way. AfterPack sorts its protection into [reversal classes](https://www.afterpack.dev/docs/reversal-classes); knowing which one covers a region tells you what an attacker walks away with.

These are static-attacker classes: they describe what someone who reads the bytes faces, grep, AST tooling, or an LLM that explains code without running it. An attacker who runs the artifact under an instrumented runtime is a separate, stronger case, covered under [Scope](#scope) below and on the [threat model](https://www.afterpack.dev/docs/threat-model) page.

Every build reaches both available classes across the whole bundle, and the [Protection Map](https://www.afterpack.dev/docs/protection-map) shows which class hit which line.

The [region overrides](https://www.afterpack.dev/docs/directives#what-you-can-set), [`preset=hard`](https://www.afterpack.dev/docs/config#preset) and [`preset=extreme`](https://www.afterpack.dev/docs/config#preset), raise a region's [complexity target](https://www.afterpack.dev/docs/config#complexity) and force its [string floor](https://www.afterpack.dev/docs/config#strings-encode) on, concentrating that same work where you ask for it.

## How secrets survive

Protecting a secret uses a different mechanism than destroying provenance. A secret (an API key, a license seed, a threshold) appears in any program that behaves the same way, so destroying which original you authored doesn't hide it.

What hides a secret from a static or blind-LLM reader is **non-materialization**: the value never appears in the output. [`transforms.comparisonHardening.enabled`](https://www.afterpack.dev/docs/config#transforms-kind-enabled) delivers it for a literal your code compares against, rewriting the comparison into a one-way digest check so only the digest ships. [`preset=extreme`](https://www.afterpack.dev/docs/config#preset) on a secret literal makes recovery expensive, and the literal still exists in the output.

An attacker who runs your code under an instrumented runtime can reach any secret the code itself uses. No build-time obfuscator changes that: if the running program needs the value, a determined dynamic attacker can observe it. Non-materialization defeats the static and blind-LLM reader and raises the re-extraction cost on every rotation. It does not make a client-side secret unrecoverable. High-value secrets belong server-side, where the client never holds them.

## Scope

A license check in a [`/* @afterpack preset=hard */`](https://www.afterpack.dev/docs/config#preset) region, shipping often with a varying [seed](https://www.afterpack.dev/docs/config#seed), isn't worth the engineering hours to crack before the next release lands. For most products, that is the right bar.

The defense is economic. Given unbounded time, any client-side code can be reversed to some degree. The goal is to push the cost above the value, and to make that cost recur every release. When an asset is worth more than any reversal effort, keep it server-side where the client never holds it. AfterPack strengthens that architecture; [it doesn't replace it](https://www.afterpack.dev/docs/threat-model#what-afterpack-does-not-replace). The [best-practices guide](https://www.afterpack.dev/docs/best-practices) covers how to spend protection where it counts.

## Next

- [Reversal classes](https://www.afterpack.dev/docs/reversal-classes): the three-class table in depth, per-transform.
- [Threat model](https://www.afterpack.dev/docs/threat-model): the A0 to A5 adversary ladder and the static-vs-dynamic axis.
- [Protection Map](https://www.afterpack.dev/docs/protection-map): the per-token view that proves which class hit which line.
- [Best practices](https://www.afterpack.dev/docs/best-practices): what to mark, what to leave alone, and how to rotate builds.
- [Tiers](https://www.afterpack.dev/docs/tiers): the full Free / Pro / Team / Enterprise boundary.
- [Quickstart](https://www.afterpack.dev/docs/quickstart): install the plugin and ship protected output in minutes.
- [Frameworks](https://www.afterpack.dev/docs/frameworks): the plugin for your stack, or the CLI path when there isn't one.
- [Configuration](https://www.afterpack.dev/docs/config): every key named above, and which surface can set it.
- [Presets](https://www.afterpack.dev/docs/presets): the `minify`/`light`/`medium`/`hard`/`extreme` ladder.
- [Directives](https://www.afterpack.dev/docs/directives): the marker grammar behind `preset=hard` and `skip`.
