Three recipes, in increasing order of how much they ask of your team. Start with the first.
Whichever you pick, install first. scriptgate reads node_modules, so a job
that scans before npm ci finds nothing and reports success.
name: supply chain
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
install-scripts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx scriptgate checkFails when a dependency runs code at install time that nobody approved, when an
approval is pinned to a version that is no longer installed, or when a script
scores high or above.
A note on npm ci under npm v12: it will not run unapproved install scripts
either, so a job that passes without an allowlist may be building a project whose
native addons were never compiled. That is the failure scriptgate check
exists to make loud.
Findings become alerts with a lifecycle: they appear on the pull request that introduced them, they can be dismissed with a stated reason, and they close themselves when fixed.
permissions:
contents: read
security-events: write
jobs:
install-scripts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Analyse install scripts
run: npx scriptgate scan --sarif > scriptgate.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scriptgate.sarif
category: scriptgatecontinue-on-error is deliberate here: scan exits 0 whether or not it finds
anything, but the upload step should still run if a future version changes that.
Findings are anchored to package.json, because that is the only file in the
repository you can edit to resolve them — the offending script lives in
node_modules, which is not committed.
What a reviewer looking at a dependency bump actually wants: the delta in install-time responsibility, with the settled decisions collapsed out of the way.
permissions:
contents: read
pull-requests: write
jobs:
install-scripts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build the report
run: npx scriptgate scan --markdown > report.md
- name: Comment
uses: actions/github-script@v7
with:
script: |
const body = require('node:fs').readFileSync('report.md', 'utf8');
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
const mine = comments.data.find((c) =>
c.user.type === 'Bot' && c.body.includes('Install-script review'));
if (mine) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}Updating the existing comment rather than adding one keeps a long-running pull request readable.
Do not run this on pull_request_target. That trigger gives the workflow
write permissions and repository secrets while checking out code from the fork —
and the whole subject of this tool is that a dependency's install script is code
you have not read. Use pull_request, which is what the recipe above does.
--markdown also renders in a job summary, which costs no permissions at all:
- run: npx scriptgate scan --markdown >> "$GITHUB_STEP_SUMMARY"Nothing above is GitHub-specific except the SARIF upload and the comment.
GitLab
install-scripts:
image: node:22
script:
- npm ci
- npx scriptgate checkA pre-commit or pre-push hook — cheap, because the gate is a static read with no network:
npx scriptgate check --quiet| Code | Meaning |
|---|---|
| 0 | clean |
| 1 | the gate failed, or explain found no such package |
| 2 | bad usage, or a project that could not be read |
Code 2 is worth separating in a pipeline: it means the tool could not do its job
— usually a missing node_modules — rather than that your dependencies are
fine. Treating it as a pass is the mistake to avoid.
check writes its report to stdout and its verdict to stderr, so this
produces a clean JSON file and still shows the reason in the log:
npx scriptgate check --json > scriptgate.jsonColour disables itself automatically when the output is not a terminal. NO_COLOR=1
forces it off; FORCE_COLOR=1 forces it on for a CI system that renders ANSI.