Commands

scalp stage verify

Verify a staged npm package tarball from stdin — hash verification, identity check, denylist, and SARIF output.

Verifies a staged npm package tarball piped through stdin. Designed for pre-publish or pre-install workflows where you receive a tarball and want to check its integrity, identity, and policy compliance before using it.

Usage

npm stage download <stage-id> | scalp stage verify --stage-id <package@version> [flags]

npm stage download is part of npm's private packages / staging feature. If you don't use npm stages, you can pipe any tarball from any source — curl, cat, or a CI pipeline — as long as it's a valid npm package tarball on stdin.

Flags

FlagDefaultDescription
--stage-idrequiredExpected package name + version (e.g. lodash@4.17.21)
--policy.scalp/policy.jsonPolicy path
--checksum""Expected SHA-512 hash for the tarball
--sarif""Path for SARIF 2.1.0 report
--cifalseForce enforcement to block

Verification checks

Hash verification — computes SHA-512 of the piped tarball in a streaming fashion (no temp files). Compares against --checksum if provided.

Package identity — extracts package/package.json from inside the tarball, parses the name field, and verifies it matches --stage-id.

Denylist check — checks the extracted package name against policy deny rules (name + pattern). This runs against the actual tarball contents, not the user-supplied --stage-id, preventing bypass attacks.

Enforcement--ci forces block. Otherwise uses policy's on_violation.

Example

npm stage download lodash@4.17.21 | scalp stage verify --stage-id lodash@4.17.21

On success:

stage verify passed

On failure (e.g. identity mismatch):

hash_mismatch: expected sha512-abc..., got sha512-def...

With checksum verification

npm stage download lodash@4.17.21 | \
  scalp stage verify \
    --stage-id lodash@4.17.21 \
    --checksum sha512-a1b2c3d4e5f6...

If the tarball hash doesn't match:

hash_mismatch: expected sha512-a1b2..., got sha512-7f8e...

With denylist enforcement

Policy blocks packages matching *-free:

{
  "version": 1,
  "packages": {
    "deny": [{ "pattern": "*-free" }]
  },
  "enforcement": { "on_violation": "block" }
}
npm stage download lodash-free@1.0.0 | scalp stage verify --stage-id lodash-free@1.0.0
ci failed: [{"PackageID":"lodash-free@1.0.0","Reason":"denylist","Rule":"pattern:*-free"}]

Security: why denylist uses tarball contents

The denylist check runs against the actual package name extracted from the tarball, not the user-supplied --stage-id.

This prevents bypass attacks where an attacker provides a benign stage ID like lodash@4.17.21 but pipes a malicious tarball with a different internal package.json. SCAL-P extracts the real name from inside the tarball and checks that against the denylist.

SARIF output

npm stage download lodash@4.17.21 | \
  scalp stage verify \
    --stage-id lodash@4.17.21 \
    --sarif .scalp/stage-report.sarif

CI mode

npm stage download lodash@4.17.21 | \
  scalp stage verify \
    --stage-id lodash@4.17.21 \
    --ci

Forces block on any violation.

How streaming works

The tarball is read from stdin via an io.TeeReader that simultaneously feeds data to both a SHA-512 hasher and a gzip decompressor. The decompressed tar stream is parsed to find package/package.json. Once the package name is extracted and the hash is computed, all checks run. No temporary files, no disk writes.

This streaming approach means SCAL-P can verify tarballs of any size without memory or disk overhead.

On this page