Skip to content
Security Intermediate Tutorial

Block Malicious npm Packages in CI with Socket.dev

Wire Socket's supply-chain scanner into GitHub Actions to stop typosquats and install scripts before merge.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Jul 31, 2026 · 6 min read
Block Malicious npm Packages in CI with Socket.dev

What you'll build

A GitHub Actions job that runs Socket's supply-chain scanner on every push and pull request, posts a security report as a PR comment, and fails the check when a new dependency trips a blocking alert — known malware, typosquats, surprise install scripts, or packages published five minutes ago.

Prerequisites

  • A GitHub repository with a package.json (Socket also scans PyPI, Go, Maven, and other ecosystems from the same workflow — nothing below is npm-specific except the test package).
  • A Socket account — the free tier is enough. Sign up at socket.dev and create an organization if you don't have one.
  • Admin access to the repo (you'll add a secret and a workflow file).
  • Versions verified for this tutorial (July 2026): socketsecurity 2.5.6 on PyPI (requires Python ≥ 3.11 — the workflow installs 3.12 itself), actions/checkout@v4, actions/setup-python@v5, ubuntu-latest runners.

Socket also ships a GitHub App that does PR comments with zero config. The CI route in this tutorial is what you want when you need a required status check that actually blocks the merge button, or when app installs are locked down by your org.

1. Create a Socket API token

In the Socket dashboard, go to Settings → API Tokens and click Create API Token. Name it something like github-actions-ci, and grant the scopes Socket documents for CI use:

  • full-scans:list, full-scans:create — the actual scanning
  • repo:list, repo:create, repo:update — lets the CLI register the repo on first run
  • security-policy:read — so the CLI knows which alerts your org blocks
  • triage:alerts-list, triage:alerts-update and packages:list

Click Confirm, then Show key and copy the token. You won't see it again after leaving the page.

2. Store the token as a GitHub secret

From your repo checkout, with the GitHub CLI:

gh secret set SOCKET_SECURITY_API_TOKEN
# paste the token at the prompt

Or in the UI: Settings → Secrets and variables → Actions → New repository secret, name SOCKET_SECURITY_API_TOKEN. If you're rolling this out org-wide, set it once as an organization secret instead.

The CLI accepts the token from any of SOCKET_SECURITY_API_TOKEN, SOCKET_SECURITY_API_KEY, SOCKET_API_TOKEN, or SOCKET_API_KEY — pick one name and stay consistent.

3. Add the workflow

Create .github/workflows/socket.yml:

name: socket-security
run-name: Socket Security Scan

on:
  push:
    branches: ['**']
  pull_request:
    types: [opened, synchronize, reopened]
  issue_comment:
    types: [created]

concurrency:
  group: socket-scan-${{ github.ref }}-${{ github.sha }}
  cancel-in-progress: true

jobs:
  socket-security:
    permissions:
      issues: write
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install Socket CLI
        run: pip install socketsecurity --upgrade

      - name: Run Socket security scan
        env:
          SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}
          GH_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          PR_NUMBER=0
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            PR_NUMBER=${{ github.event.pull_request.number }}
          elif [ "${{ github.event_name }}" == "issue_comment" ]; then
            PR_NUMBER=${{ github.event.issue.number }}
          fi
          socketcli --target-path "$GITHUB_WORKSPACE" --scm github --pr-number "$PR_NUMBER"

This is Socket's documented pattern (from the socket-python-cli repo), lightly renamed. The pieces that matter: pull-requests: write and issues: write let the CLI post its report comment via GH_API_TOKEN; fetch-depth: 2 on PRs gives it the parent commit so it can diff newly added dependencies instead of re-litigating your whole lockfile; the issue_comment trigger lets Socket react to triage commands in PR comments. On PRs, socketcli exits 1 when a blocked alert fires and 0 otherwise, which is what turns the check red.

Commit and push this to your default branch. The first push run does a full baseline scan and registers the repo in your Socket dashboard.

4. Tune the security policy

The exit code follows your org's security policy, and the defaults are softer than you probably want. Out of the box, Known Malware is Block, Possible Typosquat Attack is only Warn, and Install Scripts is Ignore.

In the dashboard, open your org's Security Policy page and set each alert to Block, Warn, Monitor, or Ignore. For the threats this pipeline is meant to stop:

  • Possible Typosquat Attack (didYouMean) → Block
  • Install Scripts (installScripts) → Warn (Block if you're strict — expect noise from native modules)
  • Recently Published (recentlyPublished) → Warn, to catch dependencies pushed to the registry hours ago

Only Block-level alerts fail the check; Warn shows up in the PR comment for a human to judge.

5. Open a PR that adds a risky dependency

Test the gate with a package that legitimately runs a postinstall script:

git checkout -b test-socket-gate
npm install puppeteer
git add package.json package-lock.json
git commit -m "test: add puppeteer to trip Socket install-script alert"
git push -u origin test-socket-gate
gh pr create --fill

Puppeteer's postinstall downloads a Chrome binary, so it reliably triggers the installScripts alert on the diff.

Verify it works

Within a couple of minutes the PR gets a comment from github-actions titled with a Socket security report: a table of the newly added packages, the alerts each one raised (you should see Install scripts on puppeteer), and a link to the full scan in your dashboard.

Check the status from the CLI:

gh pr checks

Expected output with Install Scripts at Warn:

socket-security  pass  1m12s  https://github.com/you/repo/actions/runs/...

Flip Install Scripts to Block in the policy, push an empty commit to re-trigger, and the same check reports fail — the job log ends with Process completed with exit code 1. That's your merge gate working. Finish by making it mandatory: Settings → Branches → Branch protection rules, add socket-security as a required status check.

Troubleshooting

  • Unauthorized / HTTP 401 from the API. The token env var is empty or wrong. Check the secret name matches the workflow exactly, and remember that PRs from forks don't receive repository secrets — fork PRs will fail auth unless you use the GitHub App for external contributors.
  • Resource not accessible by integration when posting the PR comment. The default GITHUB_TOKEN lacks write access. Make sure the permissions: block from step 3 is present in the job, and that your repo/org settings don't force the token to read-only (Settings → Actions → General → Workflow permissions).
  • ERROR: Could not find a version that satisfies the requirement socketsecurity during pip install. You're on Python < 3.11 (current socketsecurity releases require ≥ 3.11, so older interpreters see no installable version). Keep the setup-python step with python-version: '3.12' rather than relying on the runner's system Python.
  • A package with install scripts sailed through unflagged. That's the default policy, not a broken scan — Install Scripts ships as Ignore. Set it to Warn or Block in the Security Policy page (step 4), then re-run the job.

Next steps

  • Shift the same protection left onto laptops: the npm Socket CLI (npm install -g socket) wraps your package manager, so socket npm install <pkg> vets packages before they ever touch node_modules, and socket scan create --report gives you the same policy gate in any CI system.
  • Export findings to GitHub code scanning with socketcli --sarif-file results.sarif and upload via github/codeql-action/upload-sarif.
  • Enable reachability analysis (--reach) to prioritize alerts in code paths you actually execute.
  • Read Socket's alert catalog to decide Block/Warn/Monitor for the other ~100 alert types — obfuscated code, telemetry, unstable ownership, and friends.

Sources & further reading

  1. Socket for GitHub Actions — docs.socket.dev
  2. Socket Security CLI for CI/CD (socket-python-cli) — github.com
  3. Create Socket API Key for CI/CD — docs.socket.dev
  4. Security Policy (Default Enabled Alerts) — docs.socket.dev
  5. Socket Alert Types — socket.dev
  6. socketsecurity on PyPI — pypi.org
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading