zipnative-cli v1.0.0
Released 2026-09-05
Built on zipnative 1.0.0 — the engine's first stable release, whose 77-export API, 39-code
error vocabulary and deterministic: true output bytes are frozen under semver. This is the
first release of the CLI: 15 commands in four groups over that frozen surface, a thin
dispatch layer with no ZIP parsing of its own, an agent contract that carries the engine's
ZIP_* codes verbatim next to 13 stable E_* classes, secure-by-default extraction with the
CLI as the proven filesystem trust boundary, and a blocking ISO/IEC 21320-1:2015 conformance
gate over every archive the CLI writes. Offline in every mode. Zero extra runtime dependencies.
Node.js >= 22. The release branch closed with two independent audits (74 accepted findings,
every one tagged in CHANGELOG.md).
Highlights
- Reproducible archives from the shell:
zipnative create dist/ --deterministic -o release.zip # identical SHA-256 on every runtime zipnative inspect --input release.zip --check deterministic,no-symlinks,no-encryption zipnative verify --input release.zip --strict - Extraction that refuses instead of guessing — zip-slip and Windows device names,
symlink entries, overlapping entries, central/local header disagreement, Zip64 spoofing,
duplicate output paths and decompression bombs are refused by default, each with its
ZIP_*code; the CLI then re-proves every destination stays under--output-dir
(lexically and physically, throughrealpath), opens every file exclusively, never
overwrites without--overwrite, and never materialises a symlink. Opt-outs skip; they
never write anything unsafe. - One envelope, two codes —
--jsonputs{ ok: false, command, error: { code, message, zipCode?, entryName?, detail? } }on stderr:codeis the class (13E_*values),
zipCodeis the exact cause (zipnative's 39 frozen codes, verbatim),detailcarries
the engine's structured fields. Branch on codes, never on text. streamfor what you cannot seek — list, extract or--catfrom stdin, a pipe or an
upload body, with the engine's trust caveat made explicit (trust: "local-headers-only").modifywithout recompression, and without laundering — add, replace, remove, rename;
append-only by default,--compactfor true deletion;--in-place; every entry it
re-emits is verified first.- Agent ergonomics —
--dry-runon seven commands,--summary/--fields/ compact
JSON,--strict, eight--max-*bounds plus--max-input-size, 22schemasubjects (JSON
Schemas, an error registry and a capability manifest),doctoras a preflight,
batch --manifestpipelines whose--jsonoutput is one document, andllms.txt+
AGENTS.md+docs/data/errors.jsonin the package. - Conformance is proven, not asserted — every archive the CLI writes is validated
against ISO/IEC 21320-1:2015 by an engine-independent validator (veraZIP), blocking in CI
on Linux and Windows on every PR and before every publish, with negative canaries the
validator must reject and hostile-but-conformant archivesextractmust refuse.
Security
No parser, writer or codec of its own — every ZIP decision is the engine's. What the CLI adds,
and what the audit pass hardened before this release:
- Sink hardening (CWE-59 / CWE-367) —
extractandstream --output-dirshare one sink
(src/utils/sink.ts). Beyond the lexicalsafeJoinplan, the nearest existing ancestor of
every target isrealpath'd under the root beforemkdir -pand the created directory is
re-checked after, so a symlink or junction planted inside the destination cannot redirect a
write (E_SECURITY). Files are opened exclusively (wx) unless--overwrite, closing the
check-then-write window; partial files are removed on failure. The residual realpath → open
window is documented: extract into an empty or trusted destination. - Uniform overwrite policy —
create/modify/cat/inflate --output,extract,
stream --output-dirandbatch --task createrefuse an existing file (E_IO) unless
--overwrite;modify --in-placewrites an unpredictable, exclusively created temp file
and renames atomically. --max-input-size(CWE-400) — every buffered read (stdin and files, random-access
commands,create --stdin-name,inflate --sync) is bounded, 4 GiB by default,E_LIMIT
withdetail { limit: "maxInputSize", configured, observed }; streaming commands stay
constant-memory.modifyverifies what it re-emits — eager open, thenverifyEntry()on every entry
not removed or replaced: a CRC lie, a size lie or a local header that contradicts the
central directory is refused with the entry named (E_DATA/E_SECURITY), instead of
being copied verbatim into a canonical-looking archive. Encrypted and stream-only-codec
entries are copied as-is and counted (verifySkipped). No opt-out; runs under--dry-run.- Codec truth —
--codecstays the only dynamic import of user code (argv only, refused
from config files and from manifests without--allow-codec-load), and it is reported
honestly: a module registering method 0 / 8 replaces the writer's compressor forcreate/
modify(even under--deterministic; awarning:line says so), adeflateImplshows as
tier: "injected"unless--deterministic, andcreate --parallelrefuses such a module
because its workers cannot see it. - Signal cleanup —
SIGINT/SIGTERMremove only the file being written at that moment
(never a completed output, never the original of--in-place) and exit 130 / 143. - Argv paths are the user's —
../a.zip,-o ../out.zipare ordinary shell usage and are
accepted; the..refusal applies to path values that arrive as data (manifests), and every
entry name the CLI writes goes throughsanitizeEntryPath(). - 50 MB JSON / 1 MB config / 1 000-task caps, a mandatory
inflateoutput bound, no network
surface in any mode. Published via Trusted Publishing (OIDC) with npm provenance, an
attested CycloneDX SBOM (actions/attest-build-provenance) and a verified bin-only tarball;
CodeQL and OpenSSF Scorecard in CI.
What's new
Create & modify
create— files, directories, stdin (--stdin-name) or a JSON manifest
(--from-manifest) throughcreateZip;--deterministic(pinned pure-TS encoder),
--stream(constant memory, streamed inputs in the data-descriptor layout, reported as
layout),--parallel/--workers/--min-job-size/--job-timeout
(createParallelZipfromzipnative/worker, loaded lazily with an explicit worker URL),
--method,--level,--order canonical|insertion(insertion = argv order: an EPUB
mimetypelisted first is written first),--date epoch|now|<ISO>(UTC wall-clock),
--mtime,--comment/--comment-file(binary comments),--entry-comment,
--preserve-mode,--store-ext,--base,--prefix,--dir-entries,--include/
--exclude,--follow-symlinks,--overwrite,--dry-run; manifestextraFieldsand
commentBase64. Every entry name is pre-checked with the engine'ssanitizeEntryPath().modify—--remove,--rename,--replace,--add,--add-dir,--comment/
--comment-fileor--from-manifest(mode,extraFields,commentBase64), applied in a
fixed order throughcreateZipModifier; append-onlysave()by default (aninfo:line
documents data remanence and the 7-Zip caveat),--compactforsaveCompact(),
--in-place,--overwrite,--dry-run; envelopeverified/verifySkipped/tier/
layout.
Read & extract
list— text (unzip -lstyle) | json | ndjson,--long(withrawNameHexand
commentHex),--validate eager, globs,--summary/--fields. Nothing decompressed.inspect— eager open, archive facts, per-method statistics, a determinism verdict that
separates reproducibility (deterministic) from form (canonicalLayout), every diagnostic;
--entries/--entry/--extra; 19--checkassertions that print the report then exit
1 /E_CHECK_FAILED.cat— random-access streaming of one or more entries,--raw,--no-verify-crc,
--output(partial file removed on failure,--overwrite),--dry-run; a sync-only
--codecmethod falls back toreadEntry().extract— the two-phase sink (plan withsafeJoincontainment and overwrite /
case-fold checks, then write with realpath containment, exclusive open and backpressure);
--skip-unsafe,--skip-symlinks,--allow-symlinks(target text as a file),
--skip-unsupported,--on-duplicate,--overwrite,--flat,--buffered,
--preserve-mode,--preserve-mtime,--dry-run.stream—iterateZipEntriesover unseekable input:--list(default),
--output-dir,--cat;--long,--skip-unsafe,--skip-unsupported; attribute-dependent
flags refused;trust: "local-headers-only"in every JSON output;--summarycarries
descriptorEntries/bytesKnown.
Integrity & codecs
verify—verifyZip's report plusfailed/skipped/strict;--entry
(repeatable) verifies named entries throughverifyEntry()and reportsselected;
encrypted entries honestlyskipped; exit 1 /E_VERIFY_FAILEDwithzipCodefor
structural refusals;--strictalso fails on any diagnostic.crc32— files or stdin in 64 KiB chunks;--seed,--expect(exit 1 /
E_CHECK_FAILEDwith both CRCs indetail).inflate— the resumable inflater with a mandatory--max-outputbound (default:
the effective--max-entry-size);--sync,--method deflate|store|<id>(codecs via
--codec),--allow-trailing,--overwrite,--dry-run;bytesConsumedin the envelope.
Automation & meta
batch— directory mode (--task createper subdirectory through the fullcreate
command,--task verifyper archive,--concurrency1–64,--fail-fast,--overwrite)
and--manifestpipelines (10 whitelisted manifest commands,"@<id>"output references,
strict pre-validation,--continue-on-error,--allow-codec-loadgate, 1 000-task and
50 MB caps); under--jsonevery task's output is captured into one batch document
(tasks[i].report);--summary/--fields/--dry-run.doctor— versions (package vs the engine'sVERSIONexport), deflate tier and
pinned tier, web streams, workers, codecs, effective limits (as numbers under--json,
maxInputSizeincluded), command count; exit 0/1.schema— 22 subjects: the three manifests, every report and--summaryshape,
doctor,govern-verify,crc32, thestatusanderrorenvelopes,errors(codes
and the 39-entry mapping),limits,diagnostics, and the capabilitymanifest.completion— bash, zsh, fish, powershell (path flags complete files).govern—
rules, policy,verify-issue(exit 1 /E_POLICY), pinned to the.githubfiles by a test.
Global options and agent surface
--json,--pretty,--dry-run(create,extract,modify,stream,cat,
inflate,batch),--strict,--quiet,--no-color(NO_COLOR/FORCE_COLOR/
TERM=dumb),--config/--no-config(.zipnativerc.json,codecrefused),
--version --json,--format, -fon every command that has a format. Global flags work
before or after the command name; flags and positionals are order-independent.- The eight
--max-*bounds (--max-entries100000,--max-entry-size1 GiB,
--max-total-size8 GiB,--max-ratio1024,--max-name-bytes4096,
--max-extra-bytes65535,--max-comment-bytes65535,--max-cd-bytes256 MiB) over
the engine's CWE-taggedZipLimits, plus the CLI-owned--max-input-size(4 GiB);none
disables a bound with a warning. --pure-codecsand--codec <module>(argv only, gated in manifests, reported truthfully when a module shapes the writer).- 13 stable
E_*classes; the 39ZIP_*causes carried verbatim through a typed mapping
that failstscwhen the engine adds a code; the 11 diagnostic codes bridged to stderr
text,--jsonarrays or a--stricterror;docs/data/core-exports.jsonand
docs/data/errors.jsonshipped for agents;AGENTS.md,llms.txt. - Process contract: exit 0 / 1 / 2 (usage — including an unknown command), 130 / 143 on
signals; a closed downstream pipe ends the run quietly with exit 0; a terminal with nothing
piped is refused instead of blocking;ZIPNATIVE_JSON,ZIPNATIVE_DRY_RUN,
ZIPNATIVE_QUIET,ZIPNATIVE_STRICT,ZIPNATIVE_PURE_CODECShonoured from the environment.
Compatibility notes
The machine contract (envelope fields, E_* classes, the ZIP_* mapping, exit codes, schema
subjects) is the baseline every later 1.x release must keep additive. The audit pass settled
the following before that baseline froze — first release, so nothing to migrate, but scripts
written against pre-release builds should note:
- Dates are UTC wall-clock.
--date <ISO>and manifestdatevalues without a zone
designator are read as UTC; the stored DOS fields — and the archive bytes — are identical on
every host.nowand--mtimeare local and not reproducible. DOS resolution is 2 seconds
(odd seconds floored, warning), range 1980–2107 (warning outside). --order insertionmeans the argv order (each directory still walks name-sorted;
manifests keep their entries order). The defaultcanonicalorder is unchanged.- Argv
..is accepted.zipnative list ../a.zipand-o ../out.zipwork; only manifest
path values keep the..refusal (E_INPUT). - Existing outputs are refused unless
--overwrite— every writer, not only the
extraction sink (E_IO, the file is left intact). Re-run scripts pass--overwrite. batch --manifest --jsonwrites one stdout document; a task that would write its
artefact to stdout (create/modify/cat/inflatewithoutoutput,stream --cat)
is refused at validation (E_USAGE). Text mode keeps the interleaved output.- Unsafe entry names are
E_INPUT(exit 1) everywhere —modify --add/--rename/
--add-dirandcreate --stdin-nameincluded; malformed flags stayE_USAGE(exit 2).
Unknown commands areE_USAGE(exit 2). unixModeis four octal digits ("0000","0644","4755").stream --summaryis{ entries, bytes, descriptorEntries, bytesKnown, trust }:
data-descriptor rows carry the local header's zero sizes andbytesexcludes them.inspect --check deterministicasserts reproducibility only; usecanonical-layout
(alias ofno-data-descriptor) for the buffered layout. Acreate --streamarchive is
deterministic: true,canonicalLayout: false.- CJS-only package. The tarball ships
dist/cli.cjs(the bin) and nothing else
executable — no ESM build, no type declarations, no source maps; use the CLI, not an import. - Node.js >= 22 (the engine declares
engines.node >= 22). CI: Ubuntu Node 22 and 24,
Windows Node 22 and 24 (blocking), macOS Node 22. zipnative ^1.0.0is the sole runtime dependency and stays external in the bundle
(zipnative/workerresolves its own worker script).
Notes
- Append-only
modifyoutput and 7-Zip. The defaultsave()keeps the original bytes
verbatim (removed / replaced content remains recoverable), and 7-Zip's CLI is known to
mis-read that layout — it extracts the stale payload and misses appended entries because
it does not honour the final central directory (unzip, bsdtar, Python, jar and
Expand-Archive all do). Pass--compactwhen deletion or 7-Zip interoperability matters. streamtrusts local headers alone. There is no central directory to cross-check
names, sizes, methods or attributes; mode and symlink policy are unavailable and every
JSON output saystrust: "local-headers-only". Preferlist/extracton a complete
file; store, encrypted or custom-codec entries with data descriptors are refused in
forward mode, and a custom-method entry can be listed and skipped but not decoded there
(E_DATA— usecat/extract --codecon the complete file).- Encryption is not supported by engine policy in 1.x; encrypted entries are detected,
listed, reported asskippedbyverify, skippable withextract/stream --skip-unsupportedand copied unverified bymodify. - Streamed entries above 4 GiB are refused (
ZIP_UNSUPPORTED_ZIP64_STREAMING) until the
engine's per-entry Zip64 streaming lands; buffered entries are fully Zip64. - Memory. Random-access commands hold the whole archive in memory (bounded by
--max-input-size);stream,crc32,inflate(default) andcreate --streamare
constant-memory;create --stream --deterministicbuffers per entry (the pinned encoder is
whole-buffer). - Every sample ships as a
.sh+.ps1pair (41 demos, 73 jobs insamples/run-all.js) and
runs offline;samples/agent/walks the recommended agent loop.
Install
npm install -g zipnative-cli@1.0.0Upgrade
First release — nothing to upgrade from. Pin zipnative-cli@^1.0.0; the --json contract
and the exit / error codes are the 1.x baseline.
Verification
- 1202 vitest tests across 61 files (1193 passed + 9 platform-conditional skips) green on
Ubuntu (Node 22, 24), Windows (Node 22, 24) and macOS (Node 22); coverage statements
96.32 % / branches 92.52 % / functions 97.93 % / lines 96.91 % against the enforced thresholds
93 / 88 / 94 / 93. - veraZIP: the 37-archive corpus written by the built CLI validated against
ISO/IEC 21320-1:2015 — 33 PASS + 4 XFAIL, 0 FAIL (negative canaries correctly rejected
with their declared check ids; the 3 hostile-but-conformant archives PASS the profile and
are refused byextract), exit 0. The same gate runs blocking in CI (verazip.yml, Linux +
Windows, every push and PR,VERAZIP_REQUIRED=1) and pre-publish (publish.yml). npm auditclean;npm run lint(src and tests) 0 errors; built binary smoke-tested
(node dist/cli.cjs …) for every command, includingcreate --parallel(worker script
resolved from the bundle) anddoctorreporting thenode-zlibtier;samples/run-all.js
73/73.- Package: 7 files, 120.0 kB packed —
dist/cli.cjs,AGENTS.md,
LICENSE,README.md,llms.txt,docs/data/errors.json,package.json. - SBOM (CycloneDX, attested) and npm provenance attached by the release workflow. Consumers
can verify the published package's provenance and registry signatures with
npm audit signaturesafter installing, and the SBOM with
gh attestation verify sbom.cdx.json -R Nizoka/zipnative-cli.