Official CLI for the zipnative engine — create deterministic ZIP archives, list and inspect without extracting, extract with secure-by-default guards, verify integrity in one call, read unseekable streams, and modify archives without recompression, directly from the terminal. Zero extra runtime dependencies. Offline, always.
What's new in v1.1.0 — built on zipnative 1.1.0: all 106 engine exports bridged;
create --stream --zip64for streamed entries past 4 GiB;--name-encodingfor legacy (Shift_JIS, GBK, Windows-1252 …) names; a global--timeout(E_LIMIT) and--progress(stderr, NDJSON under--json);inspect's engine-measuredcanonicalFormwithoffendersand--check canonical-form;--longrows with extended timestamps and Unix ids; custom codecs instream. Hardened to the pkinative-cli level — and three behaviour changes shipped as security fixes:.zipnativerc.jsonis presentation only (an opt-out or a limit in it isE_USAGE), an undeclared, repeated or surplus flag isE_USAGE(a typo is no longer ignored), and--max-* noneis refused (every bound is finite).--overwritenow replaces a planted symlink instead of following it, text output escapes terminal control characters, and the release ships through a five-job attested pipeline. Migration notes: CHANGELOG, decisions: docs/adr/, release notes.⭐ Star
zipnative— the zero-dependency ZIP engine that powers this CLI.
create— build an archive from files, directories, stdin or a JSON manifest through zipnative's deterministic writer: canonical entry order, DOS-epoch timestamps and UTF-8 names by default,--deterministicto pin the pure-TS encoder (identical SHA-256 on every runtime),--streamfor a constant-memory writer (streamed inputs, data-descriptor layout),--parallelto fan deflate out across a worker pool (zipnative/worker),--include/--excludeglobs,--order insertion(argv order — an EPUBmimetypefirst),--store-ext,--preserve-mode, archive and per-entry comments (--comment-filefor raw bytes), and a--dry-runplan. An existing output file is refused without--overwrite.list/inspect— list entries without decompressing anything (text|json|ndjson,--longfor flags, offsets and extra fields), or open the archive eagerly for a forensic report — per-method statistics, a determinism verdict, every engine diagnostic — and turn it into a CI gate with repeatable--checkassertions (deterministic,canonical-form,no-encryption,no-symlinks,max-ratio=N,has=<name>, …; exit 1 /E_CHECK_FAILED).extract— secure by default. The engine sanitises every path; the CLI is the filesystem sink and re-proves containment under--output-dir— lexically and physically (realpath) — before writing a byte, and opens every file exclusively. Zip-slip, Windows device names, symlink entries, overlapping entries, central-directory / local-header disagreement, duplicate output paths and decompression bombs are refused, each with itsZIP_*code. Opt-outs are skip-not-write (--skip-unsafe,--skip-symlinks,--skip-unsupported;--allow-symlinkswrites the link target text as a plain file — a symlink is never materialised). Existing files are never overwritten without--overwrite.cat— stream one or more entries to stdout (or--output) by random access: a single file from a multi-gigabyte archive never materialises the rest.--rawemits the compressed payload zero-copy.stream— a forward-only reader for unseekable input (stdin, a pipe, an upload body): list, extract or--catentries as they arrive, with the engine's trust caveat made explicit (trust: "local-headers-only"in every JSON output — there is no central directory to cross-check).verify— one-call deep integrity verification via zipnative'sverifyZip: eager structural validation, per-entry CRC-32 / size / local-header agreement, diagnostics collected. The report is always the artefact; the exit code is the verdict (E_VERIFY_FAILED, withzipCodeset for structural refusals). Encrypted entries are honestly reported as skipped, never faked as verified.--strictalso fails on any diagnostic.modify— add, replace, remove, rename entries or set the comment without recompressing untouched entries. Every untouched entry is verified (CRC-32, sizes, local header) before it is re-emitted verbatim — a lying record is refused, never laundered. The default save is append-only (original bytes verbatim);--compactrewrites canonically so removed content is truly gone.--in-placewrites back through an exclusively created temp file + atomic rename.crc32/inflate— the ZIP checksum of files or stdin in constant memory (--expectturns it into a check,--seedcontinues a running value), and a raw-DEFLATE decoder with a mandatory output bound driven by zipnative's resumable inflater (--method <id>for codecs loaded with--codec).batch— archive every subdirectory of a folder (the fullcreatecommand per directory, bounded concurrency), verify every*.zipin a folder, or run a declarative--manifestpipeline (create→verify→extract→ …) with@<id>output references and a codec-load policy (--allow-codec-load). Under--jsonstdout is one batch document with every task's captured report inside.doctor— an offline capability preflight: CLI / Node / zipnative versions (package vs the engine'sVERSIONexport), the active deflate tier (node-zlibexpected,pureunder--pure-codecs), the pinned tier used by--deterministic, platform streaming codecs, worker-thread availability forcreate --parallel, registered codecs, the effective security limits (as numbers underdata,--max-input-sizeincluded), and the command count. Text or--format json; exit 0/1.schema— 22 versioned subjects: JSON Schemas (Draft 2020-12) for every input and output shape, theerrorsregistry plus the machine-readable capability manifest (schema manifest) andllms.txt, so agents can self-validate and discover the tool at runtime.completion—bash,zsh,fishorpowershellcompletion scripts, generated from the same command table that feeds the manifest.govern— zipnative's AI-governance / Human-in-the-Loop contract:govern rules,govern policy, andgovern verify-issue <draft.md>to gate an issue/PR draft (exit 1 /E_POLICY) before a human reviews and submits it.- Agent-native — a global
--jsonstatus/error envelope, 13 stableE_*classes, the exactZIP_*cause carried verbatim aserror.zipCode(39 frozen codes, plus 12 diagnostic codes),--dry-runon nine commands,--strict, and token-economy levers (--summary,--fields a,b.c, compact JSON under--json). See AGENTS.md. - Bounded by default — the engine's CWE-tagged
ZipLimitsare exposed as--max-entries,--max-entry-size,--max-total-size,--max-ratio,--max-name-bytes,--max-extra-bytes,--max-comment-bytesand--max-cd-bytes; the CLI adds--max-input-size(4 GiB) on every buffered read, through one descriptor. Every bound is a positive, finite number — there is no "unbounded" form. - Strict invocation — an unknown flag, a repeated switch, a single-value flag given twice or
a stray positional is
E_USAGE(exit 2): a typo is never a silent no-op. .zipnativerc.json— optional, presentation-only config file (format,fields,json,pretty,quiet,no-color,progress,strict,long,summary; global + per-command); precedence is CLI flags > config > built-in. Any other key — an input, an output, an opt-out, a bound,codec— is refused withE_USAGE, so a file planted in a cloned repository can never relax a guard.- Zero extra dependencies —
zipnativeis the sole runtime dependency; all ZIP logic lives there. No ZIP parsing in the CLI. - Offline, always — no command can open a socket. There is no network opt-in to forget.
- Stdin / stdout by default — every command is shell-pipeline friendly.
- TypeScript strict, ESM source — bundled by tsup into one CommonJS bin (
dist/cli.cjs). The package is a command-line tool only: no programmatic entry point, no type declarations. - Verifiable releases — Trusted Publishing (OIDC) with npm provenance, from a five-job pipeline where dev-dependency code never runs next to the publishing token; the published tarball is fetched back and checked against the gated one, and the tarball plus CycloneDX / SPDX SBOMs are attested (Sigstore) and attached to the GitHub Release.
| Feature | Status | Notes |
|---|---|---|
| Commands | ||
create deterministic archives |
✅ | Files, directories, stdin, --from-manifest; --method, --level, --deterministic, --order canonical|insertion, --date (UTC), --stream, --parallel, --include/--exclude, --store-ext, --preserve-mode, --dir-entries, --comment / --comment-file, --overwrite |
modify incremental edits |
✅ | --add, --add-dir, --replace, --remove, --rename, --comment / --comment-file, --from-manifest; every untouched entry verified before re-emission; append-only save() or --compact; --in-place, --overwrite |
list entries |
✅ | text | json | ndjson, --long (with rawNameHex / commentHex), --validate eager, globs, --summary / --fields |
inspect forensic report |
✅ | Eager open, stats, determinism verdict (deterministic = reproducibility, canonicalLayout = form), diagnostics, --entries / --entry / --extra, 20 --check assertions |
cat entries to stdout |
✅ | Random access, --raw (compressed payload), --no-verify-crc, --output (+ --overwrite) |
extract to a directory |
✅ | Guards on by default; --skip-unsafe, --skip-unsupported, --allow-symlinks, --skip-symlinks, --on-duplicate, --overwrite, --flat, --buffered, --preserve-mode, --preserve-mtime |
stream forward-only reader |
✅ | stdin/pipes; --list (default), --output-dir, --cat; trust: "local-headers-only"; --skip-unsafe, --skip-unsupported |
verify deep integrity |
✅ | verifyZip report + failed / skipped / strict; --entry verifies selected entries; exit 1 / E_VERIFY_FAILED |
crc32 checksum |
✅ | Files or stdin, 64 KiB chunks; --seed, --expect (exit 1 / E_CHECK_FAILED) |
inflate raw DEFLATE |
✅ | Resumable inflater, mandatory --max-output, --sync, --method <id> for registered codecs; envelope reports bytesConsumed / leftover; --overwrite |
batch orchestration |
✅ | Directory mode (--task create | verify, --concurrency 1–64, --fail-fast, --overwrite) or --manifest pipelines (10 whitelisted manifest commands); one JSON document on stdout under --json |
doctor preflight |
✅ | Versions, deflate tiers, web streams, workers, codecs, effective limits (numbers under data), command count; text or --json |
schema JSON Schema export |
✅ | 22 subjects incl. errors, limits, diagnostics, status, error and the capability manifest |
completion shell scripts |
✅ | bash / zsh / fish / powershell |
govern AI-governance / HITL |
✅ | rules / policy / verify-issue; gates drafts with E_POLICY |
.zipnativerc.json config file |
✅ | Presentation-only defaults (global + per-command); flags > config; every other key refused (E_USAGE); the file that applied is named on stderr and in the envelope (config) |
| Agent & automation | ||
Global --json envelope |
✅ | Status on success, { ok: false, command, error: { code, message, zipCode?, entryName?, detail?, remedy? } } on failure — remedy names the flag that lifts the refusal |
| Stable error classes | ✅ | 13 E_* codes — E_USAGE, E_INPUT, E_PARSE, E_IO, E_SECURITY, E_DATA, E_LIMIT, E_UNSUPPORTED, E_NOT_FOUND, E_VERIFY_FAILED, E_CHECK_FAILED, E_POLICY, E_RUNTIME |
| Exact cause | ✅ | error.zipCode = zipnative's frozen ZIP_* code, verbatim (39 codes; schema errors prints the mapping) |
| Diagnostics channel | ✅ | 12 ZIP_* diagnostic codes: text on stderr, arrays under --json, --strict escalates the first into E_CHECK_FAILED |
| Capability manifest | ✅ | schema manifest (JSON) + llms.txt — for agent tool discovery |
--dry-run validation |
✅ | create / canonicalize / merge / extract / modify / stream / cat / inflate / batch |
| Token economy | ✅ | --summary, --fields a,b.c, compact JSON under --json (--pretty opts out) on list / inspect / verify / stream / batch |
| Security defaults (engine guards, CLI switch) | ||
| Zip-slip traversal, absolute paths, drive/UNC, NUL, NTFS ADS, Windows device names | ✅ | CWE-22 / CWE-67 — ZIP_PATH_TRAVERSAL; refused by default, extract --skip-unsafe skips (nothing unsafe is ever written) |
| Decompression bombs (per-entry, total, ratio, entry flood) | ✅ | CWE-400 / CWE-409 — ZIP_LIMIT_EXCEEDED; --max-entry-size, --max-total-size, --max-ratio, --max-entries |
| Symlink entries | ✅ | CWE-59 — ZIP_SYMLINK_REJECTED; extract --allow-symlinks (target text as a regular file) or --skip-symlinks |
| Overlapping entries | ✅ | CWE-405 — ZIP_ENTRY_OVERLAP; no opt-out |
| Central-directory / local-header disagreement | ✅ | CWE-436 — ZIP_CD_LFH_MISMATCH; no opt-out (name divergence is the ZIP_NAME_MISMATCH diagnostic) |
| Zip64 sentinel spoofing | ✅ | CWE-1288 — ZIP_ZIP64_CONTRADICTION; no opt-out |
| Duplicate output paths | ✅ | CWE-694 — ZIP_EXTRACT_DUPLICATE_PATH; --on-duplicate error|first|last |
| Ambiguous EOCD / trailing garbage | ✅ | ZIP_EOCD_NOT_FOUND — refused, never guessed |
| Integer overflow (> 2^53) | ✅ | CWE-190 — ZIP_VALUE_UNREPRESENTABLE |
| Sink containment (CLI) | ✅ | safeJoin(root, path) re-proves every destination stays under --output-dir lexically, then the nearest existing ancestor is realpath-checked under the root's realpath before any mkdir (a planted symlink / junction is E_SECURITY); files are opened exclusively (wx); case-fold collisions refused on win32/darwin |
| Existing files | ✅ | Never overwritten without --overwrite (E_IO) — uniform on create / modify / cat / inflate --output, extract, stream --output-dir and batch |
| Buffered input bound (CLI) | ✅ | CWE-400 — --max-input-size (4 GiB) caps every archive or payload read into memory; E_LIMIT with detail.limit = "maxInputSize" |
| Determinism | ||
| Canonical entry order, DOS-epoch timestamps, UTF-8 names | ✅ | The engine's defaults — structurally reproducible everywhere |
| Cross-runtime byte identity | ✅ | --deterministic pins the pure-TS encoder (tier: "pure-pinned"); default tier is byte-stable per environment |
| Parallel identity | ✅ | create --parallel is byte-identical to the sequential writer (per tier; unconditional with --deterministic); create --stream yields the same content in the data-descriptor layout (not the same bytes) |
| Determinism verdict | ✅ | inspect reports determinism.{epochTimestamps, canonicalOrder, utf8Flags, noDataDescriptors, canonicalLayout, deterministic} — deterministic is reproducibility (epoch + canonical order + UTF-8 flags), canonicalLayout is the buffered layout (a --stream archive is reproducible but not canonical); --check deterministic / --check canonical-layout gate them |
| Not supported | ||
| Encryption (read or write) | ❌ | Policy of the engine in 1.x (ZipCrypto is broken); encrypted entries are detected, listed and refused with ZIP_UNSUPPORTED_ENCRYPTION |
| Other archive formats / exotic codecs | ❌ | No 7z, RAR, tar, gzip; the codec registry (--codec) is the extension point — a registered method is readable everywhere and, for methods 0/8, also drives the writer |
| Multi-disk / spanned archives | ❌ | Detected and refused (ZIP_UNSUPPORTED_MULTI_DISK) |
| Archive repair / salvage | ❌ | Structural problems are reported (verify), never guessed at |
| Streamed entries > 4 GiB | ✅ | create --stream --zip64 / --stdin-name --zip64 (zipnative 1.1): every streamed entry gets a Zip64 local header; without --zip64 such an entry is refused (ZIP_UNSUPPORTED_ZIP64_STREAMING); buffered entries are fully Zip64 |
| Network access | ❌ | None, in any mode |
Note: everything listed works today. Planned work is tracked in ROADMAP.md.
ZIP has no veraPDF, so the CLI ships its own gate — veraZIP: an ISO/IEC 21320-1:2015
(Document Container File) validator vendored from the engine
(zipnative/scripts/validate-zip.ts, commit 1e2ab42, zipnative 1.1.0) that raw-parses the bytes with its own
EOCD / central-directory / local-header reader and never imports zipnative — a validator that
shared the engine's parser would attest the engine with the engine. npm run validate:zip builds
the CLI, drives the built binary to write a 41-archive corpus (37 conformant archives —
34 across every writer path: buffered, --stream, --stream --zip64, --parallel, canonicalize, merge,
--deterministic, a clamped pre-1980 --date, --order insertion, manifest extra fields, binary
comments, modify append-only and --compact — plus
4 raw-crafted negative canaries the validator must reject with a declared clause id), then
validates every file clause by clause. Three of the conformant archives are
hostile-but-conformant (zip-slip, a Windows device name, duplicate paths): the ISO profile
constrains the container, not the meaning of names, so they PASS the validator and extract
must refuse them — the gate checks both (37 PASS, 4 XFAIL, 0 FAIL). Level 0 (the ISO clauses)
needs no external tool and always runs; level 1 re-tests every conformant archive with the
foreign integrity tools present on the machine (unzip -t, 7z t, python -m zipfile -t,
tar -tf, jar tf) and skips the absent ones visibly; VERAZIP_REQUIRED=1 (set in CI)
fails closed. Blocking in verazip.yml on Linux and Windows on
every pull request (no path filter) and again before every publish. See CONTRIBUTING.md.
Not a certification — validation evidence against a specific validator revision, and
conformant does not mean safe.
npm install --global zipnative-cliOr run without installing:
npx zipnative-cli create src/ --output src.zipRequirements: Node.js ^22.17.1 or >=24.4.1 — the patch floor of the Windows path-normalisation fixes CVE-2025-23084 and CVE-2025-27210 (device names in path.join / normalize), which the sink's containment relies on (CI runs 22 and 24 on Ubuntu and Windows, 22 on macOS). The
package ships one CommonJS bin (dist/cli.cjs) plus README.md, AGENTS.md, llms.txt and
docs/data/errors.json — it is a command-line tool, not a library.
- 📘 Quick Start (below) — Create, inspect and extract in 5 minutes
- 🏛️ KNOWLEDGE_BASE.md — Full CLI reference, architecture, the 106-export API mapping, integration patterns
- 🤖 AGENTS.md — The agent contract: envelopes, codes, token economy, the recommended loop
- 📚 samples/README.md — runnable
.sh+.ps1samples per command - 🔧 zipnative engine — the underlying ZIP engine docs
- ❓ FAQ — Common questions & troubleshooting
# A directory (entry names relative to its parent: src/a.ts, src/b/c.ts)
zipnative create src/ --output src.zip
# Several inputs, a prefix, globs, explicit directory entries
zipnative create src/ docs/ --prefix release/ --exclude '*.map' --dir-entries -o release.zip
# From stdin, as one named entry
tar -cf - build/ | zipnative create --stdin-name build.tar -o build.zip
# From a JSON manifest (schema: `zipnative schema create-manifest`)
zipnative create --from-manifest entries.json -o out.zip
# Constant memory (streamed inputs, data-descriptor layout) / worker-parallel deflate
zipnative create data/ --stream -o data.zip
zipnative create data/ --parallel --workers 4 -o data.zip
# Plan only: walk inputs, validate names, write nothing
zipnative create src/ -o src.zip --dry-run# Pin the pure-TS encoder: identical SHA-256 on every runtime, every day
zipnative create dist/ --deterministic -o a.zip
zipnative create dist/ --deterministic -o b.zip
sha256sum a.zip b.zip # identical
# Prove it from the archive itself (exit 1 / E_CHECK_FAILED otherwise)
zipnative inspect --input a.zip --check deterministic,canonical-layoutWithout --deterministic the bytes are stable per environment (same Node + zlib build) but may
differ across zlib builds; timestamps default to the DOS epoch and entries are sorted by raw name
bytes either way.
Dates. --date <ISO 8601> (and a manifest date) is UTC wall-clock: a string without a
zone designator is read as UTC, a date-only string gets T00:00:00Z, and the stored DOS fields
are identical on every host whatever its TZ. DOS time has a 2-second resolution (odd seconds
are floored, with a warning) and a 1980–2107 range (a warning outside it). --date now and
--mtime use local time and are not reproducible. inspect separates the two verdicts:
determinism.deterministic (epoch timestamps + canonical order + UTF-8 flags — run-to-run
reproducibility) and determinism.canonicalLayout (no data descriptors — the buffered layout).
A create --stream archive is reproducible but not canonical.
# unzip -l style table
zipnative list a.zip
zipnative list a.zip --long # mode, flags, offsets, extra fields
# JSON / NDJSON for pipelines and agents
zipnative list --input a.zip --format json --summary
zipnative list --input a.zip --format ndjson | jq -r 'select(.isEncrypted) | .name'
# Forensic report (eager open: every local header cross-checked) + CI assertions
zipnative inspect --input a.zip
zipnative inspect --input a.zip --entries --extra --format json
zipnative inspect --input a.zip --check no-encryption,no-symlinks,max-ratio=100,has=manifest.jsonExample inspect --format json --summary output:
{ "entries": 12, "bytes": 48213, "uncompressedSize": 131072, "zip64": false, "encrypted": 0, "deterministic": true, "canonicalLayout": true, "diagnostics": 0, "checksPassed": true }# Guards on by default: zip-slip, device names, symlinks, duplicates, bombs, overlaps are refused
zipnative extract --input a.zip --output-dir out/
# Plan first (nothing written), then extract only what you need
zipnative extract --input a.zip -d out/ --dry-run
zipnative extract --input a.zip -d out/ --include 'docs/**' --exclude '*.png'
# Hostile archive: skip the unsafe names instead of failing (nothing unsafe is ever written);
# --skip-unsupported also skips encrypted / unknown-method entries
zipnative extract --input untrusted.zip -d out/ --skip-unsafe --skip-symlinks --skip-unsupported
# Tighter bounds for untrusted input
zipnative extract --input upload.zip -d out/ --max-total-size 512m --max-ratio 50 --max-entries 5000# One entry to stdout, CRC verified at the end of the stream (like `unzip -p`)
zipnative cat a.zip README.md
zipnative cat --input a.zip --entry docs/a.md --entry docs/b.md --output merged.md
# Unseekable input: list as entries arrive, or extract / cat from the pipe
curl -sL https://example.com/pkg.zip | zipnative stream --list
curl -sL https://example.com/pkg.zip | zipnative stream --output-dir out/ --skip-unsafe
cat a.zip | zipnative stream --cat manifest.jsonstream parses local headers alone — every JSON output carries trust: "local-headers-only".
A refusal met mid-stream (a duplicate name, a hostile name, an unsupported entry without
--skip-*) ends the run with the entries written before it left on disk — a forward reader
cannot check the archive first, whereas extract validates the whole archive before writing
anything. Prefer list / extract whenever the whole file is available.
zipnative verify --input a.zip # text verdict, exit 0/1
zipnative verify --input a.zip --strict # also fail on any diagnostic
zipnative verify --input a.zip --json --summary # {"ok":true,"entries":12,"failed":0,"skipped":0,"diagnostics":0}
zipnative verify --input a.zip -e docs/a.md # only the named entries (after the structural check)# Append-only save: untouched entries are verified (CRC, sizes, local header) and copied
# verbatim — never recompressed
zipnative modify --input a.zip --output b.zip --replace docs/index.md=new.md --add CHANGELOG.md
# Removed / replaced content stays recoverable after an append-only save (and 7-Zip's CLI
# mis-reads that layout) — pass --compact for a canonical rewrite, still no recompression
zipnative modify --input a.zip -o b.zip --remove secrets.txt --compact
# In place (temp file + rename), from a JSON edit list
zipnative modify --input a.zip --in-place --from-manifest edits.jsonzipnative crc32 file.bin # "<crc> <bytes> <file>"
zipnative crc32 file.bin --expect 1a2b3c4d # exit 1 / E_CHECK_FAILED on mismatch
zipnative cat a.zip big.bin | zipnative crc32 # cross-check an entry against `list`
zipnative inflate --input payload.deflate --output payload.bin --max-output 64m
zipnative cat a.zip big.bin --raw | zipnative inflate > big.bin # deflate entry only: a stored entry's --raw payload is already the plain bytes# Every subdirectory of projects/ → archives/<name>.zip (all create flags honoured)
zipnative batch --input-dir projects/ --output-dir archives/ --deterministic --concurrency 8
# Verify every *.zip in a folder
zipnative batch --input-dir archives/ --task verify --json --summary
# Declarative pipeline with @id references (schema: `zipnative schema batch-manifest`)
zipnative batch --manifest tasks.json --dry-run
zipnative batch --manifest tasks.json --json # ONE JSON document on stdout, task reports insideAgents act as draftsmen: they may draft an issue/PR locally, but a human must review and submit it. Nothing here touches the network.
zipnative govern rules
zipnative govern policy --pretty
zipnative govern verify-issue ./draft.md # exit 1 / E_POLICY on a violationReady-to-run examples are in samples/, one directory per command — 48 demos,
each shipped as a Bash (.sh) and a PowerShell (.ps1) pair, plus a dependency-free runner
that replays them as 96 jobs:
| Category | Description |
|---|---|
create/ |
Directory, store vs deflate, deterministic + SHA-256 twice, manifest, stdin --stream, --parallel, comments + --order insertion + --date |
modify/ |
Replace / add / remove / rename, append-only vs --compact, --in-place, edits manifest |
list/ |
Text, --long, JSON, NDJSON, --summary / --fields |
inspect/ |
Forensic report, --entries --extra, --check gates |
cat/ |
Single / multiple entries, --raw, --output |
extract/ |
Safe defaults, --dry-run, globs, --skip-unsafe, tightened --max-* bounds |
stream/ |
Pipe listing, pipe extraction, --cat, the trust caveat |
verify/ |
Verdicts, --strict, --json --summary |
crc32/ |
Files, stdin, --expect, --seed |
inflate/ |
Raw DEFLATE from cat --raw, --max-output, --sync |
batch/ |
Directory mode (create / verify) and a --manifest pipeline |
doctor/ |
Environment preflight, text and JSON |
schema/ |
Subjects, the capability manifest |
completion/ |
Install scripts for the four shells |
config/ |
.zipnativerc.json defaults, --config, --no-config, flag precedence |
govern/ |
Rules, policy, verify-issue on a passing and a failing draft |
agent/ |
--json envelopes, --dry-run, deterministic error codes, token economy (--summary / --fields) |
Run all samples at once:
node samples/run-all.jsSee samples/README.md for descriptions and integration patterns (GitHub Actions, Docker, TypeScript).
The 17 commands are grouped by purpose (the global zipnative --help shows the same grouping):
| Group | Commands |
|---|---|
| Create & modify | create, modify, canonicalize, merge |
| Read & extract | list, inspect, cat, extract, stream |
| Integrity & codecs | verify, crc32, inflate |
| Automation & meta | batch, doctor, schema, completion, govern |
zipnative create [<path>...] --output <out.zip> [options]
zipnative create --from-manifest <entries.json> -o <out.zip>
cat file | zipnative create --stdin-name <name> -o <out.zip>| Flag | Default | Description |
|---|---|---|
<path>... |
— | Files and directories (directories are walked recursively, sorted by name) |
--input <path>, -i (repeatable) |
— | Same as a positional (useful in manifests) |
--stdin-name <name> |
— | Read stdin as one entry named <name> |
--from-manifest <file> |
— | JSON manifest { comment|commentBase64, order, date, compression, entries: [{ name, path|data|dataBase64|directory, raw, method, level, date, comment, mode, extraFields: [{ id, hex|base64 }] }] } — see zipnative schema create-manifest; raw: { method, crc32, uncompressedSize } makes path / dataBase64 a PRE-COMPRESSED payload, written as-is after the CLI decodes it once and checks size and CRC-32 (trailing bytes after a DEFLATE stream are refused); entries are emitted in array order under order: "insertion"; mutually exclusive with paths / --stdin-name |
--output <file>, -o |
stdout | Output path |
--overwrite |
false | Replace an existing output file (default: refuse, E_IO, the file is left intact) |
--base <dir> |
each input's parent directory | Entry names are relative to <dir> |
--prefix <dir/> |
— | Prepend <dir/> to every entry name |
--dir-entries |
false | Emit explicit directory entries (keeps empty directories) |
--include <glob> (repeatable) |
— | Keep only matching names (*, **, ?, [abc]; a pattern without / matches at any depth) |
--exclude <glob> (repeatable) |
— | Drop matching names |
--follow-symlinks |
false | Dereference symlinks (default: skipped with a warning; symlink entries are never written) |
--method store|deflate |
deflate |
Compression method |
--level 0-9 |
6 |
Deflate level |
--deterministic |
false | Pin the pure-TS encoder: identical SHA-256 on every runtime |
--order canonical|insertion |
canonical |
Entry order: canonical sorts by raw-name bytes; insertion keeps the argv order (each directory still walks name-sorted; a manifest keeps its entries order) — e.g. zipnative create book/mimetype book/META-INF book/OEBPS --base book --order insertion -o book.epub puts an EPUB mimetype first (--base rebases the names; inputs are still looked up where they are) |
--date epoch|now|<ISO 8601> |
epoch |
Timestamp for entries. epoch = DOS epoch 1980-01-01 (reproducible); an ISO date is UTC wall-clock (no zone designator → UTC; 2-second resolution, 1980–2107); now is local time and non-reproducible (ZIP_TIMESTAMP_NOT_PINNED) |
--mtime |
false | Use each file's modification time (local, non-reproducible) |
--comment <text> |
— | Archive comment |
--comment-file <path> |
— | Archive comment from a file, raw bytes (- = stdin; exclusive with --comment; at most 65535 bytes, E_INPUT beyond) |
--entry-comment <name>=<text> (repeatable) |
— | Per-entry comment |
--preserve-mode |
false | Store POSIX mode bits (never setuid/setgid/sticky; no effect on Windows) |
--store-ext png,jpg,zip |
— | Store (no deflate) entries with these extensions |
--stream |
false | Constant-memory writer: file inputs are streamed (data-descriptor layout — same content as the buffered layout, not the same bytes); entries > 4 GiB are refused (ZIP_UNSUPPORTED_ZIP64_STREAMING) unless --zip64 |
--zip64 |
false | With --stream / --stdin-name: write every streamed entry with a Zip64 local header and the 24-byte data descriptor, so it may exceed 4 GiB (zipnative 1.1 AddEntryOptions.zip64; refused without a streamed input — buffered entries are already Zip64 when they need it) |
--chunk-size <size> |
65536 |
Output chunk size for the chunked writer (--stream or --stdin-name; refused otherwise; a warning outside 1 KiB–16 MiB) |
--parallel |
false | Deflate across a worker pool (zipnative/worker); byte-identical to the sequential writer per tier. Refused (exit 2) with a --codec module that registers method 0/8, or a deflateImpl without --deterministic — the workers never see the module |
--workers <n> |
min(cores − 1, 8) | Worker count, 0–64 (0 = main thread); requires --parallel |
--min-job-size <size> |
32k |
Minimum entry size dispatched to a worker; requires --parallel |
--job-timeout <ms> |
60000 |
Per-job cap before inline fallback; requires --parallel |
--dry-run |
false | Walk inputs, validate names, print the plan; write nothing |
--parallel resolves node:zlib inside its worker bundle, so --pure-codecs cannot govern it —
combining the two requires --deterministic (exit 2 otherwise). Every entry name is pre-checked
with the engine's sanitizeEntryPath(): a name that could not be extracted safely (reserved
device name, traversal, empty segment) is refused at creation time (E_INPUT, with
entryName). Any streamed input (--stream, --stdin-name) uses the data-descriptor layout:
same content as the buffered writer, different bytes; --stream --deterministic buffers one
entry at a time (the pinned encoder is whole-buffer). Argv paths (../src, -o ../out.zip)
are ordinary shell paths; only path values inside a manifest are refused on ...
Status envelope (--json): { ok, command, output, entries, files, directories, bytes, bytesIn, method, level, deterministic, tier, order, stream, zip64, layout: "buffered" | "data-descriptor", parallel, skipped, diagnostics }. A date outside 1980–2107 (an ISO --date, a manifest date,
or --mtime on an older file) is clamped by the engine and reported as the
ZIP_TIMESTAMP_CLAMPED diagnostic (an error under --strict).
zipnative modify --input <a.zip> --output <b.zip> [edits] [--compact]
zipnative modify --input <a.zip> --in-place [edits]
zipnative modify --input <a.zip> -o <b.zip> --from-manifest <edits.json>Edits are applied in a fixed order regardless of argv order: remove → rename → replace
→ add / add-dir → comment.
| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
— (required) | Source archive (a positional path also works). Opened eagerly: overlap and CD ↔ local-header structure are checked before any edit |
--output <file>, -o |
stdout | Output path |
--overwrite |
false | Replace an existing --output file (default: refuse, E_IO) |
--in-place |
false | Write back to the input path through an exclusively created temp file (<input>.tmp-<pid>-<12 hex>) + atomic rename; mutually exclusive with --output, requires a file input |
--remove <name> (repeatable) |
— | Remove an entry |
--rename <from>=<to> (repeatable) |
— | Rename an entry (never overwrites implicitly) |
--replace <name>=<path> (repeatable) |
— | Replace an entry's content (path - = stdin) |
--add <name>=<path> (repeatable) |
— | Add a new entry (a bare <path> uses its basename). A name ending in / with a payload is E_INPUT — use --add-dir |
--add-dir <name> (repeatable) |
— | Add an explicit directory entry |
--comment <text> |
— | Set the archive comment ("" clears it) |
--comment-file <path> |
— | Set the archive comment from a file, raw bytes (- = stdin; exclusive with --comment; at most 65535 bytes) |
--from-manifest <file> |
— | JSON edits { comment|commentBase64, edits: [{ op, name, to, path|data|dataBase64, method, level, date, comment, mode, extraFields }] } — see zipnative schema modify-manifest; mutually exclusive with the edit flags |
--method / --level / --deterministic |
engine defaults | Compression for new payloads |
--date epoch|now|<ISO> |
epoch |
Timestamp for new payloads (an ISO date is UTC wall-clock, as in create) |
--compact |
false | Canonical rewrite (saveCompact): removed data is truly gone, still no recompression |
--canonical |
false | With --compact: write zipnative's canonical deterministic form (saveCompact({ canonical }) — epoch or --date timestamps, names sorted and UTF-8, extras and comments dropped), as canonicalize does; refused without --compact |
--dry-run |
false | Validate edits against the archive (including the verification pass below); write nothing |
The default save is append-only: original bytes verbatim + appended entries + a new central
directory. Removed / replaced content remains recoverable (data remanence), and 7-Zip's CLI is
known to mis-read this layout — pass --compact when either matters (the CLI prints one info:
line whenever a destructive edit is saved append-only). Archives with duplicate entry names cannot
be modified incrementally (ZIP_DUPLICATE_ENTRY_NAME).
Every re-emitted entry is verified. Before save() / saveCompact(), each entry that is
copied verbatim (not removed or replaced; renamed entries are checked under their original
record) goes through verifyEntry(): CRC-32, sizes and local header vs central directory — one
decompress pass over the untouched entries, never a recompress. A lying record is refused
instead of laundered: local-header disagreement → E_SECURITY / ZIP_CD_LFH_MISMATCH, a CRC
lie → E_DATA / ZIP_CRC_MISMATCH, a size lie → E_DATA / ZIP_SIZE_MISMATCH, each with
entryName. Encrypted entries and entries whose registered codec has no decompressSync cannot
have their content verified: their local header is still cross-checked (zipnative 1.1) — a
mismatch is E_SECURITY / ZIP_CD_LFH_MISMATCH — and they are counted in verifySkipped. An
entry with no registered codec is E_UNSUPPORTED / ZIP_UNSUPPORTED_METHOD (load --codec).
There is no opt-out.
Status envelope: { ok, command, output, bytes, edits: [{ op, name, to? }], layout: "append-only" | "compact", canonical?, changed, verified, verifySkipped, tier, diagnostics } (a
binary comment shows as "<N bytes>" in edits).
zipnative canonicalize <in.zip> -o <out.zip> [--date epoch|<ISO>] [--keep-comments] [--normalize-modes]Rewrite an archive from any producer into zipnative's canonical deterministic form
(zipnative 1.1 canonicalizeZip) — the "fix" half of inspect --check canonical-form. Timestamps
are pinned, entries sorted by raw name bytes, names re-encoded as UTF-8 (flag bit 11), extra fields
and comments dropped, version-made-by and internal attributes constant. No recompression:
every payload is copied bit for bit, so an encrypted or corrupt entry travels unchanged (run
verify first when integrity matters). Running it twice yields the same bytes.
| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Archive path (or the positional) |
--output <file>, -o |
— (required) | Output path; - = stdout |
--date epoch|<ISO> |
epoch |
Timestamp of every entry (an ISO date is UTC wall-clock, as in create); now is refused — it is not a canonical form |
--keep-comments |
false | Keep the archive and entry comments |
--normalize-modes |
false | Write the canonical modes (0644 files, 0755 directories) instead of keeping each entry's external attributes |
--overwrite |
false | Replace an existing --output file (default: refuse, E_IO) |
--dry-run |
false | Canonicalise and report; write nothing |
Status envelope: { ok, command: "canonicalize", output, entries, bytes, changed, deterministic, canonicalForm, unsafeNames, encrypted, dryRun, diagnostics } — deterministic and
canonicalForm are measured on the output by analyzeDeterminism() (the same two verdicts
inspect reports). Names keep their meaning: a traversal or device name travels into the output
(counted in unsafeNames, with a warning: line) — reproducible is not safe to extract;
gate with inspect --check safe-names or let extract refuse it.
zipnative merge <a.zip> <b.zip>... -o <all.zip> [--on-duplicate error|first|last]
zipnative merge <a.zip> --include 'docs/**' -o <docs.zip> # split: a filtered copyCombine archives — or split one with filters — without recompression (zipnative 1.1
addFromReader): each entry's compressed bytes travel with its method, CRC, sizes, timestamp,
attributes, comment and extra fields (Zip64 recomputed). Every source is opened eagerly and
bounded by --max-input-size, and every entry is verified (CRC-32 and size, one decompress
pass) before it is copied — a lying source is refused (E_DATA), never laundered, with no
opt-out. An entry that cannot be verified — encrypted, or a method no registered codec decodes —
is refused (E_UNSUPPORTED) unless --skip-unsupported leaves it out.
| Flag | Default | Description |
|---|---|---|
--input <file>, -i (repeatable) |
— | A source archive (positionals too; - = stdin, once) |
--output <file>, -o |
— (required) | Output path; - = stdout |
--include <glob> / --exclude <glob> (repeatable) |
— | Keep / drop matching names |
--on-duplicate error|first|last |
error |
A name present in two sources: refuse (E_INPUT with entryName), keep the first, or keep the last; the same directory twice is one directory |
--order canonical|insertion |
canonical |
Entry order (insertion = sources in argv order, entries in directory order) |
--comment <text> / --comment-file <path> |
— | Archive comment (raw bytes from a file) |
--skip-unsupported |
false | Leave out entries that cannot be verified instead of refusing |
--overwrite |
false | Replace an existing --output file (default: refuse, E_IO) |
--dry-run |
false | Plan and verify every entry; write nothing |
Status envelope: { ok, command: "merge", output, inputs, entries, files, directories, order, layout, skipped: [{ name, input, reason: "filtered" | "duplicate" | "unsupported" }], bytes, dryRun, diagnostics }.
zipnative list --input <a.zip> [options]
zipnative list <a.zip> [options]| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Archive path |
--format text|json|ndjson, -f |
text (json under --json) |
Output format; ndjson is one EntryRow per line |
--long |
false | Add mode, flags, versions, offsets and extra fields (no -l short form) |
--validate lazy|eager |
lazy |
Cross-check every local header up front (eager) |
--range |
false | Read the archive by byte ranges through one file descriptor (zipnative 1.1 openZipRange): only the central directory is fetched, so an archive larger than memory is listed; --max-input-size does not apply. A regular file only — stdin, a pipe or a device has no random access (E_USAGE) |
--include <glob> / --exclude <glob> (repeatable) |
— | Name filters |
--name-encoding <label> |
cp437 |
Decode names without the UTF-8 flag with this encoding: cp437 (the APPNOTE default), or any WHATWG label the Node runtime's TextDecoder knows (shift_jis, gbk, windows-1252, ibm866, …; an unknown label is exit 2). Decoded rows report nameEncoding: "custom"; the engine still sanitises the decoded name and --long keeps the bytes in rawNameHex |
--summary |
— | { entries, files, directories, compressedSize, uncompressedSize, zip64, zip64Entries, encrypted } — zip64 is the Zip64 EOCD, zip64Entries the entries with a Zip64 extra field (a streamed --zip64 archive has entries and no EOCD record) |
--fields a,b.c |
— | Dot-path projection of the JSON report |
Nothing is decompressed. JSON shape: zipnative schema entries. --long rows carry
rawNameHex (the name bytes, always) and commentHex (when the entry has a comment);
unixMode is four octal digits ("0644", "4755"), plus (zipnative 1.1) timestamps: { mtime, atime, ctime, source: "ut" | "ntfs" | "dos" } — the best timestamps the extra fields offer —
and unixIds: { uid, gid } | null (the Info-ZIP 0x7875 extra). The --format json archive object
carries commentHex whenever commentBytes > 0 (comment stays the lossy UTF-8 decode).
zipnative inspect --input <a.zip> [--format json|text] [--check <assert>]...| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Archive path. Opened eagerly: every local header cross-checked, overlap table built up front |
--format text|json, -f |
text (json under --json) |
Output format |
--entries |
false | Include every entry (long form, with rawNameHex / commentHex) in the report |
--entry <name> (repeatable) |
— | Include only the named entries (E_NOT_FOUND / ZIP_ENTRY_NOT_FOUND if absent) |
--extra |
false | Include extra-field payloads as hex |
--name-encoding <label> |
cp437 |
As in list |
--check <assert> (repeatable, comma-separable) |
— | Assertion; any failure prints the report then exits 1 with E_CHECK_FAILED |
--summary |
— | { entries, bytes, uncompressedSize, zip64, zip64Entries, encrypted, deterministic, canonicalLayout, canonicalForm, unsafeNames, symlinks, duplicateNames, diagnostics, checksPassed? } — zip64 is the Zip64 EOCD, zip64Entries the entries with a Zip64 extra field (--check zip64 passes on either) |
--fields a,b.c |
— | Dot-path projection |
Assertions: deterministic, canonical-form, epoch-timestamps, canonical-order, utf8-names,
no-data-descriptor / canonical-layout, no-zip64, zip64, no-encryption, no-symlinks, safe-names
(every name passes the engine's sanitizeEntryPath() — the pre-extraction gate verify does not give), no-duplicates,
no-diagnostics, store-only, deflate-only, max-entries=N, min-entries=N,
max-uncompressed=<size>, max-ratio=N, has=<name>, method=store|deflate|<id>.
The determinism block is measured by the engine's analyzeDeterminism() (zipnative 1.1): { epochTimestamps, canonicalOrder, utf8Flags, noDataDescriptors, canonicalLayout, deterministic, canonicalExtras, canonicalVersionMadeBy, canonicalForm, offenders }. deterministic
(reproducibility, unchanged since 1.0) = epoch timestamps + canonical order + UTF-8 flags;
canonicalLayout (form) = no data descriptors; canonicalForm is the engine's stricter verdict —
also Zip64-only extra fields and the constant version-made-by 0x032D, the form zipnative itself
writes — gated by --check canonical-form; offenders lists every { name, concern } (timestamp | order | utf8-flag | extra-field | version-made-by | data-descriptor, the last informational). The
text report prints Determinism: reproducible, layout canonical or … layout data-descriptor (streamed). JSON shape: zipnative schema inspect.
zipnative cat --input <a.zip> --entry <name> [--entry <name>]... [-o <file>]
zipnative cat <a.zip> <name> [<name>...]| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
— (required) | Archive path |
--entry <name>, -e (repeatable) |
— (required) | Entry name; entries are concatenated in order |
--output <file>, -o |
stdout | Write to a file instead of stdout |
--overwrite |
false | Replace an existing --output file (default: refuse, E_IO) |
--raw |
false | Output the compressed payload (zero-copy), no decoding |
--no-verify-crc |
false | Skip the CRC-32 check at the end of the stream |
--range |
false | Read the archive by byte ranges through one file descriptor (zipnative 1.1 openZipRange): only the central directory and the requested entries are fetched, in chunks (--raw fetches the whole compressed entry, bounded by --max-entry-compressed-size); --max-input-size does not apply. A regular file only — stdin, a pipe or a device has no random access (E_USAGE) |
--name-encoding <label> |
cp437 |
Decode non-UTF-8 names (as in list); --entry then matches the decoded name |
--dry-run |
false | Resolve the entries and report their sizes; output nothing |
The CRC is verified at the end of the stream (like unzip -p), so stdout may already carry
bytes when E_DATA fires; with --output the partial file is removed. Directory entries are
refused (E_INPUT); an unknown name is E_NOT_FOUND with zipCode: "ZIP_ENTRY_NOT_FOUND". A
--codec method that offers decompressSync but no decompressStream is read through
readEntry() (that one entry is buffered).
zipnative extract --input <a.zip> --output-dir <dir> [options]| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Archive path |
--output-dir <dir>, -d |
— (required) | Destination directory (created if missing). Every path is re-checked with sanitizeEntryPath() and contained under this root |
--include <glob> / --exclude <glob> (repeatable) |
— | Name filters |
--entry <name> (repeatable) |
— | Extract only the named entries |
--overwrite |
false | Replace existing files (default: refuse, E_IO) |
--on-duplicate error|first|last |
error |
Same sanitised path twice |
--skip-unsafe |
false | Skip entries whose names cannot be made safe instead of failing (zip-slip, absolute, drive/UNC, NUL, ADS, device names). Nothing unsafe is ever written |
--skip-unsupported |
false | Skip encrypted entries and methods with no registered codec (reason unsupported) instead of failing |
--allow-symlinks |
false | Write a symlink entry's target text as a regular file (a symlink is never materialised). Default: refuse |
--skip-symlinks |
false | Drop symlink entries silently |
--flat |
false | Drop directories, write basenames only |
--buffered |
false | Use the in-memory extractor (many tiny entries) |
--preserve-mode |
false | Apply POSIX mode bits (never setuid/setgid/sticky; no effect on Windows) |
--preserve-mtime |
false | Apply the entry timestamp to each file |
--name-encoding <label> |
cp437 |
Decode non-UTF-8 names (as in list) — before the path is sanitised and contained, so a decoded name passes the same guards |
--dry-run |
false | Plan and validate; write nothing |
Refusals (E_SECURITY + zipCode): ZIP_PATH_TRAVERSAL, ZIP_SYMLINK_REJECTED,
ZIP_EXTRACT_DUPLICATE_PATH, ZIP_ENTRY_OVERLAP, ZIP_CD_LFH_MISMATCH. Bounds (E_LIMIT):
--max-entry-size, --max-total-size, --max-ratio, --max-entries, … Extraction is
two-phase: the plan is drained without decompressing anything and every destination is proven
to stay under the root lexically (safeJoin); then, per file, the nearest existing ancestor
of the target directory is realpath-checked under the root's realpath before mkdir -p
and re-checked after (a symlink or junction planted inside the destination that points outside
is E_SECURITY, and nothing is created beyond the link), the file is opened exclusively
(wx, unless --overwrite — a file that appears between the plan and the write is refused like
any pre-existing one), and the entry is streamed in with backpressure (a CRC / size failure
removes the partial file). The only residual window is between realpath and open: use an
empty or trusted destination. Skipped reasons: unsafe-path | symlink | filtered | duplicate | unsupported.
Status envelope: { ok, command, outputDir, entries, files, directories, bytes, skipped: [{ name, reason }], symlinksAsData, diagnostics }.
curl ... | zipnative stream [--list] [--format ndjson]
curl ... | zipnative stream --output-dir <dir>
cat a.zip | zipnative stream --cat <name>| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | File to read sequentially |
--list |
default mode | List entries as they arrive |
--output-dir <dir>, -d |
— | Extract under <dir> (sanitizeEntryPath + containment) |
--cat <name> (repeatable) |
— | Write the named entry's data to stdout; mutually exclusive with --output-dir |
--format text|json|ndjson, -f |
text (ndjson under --json; json when --summary / --fields is given) |
Listing format |
--long |
false | Add flags, versions and extra fields to the rows (rawNameHex included; entry comments live only in the central directory, so there is no commentHex here; no -l short form) |
--include / --exclude, --overwrite, --on-duplicate, --flat, --preserve-mtime |
as in extract |
Extraction controls (the same sink: realpath containment, exclusive open) |
--skip-unsafe |
false | Skip unsafe names instead of failing |
--skip-unsupported |
false | Skip encrypted / unknown-method entries, and --codec methods without createDecompressor, instead of failing |
--name-encoding <label> |
cp437 |
Decode non-UTF-8 names (as in list) |
--summary / --fields |
— | Projection of the --format json report; --summary = { entries, bytes, descriptorEntries, bytesKnown, trust } |
--dry-run |
false | Iterate and plan; write nothing |
Trust caveat: the forward reader parses local headers alone. There is no central directory
to cross-check names, sizes, methods or attributes, so --preserve-mode / --allow-symlinks /
--skip-symlinks are unavailable here (E_USAGE) and every JSON output carries
trust: "local-headers-only". A warning: line says so at start (suppressed by --quiet). Prefer
list / extract whenever the whole file is available. Data-descriptor entries carry zero
sizes in their local header (a create --stream archive, for one): the rows show them as such,
--summary counts them in descriptorEntries and sets bytesKnown: false (bytes excludes
them), and --list has to inflate each such entry to find its end. Data-descriptor entries the
engine cannot delimit without the central directory (store, encrypted or custom-codec + bit 3)
are refused with ZIP_UNSUPPORTED_CD_LESS_DESCRIPTOR. A --cat name that never arrives is
E_NOT_FOUND / ZIP_ENTRY_NOT_FOUND. A --codec method is decoded when its codec exposes
createDecompressor(maxOutput) (zipnative 1.1), which returns the engine's ZipDecompressor:
{ push(chunk: Uint8Array): Uint8Array[]; end(): Uint8Array[] } — push returns the decoded
pieces so far (possibly none), end the final ones and throws on a truncated stream; a module
whose push returns anything else fails as its own E_RUNTIME. One without it is refused before its first byte
(E_UNSUPPORTED / ZIP_UNSUPPORTED_CODEC_MODE — --skip-unsupported skips it instead; cat /
extract --codec read it from the complete file).
zipnative verify --input <a.zip> [--format json|text] [--strict]| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Archive path |
--entry <name>, -e (repeatable) |
— | Verify only the named entries (CRC-32, sizes, local header of each) after the eager structural check; the report lists them under selected, entries holds only those, entryCount stays the archive total. An unknown name is E_NOT_FOUND / ZIP_ENTRY_NOT_FOUND before any output |
--format text|json, -f |
text (json under --json) |
Output format |
--name-encoding <label> |
cp437 |
Decode non-UTF-8 names for the --entry lookups (as in list); refused without --entry (the whole-archive verifyZip takes no decoder) |
--strict |
false | Also fail when any diagnostic was emitted |
--range |
false | Read the archive by byte ranges through one file descriptor (zipnative 1.1 openZipRange): each entry is fetched and verified one at a time (verifyEntry), bounded by --max-entry-compressed-size; --max-input-size does not apply. A regular file only — stdin, a pipe or a device has no random access (E_USAGE) |
--summary |
— | { ok, entries, failed, skipped, diagnostics, selected?, error? } |
--fields a,b.c |
— | Dot-path projection |
The report is zipnative's ZipVerificationReport ({ ok, error, entryCount, entries: [{ name, ok, crcMatch, sizeMatch, localHeaderMatch, skipped? }], diagnostics }) plus { failed, skipped, strict, selected? }. An entry whose content cannot be checked carries the engine's skipped
reason (zipnative 1.1) and is never ok: "encrypted" and "stream-only-codec" pass when their
local header agrees with the central directory (counted in skipped), while "unsupported-method"
(no codec registered — load --codec) and any design-time skip with a mismatching local header
fail (counted in failed). Exit 1 / E_VERIFY_FAILED when ok is false;
the error envelope carries zipCode = report.error.code for structural refusals. The --max-*
bounds apply.
zipnative crc32 [<file>...] [--seed <hex>] [--expect <hex>] [--format text|json]| Flag | Default | Description |
|---|---|---|
<file>... / --input <file>, -i (repeatable) |
stdin | Inputs |
--seed <hex> |
0 |
Continue a running checksum from this value |
--expect <hex> |
— | Single input: exit 1 / E_CHECK_FAILED on mismatch (detail: { expectedCrc, actualCrc }) |
--format text|json, -f |
text (json under --json) |
text is "<crc> <bytes> <file>"; JSON shape: zipnative schema crc32 |
Streams input in 64 KiB chunks through zipnative's incremental crc32() — constant memory for
any size (not bounded by --max-input-size). Under --json the report stays on stdout and a
{ ok, command: "crc32", files, bytes, expect?, matched? } status envelope goes to stderr.
zipnative inflate [--input <file>] [--output <file>] [--max-output <size>]| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
stdin | Compressed input |
--output <file>, -o |
stdout | Decompressed output |
--overwrite |
false | Replace an existing --output file (default: refuse, E_IO) |
--max-output <size> |
the effective --max-entry-size (1 GiB) |
Hard output bound, always finite; raise it for trusted input |
--method deflate|store|<id> |
deflate |
Codec (ids via --codec); store is a bounded pass-through |
--sync |
false | Buffer the input (bounded by --max-input-size) and use the codec's decompressSync |
--allow-trailing |
false | Silence the warning about bytes after the stream end |
--dry-run |
false | Report the plan; decompress nothing |
Default path: zipnative's resumable inflater fed chunk by chunk — constant memory, exact
bytesConsumed (= bytesIn − leftover; equals bytesIn on the --sync / codec paths),
trailing bytes reported as leftover. Errors: ZIP_DEFLATE_CORRUPT / ZIP_DEFLATE_TRUNCATED →
E_PARSE, ZIP_INFLATE_OUTPUT_OVERFLOW → E_DATA. Status envelope: { ok, command, output, method, methodName, bytesIn, bytesConsumed, bytesOut, leftover, maxOutput, sync, tier }.
Two mutually exclusive modes: directory mode and manifest mode.
zipnative batch --input-dir <dir> --output-dir <dir> [--task create] [create flags]
zipnative batch --input-dir <dir> --task verify
zipnative batch --manifest <tasks.json> [--continue-on-error] [--allow-codec-load]| Flag | Default | Description |
|---|---|---|
--input-dir <dir> |
— (required in directory mode) | --task create: each immediate subdirectory becomes <output-dir>/<name>.zip through the full create command (every create flag is honoured); --task verify: every *.zip in the directory is verified |
--output-dir <dir> |
— (required for --task create) |
Destination directory (created if absent) |
--task create|verify |
create |
Directory-mode task |
--overwrite |
false | Replace existing <name>.zip files (default: each is refused, E_IO) |
--concurrency <n> |
4 |
Parallel workers, 1–64 (exit 2 outside) |
--fail-fast |
false | Stop scheduling after the first failure |
--method / --level / --deterministic, --order, --date, --comment |
create defaults |
Directory mode forwards every create flag to each archive |
--prefix, --dir-entries, --include, --exclude, --follow-symlinks, --mtime, --comment-file, --entry-comment, --preserve-mode, --store-ext, --stream, --zip64, --chunk-size, --parallel, --workers, --min-job-size, --job-timeout |
create defaults |
The other create flags, forwarded the same way (inputs and outputs are batch's own) |
--manifest <file> |
— | Ordered pipeline of whitelisted commands with "@<id>" output references; tasks run sequentially, fail-fast by default — see zipnative schema batch-manifest |
--continue-on-error |
false | Keep running independent tasks after a failure (tasks depending on a failed task are skipped) |
--allow-codec-load |
false | Permit a codec flag inside tasks (executes user code) |
--format text|json, -f |
text (json under --json) |
Report format |
--summary |
— | { ok, command, mode, task?, dryRun?, total, succeeded, failed, skipped? } |
--fields a,b.c |
— | Dot-path projection |
--dry-run |
false | Validate and print the plan; execute nothing |
The manifest whitelist holds 12 manifest commands: create, modify, canonicalize, merge,
list, inspect, extract, cat, verify, stream, crc32, inflate — never batch, govern, schema,
completion or doctor. A flag value "@<id>" references the resolved output (or
output-dir) of an earlier task; relative paths resolve against the manifest's directory and
are refused on .. (validatePath — a manifest is data, not the invoking user). Manifests are
JSON-size-capped (50 MB) and bounded to 1 000 tasks. Exit 1 carries the first failing task's
E_* code (and zipCode).
--json / --format json: stdout is ONE batch document. Each manifest task runs under
stdout capture (64 MiB cap → E_LIMIT { limit: "captureBytes" }); its output lands in
tasks[i].report (the parsed JSON object, or an array of rows for NDJSON), tasks[i].stdout
(text) and tasks[i].stdoutBytes. Tasks that would write their artefact to stdout —
create / modify / cat / inflate without output, stream --cat — are refused at
validation (E_USAGE, exit 2, also under --dry-run). Text mode keeps the interleaved
per-task output. batch emits no status envelope: the batch document is the report
(shape: zipnative schema batch).
zipnative doctor [--format json|text]| Flag | Default | Description |
|---|---|---|
--format text|json, -f |
text (json under --json) |
Output format |
Checks: cli, node (within engines.node, ^22.17.1 || >=24.4.1), zipnative (package vs VERSION export), deflate-tier
(node-zlib expected; pure under --pure-codecs), deflate-pinned (the tier used by
--deterministic), web-streams (CompressionStream / DecompressionStream), workers
(create --parallel), codecs (registered methods), limits (the effective bounds, with
--max-* / --max-input-size overrides; under --format json the check carries data { maxEntries, maxEntryUncompressedSize, maxEntryCompressedSize, maxTotalUncompressedSize, maxCompressionRatio, maxNameBytes, maxExtraFieldBytes, maxCommentBytes, maxCentralDirectoryBytes, maxInputSize } as
numbers — maxEntryCompressedSize bounds the whole-entry fetches of --range), commands. The node check reads the supported range
from package.json engines.node. Exit 0 when every check passes, 1 otherwise. Always offline.
Print a versioned JSON Schema (Draft 2020-12) for a CLI input/output shape, so an agent can self-validate before invoking a command.
zipnative schema # create-manifest (default)
zipnative schema create-manifest # create --from-manifest input
zipnative schema modify-manifest # modify --from-manifest input
zipnative schema batch-manifest # batch --manifest input
zipnative schema entries # list --format json output (one EntryRow per line for ndjson)
zipnative schema entries-summary # list --summary output
zipnative schema inspect # inspect --format json output
zipnative schema inspect-summary # inspect --summary output
zipnative schema verify # verify --format json output
zipnative schema verify-summary # verify --summary output
zipnative schema stream # stream --format json output
zipnative schema stream-summary # stream --summary output
zipnative schema batch # batch --format json output
zipnative schema batch-summary # batch --summary output
zipnative schema doctor # doctor --format json output
zipnative schema govern-verify # govern verify-issue --json output
zipnative schema crc32 # crc32 --format json output
zipnative schema status # the --json success envelope
zipnative schema error # the --json error envelope
zipnative schema errors # E_* codes + the 39 ZIP_* → E_* mapping + diagnostics (DATA)
zipnative schema limits # ZipLimits: the nine bounds, defaults, CWEs, flags
zipnative schema diagnostics # the diagnostic shape (12 codes)
zipnative schema manifest # capability manifest: commands, flags, codes, schemas (DATA)
zipnative schema list # list the 22 subjectsThe manifest (schema manifest) is a machine-readable capability document — every command
with its group, summary and flags, the global flags, the dry-run / projected / manifest command
lists, the E_* and ZIP_* codes, the diagnostic codes, the limits — for AI-agent tool
discovery. A prose/LLM-facing version ships as llms.txt at the package root.
zipnative completion bash > /etc/bash_completion.d/zipnative
zipnative completion zsh > "${fpath[1]}/_zipnative"
zipnative completion fish > ~/.config/fish/completions/zipnative.fish
zipnative completion powershell >> $PROFILE # Register-ArgumentCompleterThe scripts are generated from the same command table as schema manifest. Path flags
(--input, --output, --output-dir, --input-dir, --base, --from-manifest, --manifest,
--config, --codec, --comment-file) complete files; other value flags require an argument;
boolean flags take none.
zipnative govern rules # the human/agent protocol on stdout
zipnative govern policy [--pretty] # the machine-readable policy (JSON)
zipnative govern verify-issue <draft.md> # validate a draft; exit 1 / E_POLICY on violation| Flag | Default | Description |
|---|---|---|
--input <file>, -i |
positional | Draft path (- = stdin), for verify-issue |
--format json|text, -f |
text (json under --json) |
Report format for verify-issue |
verify-issue fails a draft that proposes an external runtime dependency or omits a
reproduction code block; missing recommended fields (environment, expected behaviour) and
apparent anti-goal proposals (encryption, other formats, multi-disk, repair) are warnings. A
passing check is necessary but not sufficient — the human review gate always applies.
| Flag | Default | Description |
|---|---|---|
--config <file> |
nearest .zipnativerc.json upward from cwd |
Use a specific config file (presentation keys only — see the Process contract) |
--no-config |
— | Ignore any .zipnativerc.json (recommended for unattended runs) |
--quiet, -q |
— | Suppress progress output and text diagnostics on stderr (never envelopes or errors) |
--no-color |
— | Disable ANSI colour on the stderr progress lines (sets NO_COLOR). Colour is decided on stderr: NO_COLOR (any value) off, FORCE_COLOR (not 0/false) on, TERM=dumb off, otherwise on only when stderr is a TTY |
--json |
— | Agent mode: emit a JSON status/error envelope on stderr (data stays on stdout). Errors carry a stable E_* code and zipnative's ZIP_* code verbatim. batch is the exception: its JSON report is the stdout document |
--pretty |
— | Indent JSON output under --json |
--dry-run |
— | Validate inputs and plan without writing output (create, canonicalize, merge, extract, modify, stream, cat, inflate, batch) |
--strict |
— | Escalate the first engine diagnostic into E_CHECK_FAILED before any output byte (verify: fail on any diagnostic, E_VERIFY_FAILED) |
--timeout <duration> |
— | Deadline for the run (500, 500ms, 30s, 5m, 1h; at most 2³¹−1 ms). Past it the run stops at the next chunk or entry boundary with E_LIMIT / detail { limit: "timeout", configured, observed } (ms) and any file being written is removed. The engine checks the deadline (zipnative 1.1 signal) before the first byte and between chunks and entries, the CLI in its own byte loops; a synchronous step already running — opening or verifying a buffered archive — always completes, so it bounds progress, it is not a kill |
--progress |
— | Progress snapshots on stderr where the engine measures them (zipnative 1.1 onProgress: create --stream / --stdin-name / --parallel, extract without --buffered, stream, cat; the buffered writer and extractor report none): one throttled progress: <done>/<total> entries, <in> in, <out> out line per update, or one NDJSON { "type": "progress", "command", "entriesDone", "entriesTotal", "bytesIn", "bytesOut" } object per update under --json. Off under --quiet; stdout is never touched |
--max-entries <n> |
100000 |
Maximum central-directory entry count (CWE-400) |
--max-entry-size <size> |
1073741824 (1 GiB) |
Maximum decompressed size of a single entry (CWE-400) |
--max-entry-compressed-size <size> |
1074790400 (1 GiB + 1 MiB) |
Maximum compressed size of a single entry the byte-range reader fetches whole under --range (cat --raw, verify, a codec without a streaming decoder) (CWE-770) |
--max-total-size <size> |
8589934592 (8 GiB) |
Maximum total decompressed size across an extraction (CWE-400) |
--max-ratio <n> |
1024 |
Maximum uncompressed/compressed ratio, entries ≥ 1 KiB compressed (CWE-409) |
--max-name-bytes <size> |
4096 |
Maximum entry-name length in bytes (CWE-400) |
--max-extra-bytes <size> |
65535 |
Maximum extra-field block length in bytes (CWE-400) |
--max-comment-bytes <size> |
65535 |
Maximum comment length in bytes (CWE-400) |
--max-cd-bytes <size> |
268435456 (256 MiB) |
Maximum central-directory size in bytes (CWE-400) |
--max-input-size <size> |
4294967296 (4 GiB) |
CLI-owned bound on every buffered read (CWE-400): an archive or payload read from stdin (byte-counted, aborted) or a file through one descriptor (a regular file is size-checked on that descriptor and read to the checked size only; a pipe, FIFO or device such as /dev/zero is byte-counted like stdin) into memory — list, inspect, verify, extract, cat, modify, create --stdin-name, inflate --sync, govern verify-issue. Exceeding it is E_LIMIT with detail { limit: "maxInputSize", configured, observed }; always finite. Not a ZipLimits key (doctor reports it under limits). The streaming commands (stream, crc32, inflate, create --stream) are not bounded by it |
--pure-codecs |
— | Skip node:zlib and run the pure-TS codec tier |
--codec <module> |
— | Load an ESM module exporting { codecs: ZipCodec[] } (and optional inflateImpl / deflateImpl) and register it. A registered codec serves the reader for its method and the writer when it registers store (0) or deflate (8) — such a module replaces the built-in compressor for create/modify even under --deterministic; deflateImpl replaces the sync deflate tier (tier: "injected") unless --deterministic pins the engine's encoder; create --parallel refuses either (workers cannot see the module). Executes user code: only accepted on the command line, never from a config file |
--version --json |
— | { name, version, zipnative } — machine-readable version output |
--help, -h |
— | Global or per-command usage |
<size> accepts 65536, 512k, 1m, 8g, 1GiB. Every bound is a positive, finite integer:
the 1.0 none / inf form is refused since 1.1.0 (E_USAGE, with a remedy naming an explicit
value — raise a bound for trusted input instead), and a bound cannot come from a config file.
- Config discovery is upward, and a config is presentation only.
.zipnativerc.json(a regular file, at most 1 MiB) is looked up from the working directory to the filesystem root; an unattended run should pass--no-config(or--config <file>). Explicit flags win over the file. Onlyformat,fields,json,pretty,quiet,no-color,progress,strict,longandsummaryare accepted — globally or in a command section — and a key applies only where the command declares the flag; every other key (overwrite,skip-unsafe,allow-symlinks,max-*,codec, paths, …), the prototype names and a value of the wrong type areE_USAGEnaming the file (the whole file is validated, whatever the command). The file that supplied defaults is announced on stderr in text mode (info: defaults from <path>: …, suppressed by--quiet) and carried asconfig: { path, applied }in the--jsonstatus and error envelopes; a read report under--json(list,inspect,verify,stream) names it nowhere, so pass--no-configwhen the shape must not depend on a file. Before 1.1.0 every key butcodecwas accepted — migrate the others to the command line. - stderr is line-oriented. Under
--jsonthe envelope is the last line that starts with{; the other lines are text (progress, NDJSON diagnostics) that--quietremoves. - Flags and positionals are order-independent. A boolean flag never consumes the next
token, so
zipnative --json list a.zipandzipnative list --long a.zipboth work;--flag=false|0|no|offis the explicit off form,--flag=true|1|yes|onthe explicit on form; any other value is refused (exit 2). Combined short flags (-lq) and-o=xare refused (exit 2). Short aliases that take a value:-i,-o,-d,-e,-f; boolean:-q,-h,-V— each means its long flag on every command that declares it. There is no-l. - The invocation is checked before anything runs (1.1.0). A flag the command does not
declare (
--overwite), a switch given twice, a single-value flag given twice (--input a --input bonlist), a flag given both bare and with a value, more positionals than the command takes, or an operand together with its flag (list a.zip --input b.zip) isE_USAGE(exit 2) — the same contract applies to every task of abatch --manifest, checked for all tasks before the first one runs. Repeatable flags are documented as such (--include,--entry,--remove,--codec, …). - Argv paths are ordinary shell paths.
zipnative list ../a.zip,-o ../out.zipand--output-dir ../xare the invoking user's own filesystem authority and are not second-guessed. Only values that arrive as data — path flags inside abatchmanifest,pathvalues inside acreate/modifymanifest — are refused on..(E_INPUT). Entry names are always checked with the engine'ssanitizeEntryPath(). - No input and stdin is a terminal →
E_USAGE(exit 2):No input: pass --input <file> (or a positional path), or pipe data on stdin.An explicit-is never guarded. - Overwrite policy is uniform:
create -o,modify -o,cat -o,inflate -o,extract,stream --output-dirandbatch --task createrefuse an existing path — a dangling symlink included — withE_IO(Refusing to overwrite existing file <path> (pass --overwrite)) and leave it intact.--overwritewrites an exclusively created temp file next to the target andrename()s it into place: a reader never sees a half-written file, a symlink planted at the target is replaced, never followed, and a failed write leaves the original untouched. A failed write never removes a file the CLI did not create. Writing to stdout is unaffected. - Closed pipe.
EPIPEon stdout or stderr (| head) ends the process quietly with exit 0. - Interrupts. On
SIGINT/SIGTERMthe CLI removes exactly the files it is writing at that moment (never a completed output, never the original ofmodify --in-place) and exits 130 / 143. POSIX only in practice (Windows sends no signals to child processes; Ctrl+C in a console still triggers Node'sSIGINTemulation). - Unknown command → exit 2 /
E_USAGE(also with--help); flags without a command (zipnative --json) → exit 2No command given; barezipnativeprints the usage, exit 0.
| Code | Meaning |
|---|---|
0 |
Success (also a closed pipe, EPIPE) |
1 |
Failure — every E_* class except E_USAGE (E_INPUT, E_IO, E_SECURITY, E_DATA, E_LIMIT, E_VERIFY_FAILED, E_CHECK_FAILED, E_POLICY, …) |
2 |
Usage error (E_USAGE): bad flags, missing required argument, unknown command, a refused combination |
130 / 143 |
Interrupted by SIGINT / SIGTERM (in-flight files removed) |
Each ZIPNATIVE_* switch takes 1 / true / yes / on or 0 / false / no / off
(empty = off); any other value is refused (exit 2), so a misspelt ZIPNATIVE_DRY_RUN never writes.
| Variable | Effect |
|---|---|
ZIPNATIVE_JSON |
Agent mode (set by --json; honoured when set by the caller — create / extract --dry-run then print no text plan) |
ZIPNATIVE_DRY_RUN |
--dry-run |
ZIPNATIVE_QUIET |
--quiet |
ZIPNATIVE_STRICT |
--strict |
ZIPNATIVE_PURE_CODECS |
--pure-codecs |
ZIPNATIVE_PROGRESS |
--progress |
NO_COLOR |
Any value disables colour on stderr (--no-color sets it) |
FORCE_COLOR |
Any value but 0 / false forces colour on stderr |
TERM |
dumb disables colour |
ZIPNATIVE_DEBUG |
1 adds the stack trace to an error |
VERAZIP_REQUIRED, VERAZIP_REPORT_DIR and VERAZIP_TOOLS are read by the veraZIP scripts
only, never by the CLI.
Output is English and locale-independent by design: no environment locale is read, no
Intl / toLocale* formatting is used, numbers are ASCII digits, dates are ISO-8601 UTC
(--date is UTC wall-clock), sizes use binary units. Messages are not translated and are
not part of the contract — branch on error.code / error.zipCode / error.remedy. Entry
names are emitted as UTF-8 bytes (on PowerShell set [Console]::OutputEncoding to UTF-8).
| Commands | Memory profile |
|---|---|
list, inspect, cat, extract, verify, modify |
Random access: the whole archive is held in memory, bounded by --max-input-size (4 GiB) |
stream, crc32, inflate (default path), create --stream |
Constant memory — chunked, not bounded by --max-input-size |
create (buffered), create --stdin-name, inflate --sync |
Buffers the inputs / the payload (--stdin-name and --sync under --max-input-size) |
create --stream --deterministic |
Constant memory per entry: the pinned encoder is whole-buffer, so each entry is buffered in turn |
zipnative-cli is designed so an autonomous agent (or any program) can drive it
deterministically — no MCP server, no daemon, just the process contract:
- stdout = the artifact (archive bytes, entry bytes, JSON report, text, schema, script); stderr = diagnostics.
- Pass
--json(anywhere on the command line) to get a single machine-readable envelope on stderr. On failure:{ "ok": false, "command": "...", "error": { "code": "E_*", "message": "...", "zipCode"?: "ZIP_*", "entryName"?: "...", "detail"?: { ... }, "remedy"?: "--skip-unsafe (extract, stream)" } }.remedyis the machine-actionable counterpart ofmessage: the CLI flag(s) or command that lift the refusal (the engine message names library options, not flags); absent when nothing does; apply it only for trusted input. Text mode prints the same as aremedy:line. On success forcreate/modify/extract/stream/cat/inflate/crc32: a{ "ok": true, "command": "...", ... }status line.list/inspect/verify/doctor/batchput their JSON report on stdout instead (batch --json= one document with every task's captured report inside). - Branch on
error.codefor the class (E_USAGE,E_INPUT,E_PARSE,E_IO,E_SECURITY,E_DATA,E_LIMIT,E_UNSUPPORTED,E_NOT_FOUND,E_VERIFY_FAILED,E_CHECK_FAILED,E_POLICY,E_RUNTIME) and onerror.zipCodefor the exact cause (zipnative's frozenZIP_*code, e.g.ZIP_PATH_TRAVERSAL,ZIP_LIMIT_EXCEEDED) — never on the message text. Every CLI-sideE_NOT_FOUND(cat,inspect --entry,stream --cat,verify --entry) carriesZIP_ENTRY_NOT_FOUND; an unsafe entry name given as data (modify --add,create --stdin-name, manifests) isE_INPUTwithentryName, while a malformed flag staysE_USAGE. Numeric exit codes stay0(success),1(runtime / check failure),2(usage). - Use
--dry-runto validate input and print the plan without producing output. - Fetch a
schema(orschema manifest/llms.txt) to discover and validate before calling, and rundoctor --format jsonas a capability pre-flight.
See AGENTS.md and the samples/agent/ scripts.
- Offline, always — no command can open a socket; there is no network opt-in to forget.
--dry-run,--json,govern,doctorare all local. - The CLI is the filesystem trust boundary. The engine never touches the filesystem: it
returns sanitised paths and data. One extraction sink serves
extractandstream: lexical containment of every destination under--output-dir(safeJoin), physical containment (the nearest existing ancestor isrealpath-checked under the root'srealpathbefore anymkdir, and re-checked after — a planted symlink or junction isE_SECURITY), an exclusive open (wx) unless--overwrite, removal of partial files on failure, and case-fold collision refusal on case-insensitive filesystems. The only residual window is betweenrealpathandopen— use an empty or trusted destination. A symlink is never materialised, whatever the flags. - Overwrite refusal is uniform.
create/modify/cat/inflate --output,extract,stream --output-dirandbatchrefuse an existing file (E_IO) unless--overwrite;modify --in-placegoes through an exclusively created, unpredictable temp file and an atomic rename. An interrupted run (SIGINT/SIGTERM) removes only the files being written at that moment and exits 130 / 143. - Refusals, not guesses — zip-slip and device names, symlink entries, overlapping entries,
central/local header disagreement, Zip64 spoofing, duplicate output paths, ambiguous EOCDs and
2^53 sizes are refused by default with their
ZIP_*code. Opt-outs skip; they never write anything unsafe. - Bounded by default — the engine's CWE-tagged limits are always on (
--max-*to tune; always finite since 1.1.0), and the CLI bounds every buffered read with--max-input-size(4 GiB;E_LIMIT), through one descriptor (a device such as/dev/zerois byte-counted).inflatehas a mandatory, finite output bound. - Configuration cannot relax a guard —
.zipnativerc.jsonsets presentation defaults only; an opt-out, a bound, a path orcodecin it isE_USAGE(1.1.0). - Archive text cannot drive the terminal — in text mode every entry name, comment and engine
message is printed with C0 / C1 / DEL, the Unicode bidi controls and the invisible look-alike
characters made visible — a name's own LF too, so one entry is one row (
\x1b,\u202e): a hostile name cannot clear the screen, rewrite earlier lines or disguiseevil\u202etxt.exe(Trojan Source). JSON keeps the exact text (rawNameHexkeeps the bytes). - Data remanence —
modifywithout--compactkeeps removed / replaced bytes recoverable in the output. Use--compactwhen deletion matters. Either waymodifyverifies every entry it re-emits (CRC-32, sizes, local header) and refuses a lying record — an append-only save never launders a hostile archive into a clean-looking one. - No encryption — read or write, by engine policy in 1.x. Encrypted entries are detected,
listed and reported as
skippedbyverify; reads fail withZIP_UNSUPPORTED_ENCRYPTION. --codecis a trust boundary — the CLI's only dynamic import of user code (same trust asnode -r): argv only, refused from.zipnativerc.json(like every non-presentation key), refused inside abatch --manifestwithout--allow-codec-load. A module that registers method 0/8 or exportsdeflateImplalso shapes whatcreate/modifywrite — the envelope'stierand awarning:line say so, andcreate --parallelrefuses to run with such a module loaded.- Path validation is scoped to data. Paths typed on the command line (
../a.zip,-o ../out.zip) are the invoking user's own filesystem authority and are not second-guessed. Values that arrive as data — path flags inside abatchmanifest andpathvalues inside acreate/modifymanifest — are refused on..(E_INPUT); entry names are always checked with the engine'ssanitizeEntryPath(), and every extracted destination goes through the sink above. - JSON size cap — manifests, drafts and JSON inputs are capped at 50 MB before parsing (config files at 1 MiB, size-checked on the open descriptor).
- Release integrity (ADR 0009) — published by
publish.ymlin five jobs (guard → build → publish → attest → consumer): the tag must match the version, the publish gate (scripts/gate.mjs --publish --require-all) runs where no token can be minted, thenpm-publishenvironment gates the one job that publishes the handed-on tarball (both digests re-checked, npm client pinned by SHA-512) through Trusted Publishing with provenance;attestre-downloads it from the registry and attests the tarball and its CycloneDX / SPDX SBOMs, and a read-onlyconsumerjob installs it and runsnpm audit signatures(no registry code ever runs next to the signing identity). Verify withnpm audit signaturesandgh attestation verify zipnative-cli-<version>.tgz -R Nizoka/zipnative-cli. Every CI job starts withharden-runnerand installs with--ignore-scripts; every action is SHA-pinned; zizmor, actionlint, CodeQL (TypeScript and workflows), OpenSSF Scorecard, dependency review, a weekly npm audit + OSV-Scanner and the rulesets (.github/rulesets/) guard the repository. CI runs on Ubuntu, Windows and macOS (Node 22 / 24, all blocking); the veraZIP gate and the samples run on Linux and Windows for every pull request. Security contacts: SECURITY.md, security.txt, Security Insights.
See SECURITY.md for the full security policy and vulnerability disclosure procedure.
Semantic Versioning over an explicit public surface: the 17 commands and their flags, exit
codes 0/1/2/130/143, the 13 E_* classes and the ZIP_* → E_* mapping, the
envelope and report keys, the schema subjects and schema manifest shape, the
.zipnativerc.json keys, the ZIPNATIVE_* variables, and the bytes written under
--deterministic (a byte change is semver-major). Message wording, text layout and key order
are not a contract. A security fix may narrow what the CLI accepts in a minor release —
1.1.0 refuses unknown flags, the none bound and non-presentation config keys — and is always
recorded in the CHANGELOG under Security & behaviour changes with its migration. A flag is deprecated in a minor release (it keeps working and prints one
warning: line) and removed no earlier than the next major. Details in
CONTRIBUTING.md.
Have a question?
- 📖 Check the FAQ first
- 🔍 Search the samples:
grep -r "your-keyword" samples/ - 📚 Read KNOWLEDGE_BASE.md for technical details
- 💬 Open a discussion: the engine's GitHub Discussions until the CLI tab is enabled (see SUPPORT.md)
Found a bug?
- 🐛 Open an issue: GitHub Issues
- 🔐 Security issue? See SECURITY.md for responsible disclosure
Want to contribute?
- 🤝 See CONTRIBUTING.md
- 📝 All PRs add value — tests, docs, samples
zipnative— the core ZIP engine (zero dependencies, frozen 1.0 API)zipnative-mcp— Model Context Protocol server for AI clients (planned)- pdfnative / pdfnative-cli — the sibling PDF engine and CLI, same doctrine
- zipnative.dev — documentation, guides, playgrounds
If you use zipnative-cli in research or academic pipelines, please cite it:
@software{zipnative_cli_2026,
title = {zipnative-cli: Official CLI for the zipnative ZIP engine},
author = {Nizoka},
year = {2026},
url = {https://github.com/Nizoka/zipnative-cli},
license = {MIT}
}See CITATION.cff for the full metadata (auto-detected by GitHub and Zenodo).
MIT — see LICENSE.