Skip to main content
hemlock

Poison hemlock looks like parsley.

Static supply-chain checks for npm and PyPI packages.

ci pypi python license dependencies

Hemlock looks for dependency behavior that deserves a closer look: near-miss names, install hooks, obfuscated source, weak lockfile entries, unusual publisher changes, missing build provenance, and versions reported as malware. It works offline by default and never executes package code.

This is not a model or a black-box score. Every result includes the rule, evidence, remedy, and arithmetic that produced it.

Install

pipx install hemlock-scan

Python 3.11 or newer is required. A normal pip install works too:

pip install hemlock-scan

Hemlock has no runtime dependencies.

Use

hemlock scan .                    # scan a project offline
hemlock scan . --online           # add registry, provenance, and OSV checks
hemlock check chalk@5.6.1         # check a name before installing it
hemlock diff --since origin/main  # inspect only dependency changes
hemlock history . --online        # check versions previously held in git
hemlock why left-pad              # show which dependency brought it in
hemlock baseline .                # accept the current backlog
hemlock explain HEM701            # explain one rule and its remedy
hemlock rules                     # list every rule and weight
hemlock init                      # write config and CI files
hemlock update                    # offer the correct update command

Scans and diffs support terminal, Markdown, JSON, and SARIF output. Hemlock finds npm and Python manifests below the chosen directory, including common lockfiles and requirements files. If installed source is present under node_modules or a local virtual environment, the static source checks read it without following file symlinks.

An unprefixed package passed to check is treated as npm. Prefix Python packages with pypi: or pass --ecosystem pypi:

hemlock check pypi:requests==2.32.3 --online

hemlock update does not silently install anything. It detects pip, pipx, or a git checkout, prints the command, and asks before running it. Use --check to report only or --yes to skip the question.

What it reports

The 26 rules cover six areas:

Area Examples
Naming typosquats, homoglyphs, dropped npm scopes
Install behavior install hooks, downloads followed by execution, credential access
Source shape obfuscation and dynamic execution
Lockfiles floating versions, missing hashes, HTTP downloads, extra indexes
Registry trust fresh or withdrawn releases, new publishers, missing repositories
Public records build provenance, advisories, and exact malware reports

Fifteen rules run offline. Online mode adds eleven checks backed by public npm, PyPI, and OSV endpoints. Run hemlock rules for the full list and hemlock explain HEMxxx for the reason behind a rule.

An exact malware record is treated as a fact and settles the verdict. Other signals are weighted. Repeated findings in one category count less after the first, while agreement across categories increases the result. The report prints this calculation under each package.

Severity bands are:

Score Severity
85 to 100 critical
60 to 84 high
30 to 59 medium
1 to 29 low

The default failure threshold is high. Change it with --fail-on low|medium|high|critical|never or in .hemlock.toml.

Changes and history

A full scan answers what is in the tree now. Two other commands answer more specific questions.

hemlock diff compares the working tree with a valid git revision, or compares two manifest files. It reports packages that were added, updated, or removed and scores only the additions and updates.

hemlock diff --since HEAD~1 --online
hemlock diff old/package-lock.json new/package-lock.json

hemlock history reads lockfile blobs from the mainline git history. It can show when a version entered and left the project, which tagged releases held it, and whether OSV now reports it as malware. The default limit is 200 manifest commits, and the report says when that window was truncated or the checkout is shallow.

hemlock history . --online
hemlock history . --package chalk

Existing projects

A baseline lets an established project adopt Hemlock without turning every old finding into a new CI failure:

hemlock baseline .
git add .hemlock-baseline.json

The baseline fingerprint includes the package version. Updating a dependency raises its findings again. Pass --ignore-baseline to include accepted findings in the active result.

CI

hemlock init creates .hemlock.toml and a GitHub Actions workflow. Pull requests are checked as diffs. Pushes scan the full tree, and the scheduled job also checks lockfile history. Generated workflows pin Hemlock and third-party Actions to exact versions or commits.

A small manual setup looks like this:

- run: pip install hemlock-scan
- run: hemlock diff --since origin/${{ github.base_ref }} --online --fail-on high

Exit codes are 0 when the threshold is clear, 1 when a finding reaches it, and 2 when the command cannot run.

Configuration

Configuration is optional. Put .hemlock.toml at the project root:

fail_on = "high"
fresh_days = 14

[[ignore]]
rule = "HEM201"
package = "esbuild"
reason = "compiles a reviewed native binary during installation"
expires = "2027-01-01"

Expired suppressions are reported instead of applied. Invalid configuration is rejected with a clear error rather than silently falling back to defaults.

Privacy and trust

Offline scans make no network requests. Online mode talks only to public npm, PyPI, and OSV services. It needs no account or API key, and sends no telemetry. Responses are cached for six hours under ~/.cache/hemlock. HTTP response and gzip expansion sizes are bounded, and cache entries are replaced atomically.

Releases are built from version-matched tags through PyPI trusted publishing. The workflow stores no PyPI token and attaches PEP 740 attestations. Its third-party Actions are pinned to full commit hashes.

Hemlock is also static by design. It reads package metadata and source but does not execute install hooks or imported code.

Limits

  • Hemlock is not a complete CVE scanner. Use osv-scanner, pip-audit, or npm audit for full known-vulnerability coverage.
  • It is not a sandbox and cannot prove a package is safe.
  • Provenance rules read the registry's verified result. They do not yet verify the Sigstore bundle locally.
  • Heuristics can produce false positives. Suppressions and baselines exist for reviewed cases.
  • Only npm and PyPI are supported.

Development

git clone https://github.com/xzycd/hemlock
cd hemlock
pip install -e ".[dev]"
ruff check hemlock tests
pytest -q

Tests use local fixtures and stubbed registry responses. The deliberately bad project under examples/compromised-app is inert: payloads are random text and all remote addresses use .invalid domains. See examples/README.md for the fixture map.

Releasing

Only a version-matched tag can publish. Update hemlock/__init__.py and the changelog, then create an annotated tag after the release commit reaches the default branch:

git tag -s v0.8.1 -m "hemlock v0.8.1"
git push origin v0.8.1

The workflow runs lint, tests, and the known-bad fixture before it builds and publishes the wheel and source archive.

License

MIT. See LICENSE.

Metadata

Release files for hemlock-scan 0.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hemlock-scan 0.8.1
File Size Uploaded
hemlock_scan-0.8.1.tar.gz 117.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hemlock-scan 0.8.1
File Interpreter ABI Platform
hemlock_scan-0.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 210.9 kB

Release files / hemlock_scan-0.8.1.tar.gz

Download URL hemlock_scan-0.8.1.tar.gz
Size 117.1 kB
Tags Source
SHA-256 checksum
How to use checksums
97e040caf475eff1d9dc45d5ff11516fdd985463a9e9b4ac0fe2d47af874ba2e
BLAKE2b-256 checksum
How to use checksums
c1480c822b604464e119a7b279feb3e88b99e7ed0bbe4c4aff3be121de1b5dfc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release files / hemlock_scan-0.8.1-py3-none-any.whl

Download URL hemlock_scan-0.8.1-py3-none-any.whl
Size 93.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eb5287516acdda149fb1077d1ef97de8f2ffb269f4b10666a43e475f71c532b1
BLAKE2b-256 checksum
How to use checksums
abe7b78d49de034c1dcec713a5ec51888016582601463b892185deb87dfea5c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.1 This release

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page