By default, protecting the same file twice produces two different artifacts. That is deliberate. Supply a Seed and it stops: the same input, the same options and the same seed produce byte-identical output, every time, on any machine.
Why output changes when your source did not
Protection draws on randomness in several places at once. Generated identifier names, the order of the moved-string table, the shape of the per-build decoder, dead-code placement and wrapper selection are all chosen from a fresh entropy stream on every run. That is what polymorphic output means, and it is a real property rather than a side effect: a signature or an automated unpacker written against last week’s release does not match this week’s.
The cost is that your build is no longer reproducible. Two consecutive CI runs on an unchanged commit emit different bytes, so artifact hashes churn, caches miss, and a diff between two releases is unreadable. If you have ever tried to answer “did anything actually change in this deploy?” against protected output, that is the problem the seed solves.
Using a seed
The seed reaches the engine as the Seed option. Three routes are equivalent — use whichever fits your pipeline.
# 1. Command line
npx jso-protector --config jso.config.json --seed 20260810
# 2. Config file key
{
"input": "dist",
"output": "dist-protected",
"seed": "release-4.2.0"
}
# 3. Explicit engine option
{
"options": { "Seed": 20260810 }
}
An integer is used directly. Any other string is folded to a stable numeric value, so "release-4.2.0" is a perfectly good seed and is easier to trace back to a release than a bare number. The same string always folds to the same value, on every platform and in every process.
What byte-identical actually covers
Determinism runs through the whole pipeline, not just the naming layer. With a fixed seed the following are stable across runs:
- Generated identifier names from name mangling, and the rename decisions made by Protect Members and Replace Globals.
- The moved-string table — its contents, its shuffle order, its rotation offset and the index representation chosen per lookup. See Move Strings Into Array.
- String encoding and encryption parameters, including the generated decoder emitted by Encrypt Strings.
- Self-compression output from the Compressor, which is the layer most people assume will still vary. It does not.
- Dead-code insertion and control-flow layout produced by Flat Transform.
- Wrapper selection and noise arguments in the string-array wrapper controls.
Reproducibility was validated end to end against a 64-library corpus with self-compression enabled: same seed, byte-identical output across repeated runs; no seed, output varies; different seeds, output differs.
What a seed does not do
A fixed seed is not a security control. It reduces per-build variation, which is the one property polymorphism was giving you. Two releases built with the same seed and the same source produce the same artifact, so a signature written against one matches the other. Use a seed because you need attestation or diffability — not because it makes anything harder to read.
Two more boundaries worth stating plainly:
- It does not survive an option change. Reproducibility is over the triple of input, options and seed. Change a single option and the output legitimately differs, seed or not.
- It is not a secret, but it is not for logs either. A seed is an input to the transform, and publishing it alongside your artifact tells an analyst that your builds are stable and comparable. Keep seed values out of shared build logs and support tickets.
When to use one
Use a seed Release artifacts you must be able to rebuild and attest to. Comparing two candidate builds. Stable Subresource Integrity hashes across a rebuild. Regression tests that assert on protected output. Debugging a report that only reproduces on one specific build. |
Omit the seed Ordinary production releases, where you want each build to look different from the last. This is the default, and it is the right default for anything shipped to the public. |
A common middle ground is one seed per release, derived from the version tag. Every build of 4.2.0 is reproducible, and 4.3.0 looks nothing like it — you keep attestation within a release and polymorphism between releases.
Verifying reproducibility in CI
Do not take it on trust; make the pipeline prove it. Protect twice into different folders and compare hashes.
npx jso-protector --config jso.config.json --seed "$RELEASE_TAG" \
--input dist --output out-a
npx jso-protector --config jso.config.json --seed "$RELEASE_TAG" \
--input dist --output out-b
diff -r out-a out-b && echo "reproducible"
Run it once when you adopt seeding and keep it as a scheduled job. It costs one extra protection pass and catches the case where an option you added later reintroduces variation. Pair it with a --manifest or --report file so each release records the seed, the option set and the resulting hashes together — see release workflows for where those artifacts belong.
Related pages
- jso-protector CLI — full flag reference, including
--seed.
- Release workflows — where seeds, manifests and identifier maps sit in a release.
- Symbolication — the identifier map is per build, so a stable seed makes trace translation easier to reason about.
- Deployment hygiene — what must and must not ship beside a protected artifact.
- Build order — protection runs last, which is what makes its output the thing you attest to.