When you run pip install flask or npm install express, you trust that the package you get today is the same one everyone else has been using. But what if it isn't?
Supply chain attacks on package registries are real and increasingly common. An attacker compromises a maintainer's account, pushes a malicious version, and within minutes thousands of developers install it. By the time it's discovered and pulled — hours or days later — the damage is done.
safe-pin is a set of small scripts that add two simple safety rules to your dependency management:
-
Only install versions that have been public for at least 7 days. This gives the community, security scanners, and registry maintainers time to catch and remove malicious releases before you ever touch them.
-
Verify every download with a cryptographic hash. Even if a registry is compromised or a CDN serves the wrong file, the hash check will catch it.
That's it. No new tools to learn, no infrastructure to set up. Just a thin safety layer on top of pip, npm, and Composer.
Each script lives in its own folder with a SKILL.md file, making them ready to use as Claude Code skills. Copy the folders to ~/.claude/skills/:
cp -r pip npm composer ~/.claude/skills/The skills can be invoked explicitly with /pip, /npm, or /composer, or Claude can pick them up automatically when it detects you're installing packages.
For the best results, add the text from the attached CLAUDE.md to your ~/.claude/CLAUDE.md:
## Supply chain safety — ALWAYS ENFORCE
When installing, adding, or updating dependencies with pip, npm, or composer:
- ALWAYS use the corresponding skill (`/pip`, `/npm`, `/composer`) to pin versions securely.
- NEVER run `pip install`, `npm install`, `composer require`, or equivalent with unpinned versions.
- Every dependency must be pinned to an exact version that is at least 7 days old, with hash verification.
- If you are about to install a package without using the skill, stop and use the skill first.Instead of this:
pip install flask requests # latest version, no verification
npm install express # might include a release published 5 minutes ago
composer require monolog/monolog # trusting the registry blindlyYou do this:
# Python
python safe_pip_pin.py flask requests -o requirements.txt
pip install --require-hashes -r requirements.txt
# Node.js
node safe_npm_pin.js express --output package.json
npm install && npm ci
# PHP
python safe_composer_pin.py monolog/monolog -o composer.json
composer update && composer installEach script queries the registry, finds the latest version that's at least 7 days old, and pins it. The lock files and hashes take care of the rest.
Most malicious packages are discovered and removed within hours or days. A 7-day quarantine period means you're never the first to try a new release — you let the ecosystem vet it first. It's a simple heuristic, not a guarantee, but it eliminates the most common "smash-and-grab" attacks where a compromised package is published, collects credentials, and gets pulled shortly after.
If a critical security patch drops and you need it immediately, you can override the age rule — but you do it consciously, after checking the changelog and diffing the source. The scripts will tell you clearly when no version meets the age threshold.
This approach is defense-in-depth, not a silver bullet.
- Long-running compromises — If a trusted maintainer's account is hijacked and the malicious code is subtle enough to evade detection for weeks (like the xz-utils backdoor), the 7-day rule won't help.
- Transitive dependencies — The age filter applies to your top-level dependencies. Transitive dependencies (packages pulled in by your dependencies) are resolved normally by the package manager. Once resolved, they are locked and hash-verified on subsequent installs — but their initial resolution doesn't have the age filter.
Requirements: Python 3.9+, pip-tools (auto-installed if missing)
python safe_pip_pin.py requests flask numpy -o requirements.txtWhat happens:
- For each package, the script queries the PyPI JSON API and finds the latest stable version (filtering out dev/alpha/beta/RC releases) published ≥ 7 days ago.
- Writes a
requirements.inwith the pinned top-level versions. - Runs
pip-compile --generate-hashesto resolve the full dependency tree and produce arequirements.txtwith sha256 hashes for every package — including transitive dependencies.
Install with hash verification:
pip install --require-hashes -r requirements.txtOptions:
| Flag | Description |
|---|---|
-o, --output |
Output file (default: requirements.txt) |
--min-age |
Minimum version age in days (default: 7) |
--no-compile |
Only write requirements.in, skip pip-compile |
Requirements: Node.js, npm
node safe_npm_pin.js express lodash axios --output package.json
npm install
npm ciWhat happens:
- For each package, the script runs
npm view <package> time --jsonand finds the latest stable version (filtering out pre-releases like dev, alpha, beta, rc) published ≥ 7 days ago. - Writes the exact version (no
^or~) intopackage.json— intodependenciesby default, ordevDependencieswith--dev. - If the package already exists in the other section (dependencies vs devDependencies), removes it to avoid duplicates.
Then npm install generates package-lock.json with sha512 integrity hashes for the full dependency tree. Use npm ci for all subsequent installs — it verifies every hash.
Options:
| Flag | Description |
|---|---|
-o, --output |
Output file (default: package.json) |
--min-age |
Minimum version age in days (default: 7) |
--dev, -D |
Write to devDependencies instead of dependencies |
Requirements: Python 3.9+ (for the script), Composer (for resolving)
python safe_composer_pin.py monolog/monolog guzzlehttp/guzzle -o composer.json
composer update
composer installWhat happens:
- For each package, the script queries the Packagist JSON API and finds the latest stable version (filtering out dev/alpha/beta/RC releases) published ≥ 7 days ago.
- Writes the exact version into
composer.json. - If an existing
composer.jsonexists at the output path, merges into it.
Then composer update resolves the full dependency tree and writes composer.lock with dist references and shasums. Use composer install for all subsequent installs.
Note: not all Packagist packages provide a dist shasum. When missing, composer.lock still records the exact git commit reference.
Options:
| Flag | Description |
|---|---|
-o, --output |
Output file (default: composer.json) |
--min-age |
Minimum version age in days (default: 7) |
safe-pin/
├── README.md
├── CLAUDE.md
├── pip/
│ ├── SKILL.md # Claude Code skill definition
│ └── safe_pip_pin.py # Script (Python, stdlib + pip-tools)
├── npm/
│ ├── SKILL.md # Claude Code skill definition
│ └── safe_npm_pin.js # Script (Node.js, stdlib only)
└── composer/
├── SKILL.md # Claude Code skill definition
└── safe_composer_pin.py # Script (Python, stdlib only)
All scripts use only standard library modules (plus pip-tools for the pip script) and have zero other dependencies.
Sometimes you need a version that's less than 7 days old — typically a critical security patch. All three scripts will exit with a clear error when no version meets the age threshold. In that case:
- Check the changelog and release notes.
- Diff the source between the previous and new version.
- Confirm the publisher is the expected maintainer.
- Pin the version manually and add a comment explaining the override.
MIT