JavaScript Obfuscator fits best after your JavaScript assets have already been generated. Use the online tool for quick validation, then move into the desktop project and command-line workflow when you want a repeatable release process.
Typical release sequence
- Build or bundle your application so the JavaScript files you intend to ship are ready.
- Open those generated files, or the folders that contain them, in the desktop project.
- Configure obfuscation, exclusion, and cross-file options based on what your application exposes publicly.
- Test the protected output in the same environments where you normally validate a release.
- Generate the command line from the desktop project when you want to repeat the same protection settings automatically.
Good fits for the desktop workflow
- Applications that ship many JavaScript files together and need consistent renaming across them.
- Projects that embed JavaScript inside HTML, PHP, ASP, ASPX, JSP, or similar mixed files.
- Release processes that need the same protection settings reused across many builds.
How to protect public or shared names safely
If your application depends on names that must stay stable, use the Variable Exclusion List and the cross-file options such as Replace Globals and Protect Members. This is the safest way to keep integrations and shared identifiers compatible while still increasing protection.
Automation guidance
The command-line workflow is generated from the desktop project, which means your release automation can reuse the same protection settings instead of recreating them by hand. This is the recommended path for CI jobs, scripted release steps, or any environment where consistency matters more than one-off experimentation.
Start from Use Command Line when you are ready to turn a working desktop project into a repeatable build step. For Node-based pipelines the npm CLI is the canonical path and needs no desktop install at all.
Where protection goes in a CI pipeline
Protection is a release step, not a development step. The ordering below is the one that works in every stack, and each position is load-bearing:
- Install, lint, type-check, unit test. All against unprotected source, because that is what developers debug.
- Bundle and minify. Your bundler needs readable code to trace references and tree-shake. Running protection first destroys the analysis it depends on — the failure modes are enumerated in build order.
- Protect the build output in a single pass, pointing at the output directory rather than looping over files. Cross-file options make their decisions per run, so a per-file loop produces chunks that disagree with each other.
- Archive the identifier map as a private build artifact. Without it you cannot read a production stack trace from this release — see symbolication.
- Smoke test the protected artifact, not the pre-protection one. This is the single most commonly skipped step and the one that catches almost everything.
- Generate hashes, manifests and integrity attributes last, from the files you are actually deploying.
- Deploy the protected directory.
Steps 5 and 6 are where most protected releases go wrong, and both failures have the same root cause: a later step consuming the pre-protection artifact. A useful guard is to have the pipeline delete the unprotected output directory immediately after step 3, so nothing downstream can accidentally reference it.
What each release should retain
A protected release is only supportable if you keep a small set of artifacts alongside it. Store these privately, per release, for as long as that release can appear in a stack trace:
- The identifier map — the private record of which original name became which generated name. Never publish it beside the bundle.
- The exact option set used, ideally the committed config file plus any CI overrides that were applied.
- The seed, if you used one, so the build can be reproduced byte-for-byte later — see reproducible builds.
- Hashes of every emitted file, which is what lets you later prove that a file served in production is the file you released.
- The compatibility-validation result, so a regression has a baseline to be compared against.
Anything that must not ship — source maps, identifier maps, config files with credentials — is covered in deployment hygiene.
Making a protected release reviewable
Polymorphic output is the default: the same input produces a different artifact every build. That is a real protection property, and it makes change review harder, because every rebuild looks like a total rewrite of the file.
If your process needs to answer “did anything actually change in this deploy?”, protect release candidates with a seed derived from the version tag. Every build of a given version is then byte-identical, and a diff between two builds of the same version is empty unless something genuinely changed. Different versions still look nothing alike, so you keep polymorphism between releases while gaining diffability within one.
npx jso-protector --config jso.config.json \
--seed "$RELEASE_TAG" \
--input dist --output dist-protected \
--manifest dist-protected/jso-manifest.json
Smoke tests worth running on every protected build
Keep this short enough that it runs on every release rather than being skipped. The goal is coverage of the paths where protection changes behaviour, not coverage of your application:
- Load the app cold and confirm no console error on first paint.
- Exercise one path that crosses a network boundary, so JSON field names are checked.
- Exercise one path that reads or writes storage, so persisted keys are checked.
- Navigate to a lazily loaded route, so cross-chunk naming is checked.
- Confirm no
.map file and no identifier map is reachable from the deployed origin.
Compatibility validation automates the first layer of this by checking the protected output parses and behaves like its input; the list above covers the contracts that only your application knows about.
Practical rule of thumb: obfuscate the code you actually ship, not the original authoring sources that will still be transformed later by other build steps. If a step after protection reads the pre-protection files, that step is a bug waiting to reach production.
Frequently asked questions
Where exactly does protection belong in a CI pipeline?
After bundling and minification, and before hashing, manifests and deployment. Install, lint, type-check and unit tests all run against unprotected source because that is what developers debug. Your bundler then needs readable code to trace references and tree-shake, so protection cannot come first. Protection runs on the build output, and only then do you archive the identifier map, smoke test, generate hashes and deploy.
Should we protect file by file or point at the output directory?
Point at the output directory and protect in a single pass. Cross-file options make their decisions per run, so a loop that protects one file at a time produces chunks that disagree with each other about which shared name became what. A single pass over the directory is the only arrangement in which cross-file renaming stays consistent.
Which step is most commonly skipped, and what does it cost?
Smoke testing the protected artifact rather than the pre-protection one. It is the single most commonly skipped step and the one that catches almost everything. The related failure is generating hashes or integrity attributes from the wrong directory. Both have the same root cause, which is a later step consuming the pre-protection output, and a useful guard is to have the pipeline delete the unprotected output directory immediately after the protection step so nothing downstream can reference it.
What should each release retain, and for how long?
The identifier map, the exact option set used, the seed if you used one, hashes of every emitted file, and the compatibility-validation result. Store them privately, per release, for as long as that release can still appear in a stack trace. The identifier map in particular must never be published beside the bundle.
How do we review a diff when every build looks completely different?
Protect release candidates with a seed derived from the version tag. Polymorphic output is the default, so the same input normally produces a different artifact every build, which makes change review hard because every rebuild reads as a total rewrite. With the seed tied to the version, every build of a given version is byte-identical and a diff between two builds of that version is empty unless something genuinely changed. Different versions still look nothing alike, so you keep polymorphism between releases and gain diffability within one.
How do we keep names that other systems depend on from being renamed?
Use the variable exclusion list together with the cross-file options. If your application depends on names that must stay stable, such as public callbacks, integration entry points or shared identifiers, exclude them explicitly rather than hoping a transform leaves them alone. This is the safest way to keep integrations compatible while still raising protection on everything else.