Skip to content

v1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0

Latest

Choose a tag to compare

@Nizoka Nizoka released this 05 Sep 11:49
dd43f98

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, through realpath), 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--json puts { ok: false, command, error: { code, message, zipCode?, entryName?, detail? } } on stderr: code is the class (13 E_* values),
    zipCode is the exact cause (zipnative's 39 frozen codes, verbatim), detail carries
    the engine's structured fields. Branch on codes, never on text.
  • stream for what you cannot seek — list, extract or --cat from stdin, a pipe or an
    upload body, with the engine's trust caveat made explicit (trust: "local-headers-only").
  • modify without recompression, and without laundering — add, replace, remove, rename;
    append-only by default, --compact for true deletion; --in-place; every entry it
    re-emits is verified first.
  • Agent ergonomics--dry-run on seven commands, --summary / --fields / compact
    JSON, --strict, eight --max-* bounds plus --max-input-size, 22 schema subjects (JSON
    Schemas, an error registry and a capability manifest), doctor as a preflight,
    batch --manifest pipelines whose --json output is one document, and llms.txt +
    AGENTS.md + docs/data/errors.json in 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 archives extract must 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)extract and stream --output-dir share one sink
    (src/utils/sink.ts). Beyond the lexical safeJoin plan, the nearest existing ancestor of
    every target is realpath'd under the root before mkdir -p and 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 policycreate / modify / cat / inflate --output, extract,
    stream --output-dir and batch --task create refuse an existing file (E_IO) unless
    --overwrite; modify --in-place writes 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
    with detail { limit: "maxInputSize", configured, observed }; streaming commands stay
    constant-memory.
  • modify verifies what it re-emits — eager open, then verifyEntry() 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--codec stays 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 for create /
    modify (even under --deterministic; a warning: line says so), a deflateImpl shows as
    tier: "injected" unless --deterministic, and create --parallel refuses such a module
    because its workers cannot see it.
  • Signal cleanupSIGINT / SIGTERM remove 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.zip are 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 through sanitizeEntryPath().
  • 50 MB JSON / 1 MB config / 1 000-task caps, a mandatory inflate output 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) through createZip; --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
    (createParallelZip from zipnative/worker, loaded lazily with an explicit worker URL),
    --method, --level, --order canonical|insertion (insertion = argv order: an EPUB
    mimetype listed 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; manifest extraFields and
    commentBase64. Every entry name is pre-checked with the engine's sanitizeEntryPath().
  • modify--remove, --rename, --replace, --add, --add-dir, --comment /
    --comment-file or --from-manifest (mode, extraFields, commentBase64), applied in a
    fixed order through createZipModifier; append-only save() by default (an info: line
    documents data remanence and the 7-Zip caveat), --compact for saveCompact(),
    --in-place, --overwrite, --dry-run; envelope verified / verifySkipped / tier /
    layout.

Read & extract

  • list — text (unzip -l style) | json | ndjson, --long (with rawNameHex and
    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 --check assertions 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
    --codec method falls back to readEntry().
  • extract — the two-phase sink (plan with safeJoin containment 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.
  • streamiterateZipEntries over unseekable input: --list (default),
    --output-dir, --cat; --long, --skip-unsafe, --skip-unsupported; attribute-dependent
    flags refused; trust: "local-headers-only" in every JSON output; --summary carries
    descriptorEntries / bytesKnown.

Integrity & codecs

  • verifyverifyZip's report plus failed / skipped / strict; --entry
    (repeatable) verifies named entries through verifyEntry() and reports selected;
    encrypted entries honestly skipped; exit 1 / E_VERIFY_FAILED with zipCode for
    structural refusals; --strict also fails on any diagnostic.
  • crc32 — files or stdin in 64 KiB chunks; --seed, --expect (exit 1 /
    E_CHECK_FAILED with both CRCs in detail).
  • inflate — the resumable inflater with a mandatory --max-output bound (default:
    the effective --max-entry-size); --sync, --method deflate|store|<id> (codecs via
    --codec), --allow-trailing, --overwrite, --dry-run; bytesConsumed in the envelope.

Automation & meta

  • batch — directory mode (--task create per subdirectory through the full create
    command, --task verify per archive, --concurrency 1–64, --fail-fast, --overwrite)
    and --manifest pipelines (10 whitelisted manifest commands, "@<id>" output references,
    strict pre-validation, --continue-on-error, --allow-codec-load gate, 1 000-task and
    50 MB caps); under --json every task's output is captured into one batch document
    (tasks[i].report); --summary / --fields / --dry-run.
  • doctor — versions (package vs the engine's VERSION export), deflate tier and
    pinned tier, web streams, workers, codecs, effective limits (as numbers under --json,
    maxInputSize included), command count; exit 0/1.
  • schema — 22 subjects: the three manifests, every report and --summary shape,
    doctor, govern-verify, crc32, the status and error envelopes, errors (codes
    and the 39-entry mapping), limits, diagnostics, and the capability manifest.
  • completion — bash, zsh, fish, powershell (path flags complete files). govern
    rules, policy, verify-issue (exit 1 / E_POLICY), pinned to the .github files 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, codec refused),
    --version --json, --format, -f on 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-entries 100000, --max-entry-size 1 GiB,
    --max-total-size 8 GiB, --max-ratio 1024, --max-name-bytes 4096,
    --max-extra-bytes 65535, --max-comment-bytes 65535, --max-cd-bytes 256 MiB) over
    the engine's CWE-tagged ZipLimits, plus the CLI-owned --max-input-size (4 GiB); none
    disables a bound with a warning.
  • --pure-codecs and --codec <module> (argv only, gated in manifests, reported truthfully when a module shapes the writer).
  • 13 stable E_* classes; the 39 ZIP_* causes carried verbatim through a typed mapping
    that fails tsc when the engine adds a code; the 11 diagnostic codes bridged to stderr
    text, --json arrays or a --strict error; docs/data/core-exports.json and
    docs/data/errors.json shipped 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_CODECS honoured 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 manifest date values without a zone
    designator are read as UTC; the stored DOS fields — and the archive bytes — are identical on
    every host. now and --mtime are local and not reproducible. DOS resolution is 2 seconds
    (odd seconds floored, warning), range 1980–2107 (warning outside).
  • --order insertion means the argv order (each directory still walks name-sorted;
    manifests keep their entries order). The default canonical order is unchanged.
  • Argv .. is accepted. zipnative list ../a.zip and -o ../out.zip work; 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 --json writes one stdout document; a task that would write its
    artefact to stdout (create / modify / cat / inflate without output, 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-dir and create --stdin-name included; malformed flags stay E_USAGE (exit 2).
    Unknown commands are E_USAGE (exit 2).
  • unixMode is four octal digits ("0000", "0644", "4755").
  • stream --summary is { entries, bytes, descriptorEntries, bytesKnown, trust }:
    data-descriptor rows carry the local header's zero sizes and bytes excludes them.
  • inspect --check deterministic asserts reproducibility only; use canonical-layout
    (alias of no-data-descriptor) for the buffered layout. A create --stream archive 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.0 is the sole runtime dependency and stays external in the bundle
    (zipnative/worker resolves its own worker script).

Notes

  • Append-only modify output and 7-Zip. The default save() 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 --compact when deletion or 7-Zip interoperability matters.
  • stream trusts 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 says trust: "local-headers-only". Prefer list / extract on 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 — use cat / extract --codec on the complete file).
  • Encryption is not supported by engine policy in 1.x; encrypted entries are detected,
    listed, reported as skipped by verify, skippable with extract / stream --skip-unsupported and copied unverified by modify.
  • 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) and create --stream are
    constant-memory; create --stream --deterministic buffers per entry (the pinned encoder is
    whole-buffer).
  • Every sample ships as a .sh + .ps1 pair (41 demos, 73 jobs in samples/run-all.js) and
    runs offline; samples/agent/ walks the recommended agent loop.

Install

npm install -g zipnative-cli@1.0.0

Upgrade

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 by extract), 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 audit clean; npm run lint (src and tests) 0 errors; built binary smoke-tested
    (node dist/cli.cjs …) for every command, including create --parallel (worker script
    resolved from the bundle) and doctor reporting the node-zlib tier; 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 signatures after installing, and the SBOM with
    gh attestation verify sbom.cdx.json -R Nizoka/zipnative-cli.