Skip to content

About

Agent-grade ZIP CLI on the zipnative engine — create deterministic, reproducible archives, list and inspect without extracting, extract with zip-slip / zip-bomb / symlink guards, verify, stream, modify without recompression. Stable --json contract for AI agents, ISO/IEC 21320-1 validated in CI. Node ≥22, zero extra dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zipnative-cli

CI CodeQL ISO/IEC 21320-1 (veraZIP) npm version npm downloads zero extra runtime dependencies TypeScript License: MIT npm provenance OpenSSF Scorecard

zipnative website

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 --zip64 for streamed entries past 4 GiB; --name-encoding for legacy (Shift_JIS, GBK, Windows-1252 …) names; a global --timeout (E_LIMIT) and --progress (stderr, NDJSON under --json); inspect's engine-measured canonicalForm with offenders and --check canonical-form; --long rows with extended timestamps and Unix ids; custom codecs in stream. Hardened to the pkinative-cli level — and three behaviour changes shipped as security fixes: .zipnativerc.json is presentation only (an opt-out or a limit in it is E_USAGE), an undeclared, repeated or surplus flag is E_USAGE (a typo is no longer ignored), and --max-* none is refused (every bound is finite). --overwrite now 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.

Highlights

  • 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, --deterministic to pin the pure-TS encoder (identical SHA-256 on every runtime), --stream for a constant-memory writer (streamed inputs, data-descriptor layout), --parallel to fan deflate out across a worker pool (zipnative/worker), --include / --exclude globs, --order insertion (argv order — an EPUB mimetype first), --store-ext, --preserve-mode, archive and per-entry comments (--comment-file for raw bytes), and a --dry-run plan. An existing output file is refused without --overwrite.
  • list / inspect — list entries without decompressing anything (text | json | ndjson, --long for 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 --check assertions (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 its ZIP_* code. Opt-outs are skip-not-write (--skip-unsafe, --skip-symlinks, --skip-unsupported; --allow-symlinks writes 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. --raw emits the compressed payload zero-copy.
  • stream — a forward-only reader for unseekable input (stdin, a pipe, an upload body): list, extract or --cat entries 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's verifyZip: 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, with zipCode set for structural refusals). Encrypted entries are honestly reported as skipped, never faked as verified. --strict also 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); --compact rewrites canonically so removed content is truly gone. --in-place writes back through an exclusively created temp file + atomic rename.
  • crc32 / inflate — the ZIP checksum of files or stdin in constant memory (--expect turns it into a check, --seed continues 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 full create command per directory, bounded concurrency), verify every *.zip in a folder, or run a declarative --manifest pipeline (create → verify → extract → …) with @<id> output references and a codec-load policy (--allow-codec-load). Under --json stdout is one batch document with every task's captured report inside.
  • doctor — an offline capability preflight: CLI / Node / zipnative versions (package vs the engine's VERSION export), the active deflate tier (node-zlib expected, pure under --pure-codecs), the pinned tier used by --deterministic, platform streaming codecs, worker-thread availability for create --parallel, registered codecs, the effective security limits (as numbers under data, --max-input-size included), 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, the errors registry plus the machine-readable capability manifest (schema manifest) and llms.txt, so agents can self-validate and discover the tool at runtime.
  • completion — bash, zsh, fish or powershell completion 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, and govern 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 --json status/error envelope, 13 stable E_* classes, the exact ZIP_* cause carried verbatim as error.zipCode (39 frozen codes, plus 12 diagnostic codes), --dry-run on 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 ZipLimits are exposed as --max-entries, --max-entry-size, --max-total-size, --max-ratio, --max-name-bytes, --max-extra-bytes, --max-comment-bytes and --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 with E_USAGE, so a file planted in a cloned repository can never relax a guard.
  • Zero extra dependencies — zipnative is 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.

Supported Features

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.

Conformance status (v1.1.0)

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.

Installation

npm install --global zipnative-cli

Or run without installing:

npx zipnative-cli create src/ --output src.zip

Requirements: 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.

Documentation

  • 📘 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 + .ps1 samples per command
  • 🔧 zipnative engine — the underlying ZIP engine docs
  • ❓ FAQ — Common questions & troubleshooting

Quick Start

Create an archive

# 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

Reproducible builds

# 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-layout

Without --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.

List and inspect

# 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.json

Example inspect --format json --summary output:

{ "entries": 12, "bytes": 48213, "uncompressedSize": 131072, "zip64": false, "encrypted": 0, "deterministic": true, "canonicalLayout": true, "diagnostics": 0, "checksPassed": true }

Extract safely

# 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

Read entries and stream from a pipe

# 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.json

stream 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.

Verify integrity

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)

Modify without recompressing

# 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.json

Checksums and raw DEFLATE

zipnative 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

Batch and pipelines

# 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 inside

AI-governance / Human-in-the-Loop

Agents 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 violation

Examples

Ready-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.js

See samples/README.md for descriptions and integration patterns (GitHub Actions, Docker, TypeScript).


Command Reference

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

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

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

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

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 copy

Combine 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

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

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

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

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 }.

zipnative stream

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

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

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

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 }.

zipnative batch

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

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.

zipnative schema

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 subjects

The 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

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-ArgumentCompleter

The 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

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.

Global options

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.

Process contract

  • 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. Only format, fields, json, pretty, quiet, no-color, progress, strict, long and summary are 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 are E_USAGE naming 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 as config: { path, applied } in the --json status and error envelopes; a read report under --json (list, inspect, verify, stream) names it nowhere, so pass --no-config when the shape must not depend on a file. Before 1.1.0 every key but codec was accepted — migrate the others to the command line.
  • stderr is line-oriented. Under --json the envelope is the last line that starts with {; the other lines are text (progress, NDJSON diagnostics) that --quiet removes.
  • Flags and positionals are order-independent. A boolean flag never consumes the next token, so zipnative --json list a.zip and zipnative list --long a.zip both work; --flag=false|0|no|off is the explicit off form, --flag=true|1|yes|on the explicit on form; any other value is refused (exit 2). Combined short flags (-lq) and -o=x are 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 b on list), 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) is E_USAGE (exit 2) — the same contract applies to every task of a batch --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.zip and --output-dir ../x are the invoking user's own filesystem authority and are not second-guessed. Only values that arrive as data — path flags inside a batch manifest, path values inside a create / modify manifest — are refused on .. (E_INPUT). Entry names are always checked with the engine's sanitizeEntryPath().
  • 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-dir and batch --task create refuse an existing path — a dangling symlink included — with E_IO (Refusing to overwrite existing file <path> (pass --overwrite)) and leave it intact. --overwrite writes an exclusively created temp file next to the target and rename()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. EPIPE on stdout or stderr (| head) ends the process quietly with exit 0.
  • Interrupts. On SIGINT / SIGTERM the CLI removes exactly the files it is writing at that moment (never a completed output, never the original of modify --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's SIGINT emulation).
  • Unknown command → exit 2 / E_USAGE (also with --help); flags without a command (zipnative --json) → exit 2 No command given; bare zipnative prints the usage, exit 0.

Exit codes

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)

Environment

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.

Locale

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).

Memory

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

Driving from AI agents

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)" } }. remedy is the machine-actionable counterpart of message: 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 a remedy: line. On success for create / modify / extract / stream / cat / inflate / crc32: a { "ok": true, "command": "...", ... } status line. list / inspect / verify / doctor / batch put their JSON report on stdout instead (batch --json = one document with every task's captured report inside).
  • Branch on error.code for 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 on error.zipCode for the exact cause (zipnative's frozen ZIP_* code, e.g. ZIP_PATH_TRAVERSAL, ZIP_LIMIT_EXCEEDED) — never on the message text. Every CLI-side E_NOT_FOUND (cat, inspect --entry, stream --cat, verify --entry) carries ZIP_ENTRY_NOT_FOUND; an unsafe entry name given as data (modify --add, create --stdin-name, manifests) is E_INPUT with entryName, while a malformed flag stays E_USAGE. Numeric exit codes stay 0 (success), 1 (runtime / check failure), 2 (usage).
  • Use --dry-run to validate input and print the plan without producing output.
  • Fetch a schema (or schema manifest / llms.txt) to discover and validate before calling, and run doctor --format json as a capability pre-flight.

See AGENTS.md and the samples/agent/ scripts.

Security

  • Offline, always — no command can open a socket; there is no network opt-in to forget. --dry-run, --json, govern, doctor are 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 extract and stream: lexical containment of every destination under --output-dir (safeJoin), physical containment (the nearest existing ancestor is realpath-checked under the root's realpath before any mkdir, and re-checked after — a planted symlink or junction is E_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 between realpath and open — 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-dir and batch refuse an existing file (E_IO) unless --overwrite; modify --in-place goes 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/zero is byte-counted). inflate has a mandatory, finite output bound.
  • Configuration cannot relax a guard — .zipnativerc.json sets presentation defaults only; an opt-out, a bound, a path or codec in it is E_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 disguise evil\u202etxt.exe (Trojan Source). JSON keeps the exact text (rawNameHex keeps the bytes).
  • Data remanence — modify without --compact keeps removed / replaced bytes recoverable in the output. Use --compact when deletion matters. Either way modify verifies 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 skipped by verify; reads fail with ZIP_UNSUPPORTED_ENCRYPTION.
  • --codec is a trust boundary — the CLI's only dynamic import of user code (same trust as node -r): argv only, refused from .zipnativerc.json (like every non-presentation key), refused inside a batch --manifest without --allow-codec-load. A module that registers method 0/8 or exports deflateImpl also shapes what create/modify write — the envelope's tier and a warning: line say so, and create --parallel refuses 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 a batch manifest and path values inside a create / modify manifest — are refused on .. (E_INPUT); entry names are always checked with the engine's sanitizeEntryPath(), 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.yml in 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, the npm-publish environment 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; attest re-downloads it from the registry and attests the tarball and its CycloneDX / SPDX SBOMs, and a read-only consumer job installs it and runs npm audit signatures (no registry code ever runs next to the signing identity). Verify with npm audit signatures and gh attestation verify zipnative-cli-<version>.tgz -R Nizoka/zipnative-cli. Every CI job starts with harden-runner and 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.

Versioning and stability

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.

Getting Help

Have a question?

Found a bug?

Want to contribute?

Related Projects

  • 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

Citation

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).

License

MIT — see LICENSE.

About

Agent-grade ZIP CLI on the zipnative engine — create deterministic, reproducible archives, list and inspect without extracting, extract with zip-slip / zip-bomb / symlink guards, verify, stream, modify without recompression. Stable --json contract for AI agents, ISO/IEC 21320-1 validated in CI. Node ≥22, zero extra dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages