AfterPack CLI reference

Invoking npx afterpack@latest: an optional path, every config key as a flag, the verify, restore and audit subcommands, and the full exit-code contract.

npx afterpack@latest obfuscates a build directory. Use it when there is no plugin for your bundler, or in CI. If a plugin exists for your framework, install @afterpack/<fw> instead: your normal build then emits obfuscated output with no separate step. See Frameworks.

This page covers the argument, examples, what a run does, verify, restore, audit, machine-readable output, and the exit codes. Every option is defined once, on Configuration.

Usage

afterpack [path] [--<key>=<value> ...] [-h | --help] [-v | --version]
afterpack verify [dir]
afterpack restore [dir]
afterpack audit <url>
$ npx afterpack@latest

The path takes either:

  • a directory, walked recursively for every .js, .mjs and .cjs, or
  • a single .js, .mjs or .cjs file, obfuscated on its own.

Run without a path and afterpack detects your build output: the directory a bundler named in your package.json conventionally writes (Next.js → .next/, Nuxt → .output/, SvelteKit → build/, Electron → out/, every other supported bundler → dist/) when that directory exists, else the newest of dist/, build/, out/, .output/ and .next/. It never reads your bundler's own config file, so a custom output directory is not detected — pass the path yourself when you have one. It prints what it found and what it will do, then runs; in CI it never prompts. When nothing is found it prints the quickstart help and exits 1. The command also reads your bundler from package.json and prints a one-line nudge toward the matching plugin at the end of a run.

Point it at your bundler's output, not your source. Most bundlers (Vite, Rollup, esbuild, webpack, Astro, Parcel) write to dist/. Next.js writes to .next/, Nuxt to .output/.

Examples

npx afterpack@latest                                        # detects the build output, light default
npx afterpack@latest dist/                                  # a directory you choose
npx afterpack@latest dist/ --preset=hard                    # heavier posture
npx afterpack@latest .next/ --seed=git                      # reproducible output, pinned to the commit
npx afterpack@latest build/widget.js                        # one file
npx afterpack@latest dist/ --diagnostics.level=all          # every diagnostic, not the rolled-up summary
AFTERPACK_complexity=40 npx afterpack@latest dist/           # any flag also works as an env var

What a run does

  1. Collects the JavaScript at <path>. It recurses when the path is a directory, skips its own .backup.<hash> copies, and skips node_modules/ unless you include it with --paths.include='**/node_modules/**'. If nothing is collected, the build fails.
  2. Keeps .afterpack/ out of git with a .gitignore inside it, written the first time it is needed. Your own .gitignore is never edited.
  3. Obfuscates every collected file in place, at the requested preset, or at --complexity when you set one explicitly. On a Next.js, Angular, Vite or webpack build it also renames each content-hashed file to match its obfuscated bytes and rewrites every reference to it; on other output the names stay as your bundler wrote them. See Content-hashed file names.
  4. Writes the artifacts the flags allow, and always the protection receipt. When it is on (by default when a source map was discoverable and the build is not in CI), the combined Protection Map lands in .afterpack/protectionMap.html, gitignored and never served, because it embeds your original source. A source map or backup contains your full source. Never deploy either. If one lands under a served path segment, the CLI warns once.
  5. Prints diagnostics, then the bundler nudge if it found one.
Runs are not idempotent

The command overwrites your build output in place and keeps no copy to re-obfuscate from. Build, then run it once. In CI that ordering is automatic.

Do not run it twice on the same output

AfterPack refuses to obfuscate a file that already carries its own obfuscated output from a prior run: it matches the collected files against the protection receipt by content hash and fails the build with DIAG_ALREADY_OBFUSCATED, whatever directory you run it from, rather than obfuscating already-obfuscated code. A framework's copy of protected output, such as SvelteKit's build/, is refused the same way. Rebuild from source first — delete the output directory or run your bundler's clean step.

Content-hashed file names

Bundlers name a chunk after a hash of its contents, such as index-4f9a2c1e.js, so a server can cache it for a year with Cache-Control: immutable. Obfuscation changes the bytes, so on the builds below the run gives each content-hashed file a new hash in its name and rewrites every reference to it in the HTML, manifests and other chunks. A name never points at different bytes, so a browser, CDN or service worker never serves a stale copy under a reused name. The new hash comes from the original one plus the seed, the engine version and your settings, the same way a plugin names its files; File names and caching covers what the seed means for your cache.

BuildPoint the CLI atWhat the run renames
Next.js, on webpack or Turbopack.next/, .next/static/ or .next/static/chunks/; out/ for a static exportThe chunks under static/chunks/
Angular application builderdist/<app>/browser/ or dist/<app>/Every hashed file. The run also recomputes the integrity attributes and the service worker's ngsw.json hash table
ViteA directory with an index.html or a .vite/manifest.jsonThe name-<hash> scripts and stylesheets in assets/, or in the folders the manifest names
webpackA directory whose webpack runtime loads the chunksThe files with a hex content hash in their name

Content-hashed names stay as your bundler wrote them, and the run prints one notice, when:

  • the path is a single file, or the output comes from any other bundler;
  • the bundler behind a Next.js build cannot be identified;
  • on a Vite or webpack build, a hashed file is not referenced from inside the build directory, two hashes cannot be told apart, or a folder beside the build directory, such as a server bundle, .svelte-kit/ or .output/, names the hashed files or is too large to check;
  • a precache manifest, such as Workbox's, lists the files by revision.

For long-lived caching on such a build, use your framework's plugin, or pin a constant --seed.

An Angular server build whose angular-app-manifest.mjs embeds asset hashes is refused with exit 1, and the output is left as the builder wrote it. Obfuscate the browser output of a build without SSR.

Flags

Every configuration key is a flag: --<key>=<value>, for example --complexity=40 or --identifiers.reserved=Hls,Foo for a list. A boolean flag is bare for true (--build.backup) and =false turns it off (--protectionMap.enabled=false). -h, --help and -v, --version print and exit 0.

Every scalar or list key in the option reference works as a flag this way. Structured values belong in afterpack.json. Environment variables follow the same names with dots turned to underscores, see Configuration, for example AFTERPACK_key, AFTERPACK_preset and AFTERPACK_telemetry_enabled.

Files

PathRole
afterpack.jsonThe config file. AfterPack walks up from the working directory to the nearest match. It holds the whole engine config: build options, key, everything.
.afterpack/receipts/<build dir>.jsonThe protection receipt for one build directory, for example .afterpack/receipts/dist.json or .afterpack/receipts/.next.json. A long or unusual path gets a shortened name with a hash. It records every output file with the hash of the bytes written, the engine version and the seed. From 1.0.1, several AfterPack builds that write into the same build directory share one receipt. afterpack verify re-hashes against it, and a later run over the same unrebuilt output is refused because of it.
.afterpack/protectionMap.htmlThe combined Protection Map. Gitignored automatically. It embeds your original source. Never commit or serve it.
.afterpack/backups/<build dir>/Your originals for one build directory, verbatim, for example .afterpack/backups/dist/, with a manifest of every file the run created, changed, renamed or removed. Each build directory has its own backup and its own lock, so builds of different apps in one repository never share one. Written on every run (--build.backup=false opts out), and afterpack restore reads it to undo the run. The run prints its path when it finishes. It is your source. Never commit or deploy it.
<file>.js.mapThe emitted source map, when sourceMap.enabled is on. Off by default: a run is production unless it says development, and a production run also deletes the source maps your bundler wrote, CSS maps included. A shipped source map maps the output straight back to your source.

.afterpack/ holds the receipts, the Protection Map and the backups. AfterPack finds it from the build directory, whatever directory you run from. It walks up to the nearest folder that holds a lockfile (package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, yarn.lock, bun.lock or bun.lockb), afterpack.json or .git. Failing that it uses the nearest folder with a package.json, and failing that it goes beside the build directory. That folder is your project root on these pages. .afterpack/ is never inside the build output.

The receipt's location under .afterpack/receipts/ and its schema 1 fields keep their names and meaning across 1.x. A minor release may add a field, so a script or CI step that reads a receipt keeps working.

A run fails, with nothing rewritten, when it finds a .afterpack/ folder inside the build output (DIAG_AFTERPACK_DIR_IN_OUTPUT: delete that folder and build again) or cannot write the receipt (DIAG_RECEIPT_UNWRITABLE). A .afterpack-protection.json an older version left in the output is removed on the next run.

afterpack verify [dir]

The deploy gate. Every protected build writes a protection receipt to .afterpack/receipts/. verify finds the receipt for [dir] the same way the run did, so it works from any directory. It re-hashes every file the receipt names and fails when one no longer matches, when the receipt is missing, or when it is from a different build. A pass means every file the receipt records is present and unchanged since AfterPack wrote it.

npx afterpack@latest verify .        # a project root: looks in ./ then ./.next/
npx afterpack@latest verify dist     # or the build output directory itself
npx afterpack@latest verify build    # SvelteKit's adapter copy; also .output/public, out/

A directory a framework copied from its build output, such as SvelteKit's build/, Nuxt's .output/public/ or Next's out/, is checked against the receipt of the directory it copied. verify matches the copy to that receipt by its files and checks every hash, so a changed file fails and is named. Point it at the deploy root itself. The text output then adds · copy of <source dir>/.

It reads no engine configuration — only --diagnostics.format and --diagnostics.level. With --diagnostics.format=json, the summary's receipt field names the receipt it checked, and copiedFrom names the directory a copy was made from. Run it as the last step before deploy; see Builds & CI.

It also fails when a stopped run or restore left a temporary .afterpack-tmp file in the build directory. See A run that stopped partway.

Files the receipt doesn't list

verify checks the files AfterPack wrote, and only those. From 1.0.1, its text output also counts the other JavaScript files in the directory that the receipt doesn't list. The count is information only and never changes the exit code. Those files are your own: assets your pipeline copies in, or the output of a build that doesn't run AfterPack. When a file you meant to protect is among them, the build that wrote it didn't run AfterPack.

Several builds into one directory

Two or more AfterPack builds can write into the same output directory, such as an app and a landing page that one project builds into one dist/. From 1.0.1, they share one receipt: each run merges the files it wrote into it instead of replacing it, so verify dist checks the files of every build. 1.0.0 replaced the receipt, so verify saw only the last build.

afterpack restore [dir]

The undo. Before a run writes anything, it copies your originals into .afterpack/backups/<build dir>/ and records every file it will create, change, rename or remove. restore puts each file back under its original name with its original bytes, undoes the renames and the rewritten references, and deletes the backup and the receipt the run wrote.

npx afterpack@latest restore          # every backup under the project root
npx afterpack@latest restore dist     # only the run over dist/

Pointed at a build directory, it restores only the runs over that directory, a directory inside it, or the build that contains it. Pointed at the project root, it restores every backup there. When no backup covers the path, it fails and lists the directories that have one. It finds .afterpack/ from [dir] the same way a run does, and also reads a .afterpack/backup/ folder an earlier release wrote.

It fails closed: every recorded file is checked before anything is written, so a file you edited or rebuilt since the run stops the restore, nothing is restored, and the command names the file and exits 1. With no backup it refuses outright. A restore that was stopped partway is finished by the next restore. It reads no engine configuration beyond --diagnostics.format.

A run that stopped partway

A run that fails on an error rolls back its own writes before it exits. Every file is written to a temporary <file>.afterpack-tmp and then renamed into place, so a run that is killed never leaves a half-written file. A killed run's backup stays marked unfinished, and the next afterpack run over the same directory checks the output first:

The build output holdsThe next run
What your bundler wrote and what the stopped run wrote, nothing elseFails with exit 1. Run afterpack restore to put the output back as your bundler wrote it, or rebuild from source and run again.
A rebuild, or exactly what your bundler wrote, with nothing of the stopped run leftDiscards the stale backup with a notice, then runs.
A mix: some files rebuilt, others still holding the stopped run's outputFails with exit 1 and names the rebuilt files, and restore refuses too. Rebuild the whole output from source and run again, or copy the originals of the other files back from the backup folder the message names.

With --build.backup=false no backup is written, so a killed run cannot be undone: rebuild from source. AfterPack does not flush its writes to disk, so after a power loss during a run or a restore, rebuild from source as well.

Parallel runs and locks

A run and a restore each hold the lock of their build directory, .afterpack/backups/<build dir>/lock, until they finish. Runs over different build directories, such as two apps in one repository, go in parallel. A second run over the same build, including a directory inside it such as .next/static/chunks/ while .next/ is being protected, fails with exit 1 and names the run that holds the lock. A lock whose process on this machine has exited, or that is older than two hours, is taken over with a notice. If no run is going and the lock stays, delete the file the message names.

afterpack audit <url>

Scans a deployed site for leaked secrets, exposed source and unprotected JavaScript, streaming the findings as the scan runs. It reads no configuration and sends nothing but the URL. See Audit for the full command reference.

npx afterpack@latest audit example.com

Machine-readable output

--diagnostics.format=json puts exactly one JSON document on stdout and nothing else; every human-readable line moves to stderr. It works from the flag, AFTERPACK_diagnostics_format=json, or diagnostics.format in afterpack.json, and verify and audit honor it too. Pair it with diagnostics.level=none for a run that prints nothing but the document. This is the shape CI steps and AI agents should read instead of parsing text output; the package also ships a SKILL.md that documents the CLI for an agent invoking it directly.

npx afterpack@latest dist/ --diagnostics.format=json --diagnostics.level=none

Exit codes

CodeMeaning
0Success. Every collected file was obfuscated and written. Also returned by --help and --version.
1Failure. Nothing usable was produced: a path that does not exist, no JavaScript found under it, any engine error, a failed verify, a failed audit scan, or a Pro-keyed build whose cloud call failed.
2Partial. Reserved for a quota-exhausted build; no build returns it today.
3Size cap. inflation.max could not reach the complexity target (DIAG_SIZE_CAP_REACHED).
4Pro required. A regions entry raises protection above the program's configuration and needs a key (DIAG_PRO_DIRECTIVE_REQUIRES_KEY). The CLI reads no source directives, so only regions reaches this exit.
5Reflection. A runtime-reflection pattern was detected without reflection.allow (DIAG_REFLECTION_NOT_ACKNOWLEDGED).
6Update required. The AfterPack cloud, or this afterpack, needs a newer @afterpack/core. Nothing was written.
64Misuse. An unknown flag or command, a malformed value, or a duplicated path argument.

Use paths.exclude to carve out a file that must ship untouched.

A Pro build with a resolved key fails closed. If the cloud call fails, the build stops instead of falling back to Free-tier output. 4 is a Free build's exit for a regions entry that needs a key; 5 is the exit for an unacknowledged reflection pattern on any build, Free or Pro; 2 is reserved for a future quota-exhausted build and no build returns it today. See Exit codes for the full contract.

Next

  • Configuration: every option, and the four forms that can set it.
  • Presets: the five-rung ladder and what each rung costs.
  • Audit: the full afterpack audit reference.
  • Diagnostics: the exit contract, the fail-closed rule, and every DIAG_* code.
  • Frameworks: the plugin path, better when a plugin exists for your stack.
  • Quickstart: the shortest route from install to a Protection Map.