Skip to content

postinstall scripts blocked by pnpm v10+ security policies #166

@leggetter

Description

@leggetter

Problem

The current distribution approach for hookdeck-cli relies exclusively on postinstall scripts via go-npm, which creates a poor developer experience with modern package managers, particularly pnpm v10+.

Impact on Users

When users install hookdeck-cli with pnpm v10+, they encounter this error:

$ pnpm install -D hookdeck-cli
# Install succeeds but shows warning about ignored build scripts

$ pnpm hookdeck
./node_modules/.bin/hookdeck: cannot execute binary file

Root Cause: pnpm v10+ blocks lifecycle scripts by default for security reasons. The package ships with a Linux x86-64 binary as a placeholder, and the postinstall script is supposed to download the correct platform-specific binary. When the script is blocked, users are left with the wrong binary.

Current Workarounds (Poor UX)

Users must manually configure pnpm before installation:

Option 1: Create pnpm-workspace.yaml before installing:

onlyBuiltDependencies:
  - hookdeck-cli

Option 2: Add to package.json before installing:

{
  "pnpm": {
    "onlyBuiltDependencies": ["hookdeck-cli"]
  }
}

Option 3: Post-install approval (multi-step):

pnpm install -D hookdeck-cli  # Fails
pnpm approve-builds           # Interactive selection required
pnpm rebuild hookdeck-cli     # Finally works

None of these are acceptable for a good CLI installation experience.

Reproduction Steps

  1. Create new project with pnpm v10+
  2. Run pnpm install -D hookdeck-cli
  3. Try to run pnpm hookdeck --version
  4. Result: cannot execute binary file error

Environment:

  • pnpm v10.19.0
  • macOS (darwin arm64)
  • hookdeck-cli v1.1.0

Recommended Solution

Switch to the optionalDependencies pattern used by industry-standard tools like esbuild, swc, and Sentry CLI. This approach:

  • ✅ Works with pnpm v10+ without configuration
  • ✅ No postinstall scripts required
  • ✅ Works with --ignore-scripts flag
  • ✅ Faster installs (only downloads ~1-2MB for one platform)
  • ✅ Better offline support (binaries in npm registry)
  • ✅ Can still use postinstall as optional fallback

Implementation Pattern

1. Create platform-specific packages:

Publish separate packages for each platform:

@hookdeck/cli-darwin-arm64
@hookdeck/cli-darwin-x64  
@hookdeck/cli-linux-x64
@hookdeck/cli-linux-arm64
@hookdeck/cli-win32-x64
etc.

Each platform package contains:

{
  "name": "@hookdeck/cli-darwin-arm64",
  "version": "1.1.0",
  "os": ["darwin"],
  "cpu": ["arm64"],
  "bin": {
    "hookdeck": "./hookdeck"
  },
  "files": ["hookdeck"]
}

2. Update main package to use optionalDependencies:

{
  "name": "hookdeck-cli",
  "version": "1.1.0",
  "bin": {
    "hookdeck": "./bin/hookdeck.js"
  },
  "optionalDependencies": {
    "@hookdeck/cli-darwin-arm64": "1.1.0",
    "@hookdeck/cli-darwin-x64": "1.1.0",
    "@hookdeck/cli-linux-x64": "1.1.0",
    "@hookdeck/cli-linux-arm64": "1.1.0",
    "@hookdeck/cli-win32-x64": "1.1.0",
    "@hookdeck/cli-win32-arm64": "1.1.0"
  }
}

3. Create JavaScript wrapper to detect and run correct binary:

#!/usr/bin/env node
// bin/hookdeck.js
const { execFileSync } = require('child_process');
const { existsSync } = require('fs');
const path = require('path');

function getPlatformBinary() {
  const platform = process.platform;
  const arch = process.arch;
  const packageName = `@hookdeck/cli-${platform}-${arch}`;
  
  try {
    const binaryPath = require.resolve(`${packageName}/hookdeck`);
    if (existsSync(binaryPath)) {
      return binaryPath;
    }
  } catch (e) {
    // Platform package not installed
  }
  
  // Fallback: could still use postinstall download as backup
  const fallbackPath = path.join(__dirname, 'hookdeck');
  if (existsSync(fallbackPath)) {
    return fallbackPath;
  }
  
  console.error(`Unsupported platform: ${platform}-${arch}`);
  console.error('Please report this issue at https://github.com/hookdeck/hookdeck-cli/issues');
  process.exit(1);
}

try {
  const binaryPath = getPlatformBinary();
  execFileSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
} catch (error) {
  process.exit(error.status || 1);
}

4. (Optional) Keep postinstall as fallback:

The postinstall script can remain as a fallback mechanism for edge cases, but the CLI will work without it:

{
  "scripts": {
    "postinstall": "node scripts/install-fallback.js"
  }
}

How npm/pnpm Handle This

When a user runs pnpm install hookdeck-cli:

  1. npm/pnpm reads the optionalDependencies list
  2. Automatically filters based on current platform's os and cpu
  3. Only installs the matching platform package (e.g., @hookdeck/cli-darwin-arm64)
  4. The wrapper script detects and executes that binary
  5. No postinstall script needed - everything works out of the box

Migration Benefits

  • Immediate: Users can install without any pnpm configuration
  • Security: Works with --ignore-scripts flag
  • Performance: Only downloads 1-2MB instead of running download script
  • Reliability: Binaries are part of npm registry (better caching, offline support)
  • Compatibility: Works with all package managers (npm, yarn, pnpm, bun)

References

Additional Context

This issue affects all users on pnpm v10+ (released in 2024), which is becoming the default in many environments. The current workarounds require users to understand pnpm's security model and manually configure their workspace before installation - this is not acceptable for a CLI tool that should "just work."

The optionalDependencies pattern is now the industry standard for distributing native binaries via npm. Making this change would dramatically improve the developer experience and align hookdeck-cli with best practices used by major tools in the ecosystem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions