@ssbrouhard/packsift
v0.2.1
Published
Deterministic npm tarball diff CLI with supply-chain tripwires.
Maintainers
Readme
PackSift
Your Git diff is not your npm release.
PackSift compares the npm tarballs users actually install, including a local package just before publication. It reports unexpected files, lifecycle scripts, native binaries, install-path network code, and other supply-chain drift with deterministic evidence instead of a verdict.
Install it and compare two published releases:
npm i -g @ssbrouhard/packsift
packsift [email protected] [email protected]Demo
PackSift can also inspect one published package version or compare two npm,
yarn, or pnpm lockfiles and run tarball analysis for changed and newly added
registry-backed dependencies. packsift pack-check compares the exact output of
npm pack (or an existing .tgz) with a previously published version for
pre-publish gates.
Doctrine: Evidence, Never Verdict
PackSift puts the facts next to each other. Its own narration never tells you what they mean.
The human report lists bytes, hashes, file changes, metadata correlations, and
deterministic signal matches, then stops. PackSift-authored labels avoid merge,
review, hold, urgent, risk-score, and recommendation-style verdicts. Package
names, file paths, registry metadata, and --json output still preserve source
data exactly.
The core tarball comparison is deterministic and offline after published npm
artifacts are fetched. The --advisories sidecar is the explicit exception:
opt-in, networked, non-deterministic at query time, attributed to OSV.dev or
the configured OSV-compatible endpoint, timestamped, and structured by default.
--advisories=summary additionally passes through OSV summary text as
third-party source text.
Requirements
Node.js 20 or newer.
Install
Install the CLI globally:
npm i -g @ssbrouhard/packsift
packsift [email protected] [email protected]Or run it without installing:
npx -y @ssbrouhard/packsift [email protected] [email protected]Tagged releases are published from the public PackSift repository with npm provenance, linking new package versions to the GitHub workflow that built them.
To run from source:
npm ci && npm run build && node dist/cli.js <args>pack-check requires PackSift 0.2.0 or later. If the version you need is not
yet published on npm, run it from a source build as shown above, for example:
node dist/cli.js pack-check .Optional no-mistakes and agent-pipeline check
PackSift can be an optional evidence-producing step when a pull request or
release touches npm packages. Attach its human output or --json output to the
review record. Treat that output as facts for a reviewer: do not turn file
drift, signals, or other PackSift evidence into an automatic risk score or
merge verdict.
For dependency changes, compare the base and proposed lockfiles:
packsift batch origin/main:package-lock.json package-lock.json
packsift batch origin/main:pnpm-lock.yaml pnpm-lock.yaml --json
git show origin/main:yarn.lock | packsift batch - yarn.lock
# If a package.json change identifies one transition but has no lockfile:
packsift [email protected] [email protected]For a pre-publish or release check, compare the exact local npm artifact with the previously published package:
packsift pack-check .
packsift pack-check . --jsonThis repository includes
scripts/no-mistakes-packsift-check.sh
as an optional custom-step helper. Copy or adapt it in a package repository
where PackSift is installed, then invoke one of:
# PR dependency evidence; base defaults to GITHUB_BASE_REF, then main.
scripts/no-mistakes-packsift-check.sh
scripts/no-mistakes-packsift-check.sh --base origin/main --json
# Release evidence.
scripts/no-mistakes-packsift-check.sh release . --jsonDependency mode detects changed package.json, package-lock.json,
npm-shrinkwrap.json, pnpm-lock.yaml, and yarn.lock files, including files
in workspace package directories. It runs packsift batch for each changed
lockfile that exists on both sides. A new lockfile or a package.json-only
change is described without
guessing a package transition; supply a published old/new pair when one is
known. Release mode runs packsift pack-check.
The helper writes evidence to stdout and returns zero when checks complete,
regardless of reported drift. It returns non-zero only for usage, helper
setup/I/O, git/base resolution, or PackSift execution failures; reported
package drift never causes a failure. Set PACKSIFT_BIN to select a PackSift
executable and PACKSIFT_BASE_REF to override the fallback base.
With --json, stdout contains one envelope with structured events and all
PackSift result documents, including when no comparable lockfile exists or
multiple lockfiles changed; helper status text remains on stderr.
The no-mistakes custom-check configuration API is not defined by this repository, so wiring is deliberately manual: add the helper invocation as a custom command in the pipeline configuration used by your no-mistakes installation. Keep it optional and preserve stdout as a review artifact.
Usage
# Compare one package transition.
packsift <name>@<old> <name>@<new>
packsift [email protected] [email protected]
packsift @scope/[email protected] @scope/[email protected]
packsift [email protected] [email protected] --advisories
packsift [email protected] [email protected] --advisories=summary
# Compare a local package about to publish with its latest published version.
packsift pack-check
packsift pack-check path/to/package
packsift pack-check path/to/package.tgz --against @scope/[email protected]
packsift pack-check . --json --diff
# Inspect one package version.
packsift inspect <name>@<version>
packsift inspect [email protected]
packsift inspect @aspect-security/[email protected] --json
packsift inspect [email protected] --advisories
# Compare lockfile transitions.
packsift batch <old-lockfile> <new-lockfile>
packsift batch package-lock.before.json package-lock.json
packsift batch HEAD~1:pnpm-lock.yaml HEAD:pnpm-lock.yaml
git show HEAD~1:yarn.lock | packsift batch - yarn.lock
packsift batch package-lock.before.json package-lock.json --advisoriesOptions:
--jsonemits structured JSON.--diffincludes full text line diffs for changed text files up to 512 KB in transition reports. In batch mode,--diffapplies to changed transitions and requires--jsonunless--detailis set.--advisoriesadds an opt-in OSV.dev sidecar for inspect mode, single-package transitions, and analyzed batch entries. Inspect mode queries the inspected npm version once. Transition mode queries old and new npm versions. It renders id, aliases, severity, affected ranges, and reference URLs. Advisory flags are not supported bypack-check: its local side is not a published registry coordinate, so querying it would be misleading.--advisories=summaryadds the OSVsummaryfield as a labeled third-party passthrough line. The default--advisoriesmode omits summary text in both human output and JSON.--advisory-endpoint <url>sends advisory queries to an OSV-compatible endpoint, such as a private mirror. This is the preferred path for custom registries because package coordinates do not go to public OSV.dev. Pointing this flag at the public OSV.dev endpoint with a custom registry still requires--advisories-allow-public.--advisories-allow-publicexplicitly permits public OSV.dev advisory lookups when--registryis nothttps://registry.npmjs.org. Without this flag or a non-public--advisory-endpoint, custom registries are treated as private and rejected before any public OSV.dev query.--registry <url>selects the npm registry, defaulting tohttps://registry.npmjs.org.--keeppreserves extracted tarballs and temp dirs for debugging.--concurrency <n>sets batch fetch/analyze parallelism, defaulting to 4.--detailexpands analyzed batch entries in human output to the same per-package report used by single-transition or inspect output.
packsift pack-check [dir-or-tgz] uses . when no input is given. For a
directory, it runs npm pack with normal npm lifecycle behavior and compares
the resulting tarball—not the working tree. An existing .tgz is used as-is.
The local package name and version come from the packed package.json.
--against <name>@<version> selects the published baseline; without it,
PackSift resolves the package's latest dist-tag from --registry. A missing
published baseline is an error, and --against none is not supported.
Both sides use the same package/ prefix stripping, raw-byte hashing, file
diff, and signal engine as published-to-published transitions. Registry
integrity and shasum verification applies only to the published baseline.
Publisher and maintainer change evidence is omitted because equivalent
registry metadata does not exist for the unpublished local side. --json,
--diff, --registry, and --keep work as in transition mode.
Inspect mode fetches one npm tarball through the same registry path as
transition mode, strips the tarball package/ prefix, hashes raw unpacked file
bytes, and runs the deterministic signal engine with no baseline. Every file in
the tarball is treated as added. The report includes lifecycle scripts,
install-path network indicators, native/GYP payload evidence, bin entries,
minified or obfuscated source heuristics, unpacked size, and a file inventory
summary. When the registry metadata already fetched for the tarball includes a
publish timestamp, maintainer list, or versions map, inspect output also reports
publish date, maintainer count, and version count. It does not perform
name-similarity or typosquat detection.
Batch mode accepts npm package-lock.json/npm-shrinkwrap.json v2/v3, yarn
yarn.lock v1/Berry, and pnpm-lock.yaml v5.4/v6/v9. Each lockfile argument
may be a working-tree path, <ref>:<path> read through git show, or - for
stdin. Only one side may be stdin. Mixed formats are supported because each
parser lowers to the same name/version map; human and JSON output always label
the detected old and new formats, such as old: npm package-lock v3 and
new: pnpm-lock.yaml v9.0.
Batch mode only considers registry-backed package entries. Linked, aliased,
file, git, off-registry, and unresolved entries are ignored before transition
classification. Packages are analyzed when both lockfiles contain exactly one
version and that version changed. Added-only packages with exactly one resolved
version are analyzed with the same single-tarball path as packsift inspect and
labeled added (no prior version to compare). Removed-only and
multiple-version packages are listed as skipped.
The npm package is @ssbrouhard/packsift, and its CLI command is packsift.
Version 0.1.0 included a one-release sift compatibility alias; version 0.1.1
and later expose only packsift. The public repository was renamed from
SSBrouhard/sift to
SSBrouhard/packsift.
The supported interface is the CLI. The package build also contains TypeScript modules used by the CLI, but the package does not expose a root library entry.
Output
PackSift strips the tarball package/ prefix, hashes raw unpacked file bytes, and
omits unchanged files from the report. Changed text files show line counts by
default; --diff adds unified diffs. Binary, non-text, or large changed files
show size-only changes.
Batch human output starts with the detected old/new lockfile formats, then has
analyzed, skipped, and error sections. Transition entries summarize changed file
counts plus signal and integrity evidence. Added entries summarize file count,
signals, integrity evidence, and unpacked size. With --advisories, each
analyzed entry also gets a compact advisory count line; JSON nests the full
advisorySidecar under each analyzed transition or added entry. Per-package
errors do not stop the rest of the batch, but they set the process exit code to
- Use
packsift batch --jsonfor structured per-package reports, orpacksift batch --detailfor expanded human reports.
With --advisories, inspect and single-transition output add an Advisory
sidecar block after the files block. Empty OSV results render none returned.
OSV failures are non-fatal: the tarball report still prints, and the affected
version says advisories unavailable: <reason>. JSON includes
advisorySidecar only when the flag is set. Inspect sidecars record enabled,
source, fetchedAt, and version; transition sidecars record enabled,
source, fetchedAt, oldVersion, and newVersion. Each version records
version, vulns, and optional unavailable. Summary mode adds summary
only when OSV provided it.
Integrity and shasum mismatches are reported as warnings instead of stopping the comparison.
Signals are deterministic tripwires:
- Lifecycle script changes for
preinstall,install,postinstall,prepare, andprepublishOnly. - Maintainer or publisher changes from npm registry metadata.
- Added or changed native executable payloads, including
.node,.wasm, ELF, Mach-O, and Windows PE files. - New or newly minified JavaScript or TypeScript source by line-length heuristic.
- Added or changed
binding.gyp,*.gyp, or*.gypifiles, with GYP command substitutions, interpreter build commands, and native source presence. - Install-path network-capable code found in lifecycle commands or local GYP
command-substitution/build-command targets, plus one-hop local
require/importreferences. - New
binentries. - Unpacked size growth over 2x or over 1 MB.
- Dependency-field changes in
dependencies,optionalDependencies,peerDependencies, anddevDependencies. - Package license field changes and changed license files.
Scope
PackSift uses published npm artifacts plus an explicitly supplied local packed
artifact in pack-check; lockfiles are used only to discover package transitions
and added dependencies for batch analysis. The deterministic core does not clone
GitHub repositories, call model APIs, ingest advisories into its analysis, score
risk, comment on PRs, or inspect consumer project source.
--advisories is a fenced sidecar exception, not part of the analyzer. It calls
OSV.dev without authentication for requested npm versions only when the registry
is exactly https://registry.npmjs.org, unless the user supplies a private
OSV-compatible endpoint or explicitly acknowledges public OSV.dev use with a
custom registry. It labels each sidecar with OSV.dev or the configured
OSV-compatible endpoint, never maps files to advisories, and never turns
advisory data into a verdict.

