script-jail
v0.2.4
Published
Backend-isolated install auditor for npm/pnpm/yarn lifecycle scripts.
Downloads
1,150
Readme
script-jail
Backend-isolated audit of npm, pnpm, and Yarn lifecycle scripts. script-jail
turns dependency install behavior into a deterministic .script-jail.lock.yml
so suspicious file, env, process, native-addon, and network behavior is visible
in code review.
What It Does
When package-lock.json, pnpm-lock.yaml, or yarn.lock changes, the GitHub
Action re-runs the install through a Linux audit backend. backend: auto tries
Firecracker first, then Docker, then a bare Linux executor. Each backend runs
the same guest agent with strace plus the LD_PRELOAD shim and Node preloads.
The generated .script-jail.lock.yml records lifecycle-script reads and writes
outside the owning package directory, env-var reads, protected-secret accesses,
execve, audit-bypass attempts, legacy dlopen quarantine events, and blocked
network attempts. In check mode, any non-canonical lockfile diff fails the PR
with a unified diff.
Status
Released. script-jail and its three platform packages are published to npm,
and each GitHub release carries the full artifact set. The Action and CLI
verify every downloaded rootfs, kernel, shim, and Docker image against the
hash manifest committed in src/action/artifact-manifest.ts before use.
Backends: Firecracker, Docker, and bare Linux in CI; a Virtualization.framework
VM (vz) and a native no-VM bare backend on macOS.
GitHub Action
Use check mode on pull requests. Commit .script-jail.lock.yml when an
intentional dependency change alters the audit.
name: script-jail
on:
pull_request:
paths:
- package-lock.json
- pnpm-lock.yaml
- yarn.lock
- .script-jail.yml
- .script-jail.lock.yml
jobs:
audit-install:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: Brooooooklyn/scriptjail@<pinned-tag>
with:
mode: check
config: .script-jail.yml
lock: .script-jail.lock.yml
backend: auto
spoof-platform: linux
# Defaults to the runner CPU architecture when omitted.
# spoof-arch: arm64
cache-firecracker: "true"backend: auto prefers Firecracker when Linux, /dev/kvm, and tap0 are
available. On GitHub-hosted ubuntu-24.04-arm, KVM is unavailable, so the
parity workflow normally falls through to Docker.
Configuration
.script-jail.yml defines which files and env vars should be hidden from
lifecycle scripts and marked as protected in the lockfile:
protected:
files:
- ~/.ssh/**
- ~/.npmrc
- $REPO/.env
- $REPO/.env.*
env:
- NPM_TOKEN
- NODE_AUTH_TOKEN
- GITHUB_TOKEN
spoof:
platform: linux
arch: arm64The Action inputs spoof-platform and spoof-arch override the config for
that run without modifying the file on disk.
Lockfile Example
The generated .script-jail.lock.yml is grouped by package identity and
lifecycle stage. Empty lists are intentional: they keep the schema stable and
make newly observed behavior obvious in diffs. Two extra lists, audit_bypass
and env_tamper, appear only when populated — a clean run renders neither.
schema_version: 1
manager: pnpm
manager_lockfile_sha256: "..."
node_version: 24.15.0
generated_at: 2026-05-28T08:00:00.000Z
packages:
[email protected]:
lifecycle:
postinstall:
external_reads:
- <HIDDEN> $HOME/.npmrc
- $REPO/package.json
escaped_writes:
- <CROSS_PACKAGE> $NODE_MODULES/victim-package/package.json
- $TMPDIR/<hash>/build.log
env_read:
- <HIDDEN> NPM_TOKEN
- PATH
spawn_attempts:
- node postinstall.js
spawn_blocked:
- <ENOENT> gcc -c native.c
dlopen_attempts: []
network_attempts:
- <BLOCKED> connect 198.51.100.7:443macOS CLI
On macOS 14 or newer, the CLI audits installs through one of two backends:
vz(default on Apple Silicon) — boots the same Linux guest agent in a lightweight VM through Apple's Virtualization.framework.bare— runs natively with a Mach-ODYLD_INSERT_LIBRARIESshim and bundled bash/coreutils substitutes, no VM. Network activity is recorded but not blocked on this backend; SIP-protected tools that cannot be instrumented are marked<AUDIT_BLIND>.
pnpm exec script-jail init # create .script-jail.lock.yml
pnpm exec script-jail update # overwrite .script-jail.lock.yml
pnpm exec script-jail check # diff against the committed lockfile
pnpm exec script-jail check --backend bare # native audit, no VMWhen no subcommand is provided, the CLI defaults to init if the lockfile does
not exist and check if it does. The runtime artifacts (VZ helper, VZ kernel,
rootfs, and the .so/.dylib shims) ship inside @script-jail/darwin-arm64;
a repo checkout resolves them from images/ instead.
Installation and packaging
The main script-jail npm package is JS-only: it ships dist/cli.cjs, the
guest agent (dist/guest-agent.cjs), the Node preloads, and this README — no
runtime artifacts. The platform-specific runtime payloads live in three
optional dependency packages, one per supported host:
@script-jail/darwin-arm64— VZ helper (script-jail-vm), VZ kernel (vmlinux-vz-arm64),libscriptjail-arm64.so, a compressed Ubuntu 24.04 arm64 rootfs, and the bare-backend binaries (libscriptjail-arm64.dylib,bash-arm64,coreutils-arm64).@script-jail/linux-x64—libscriptjail.soand a compressed Ubuntu 24.04 x64 rootfs.@script-jail/linux-arm64—libscriptjail-arm64.soand a compressed Ubuntu 24.04 arm64 rootfs.
Each optional package declares matching os/cpu, so npx script-jail (or any
install) automatically pulls only the @script-jail/<os>-<arch> that matches
the current host and skips the rest. Intel macOS (darwin-x64) is not
supported. In a repo checkout the CLI instead resolves these artifacts from
images/ as a development fallback. On first run, the CLI expands the
compressed rootfs into a sparse cache under ~/Library/Caches/script-jail on
macOS, or under SCRIPT_JAIL_CACHE_DIR (falling back to the system temp dir) on
Linux.
How It Works
Install auditing is split into two phases. Phase A runs the package-manager
fetch with network enabled and no audit output. Phase B disables network and
runs lifecycle scripts under strace, the Rust LD_PRELOAD shim, and Node
preloads. The guest normalizes attributed events into a byte-stable YAML
lockfile. See docs/design.md for rationale and
docs/architecture.md for the control flow.
Why Backend Isolation
A pure-JS install sandbox cannot close the important gaps: native addons and
child_process reach the kernel, libc/libuv env reads bypass a process.env
Proxy, and bun does not honor the Node preload model. Firecracker is the
strongest Linux isolation boundary; Docker and bare mode keep the same
syscall/preload audit available on runners without KVM.
Docs
- Design - rationale, threat model, and tradeoffs.
- Architecture - host/guest split and audit pipeline.
- Development - build, release, and CI conventions.
- Releasing - first-release runbook and version-bump sequence.
- Testing - Vitest projects, fixtures, and e2e workflows.
- Divergence - cross-host parity limits.
- Parity testing - Linux/macOS parity workflow.
- N-API preload research - shared-state probe for a future trusted startup signal.
License
MIT
