Honest answers about what phi does, what it doesn't, and how it co-exists with the rest of your toolchain.
preinstall / install /
postinstall in package.json is the single
most-abused attack vector in npm. A compromised package can do
anything as soon as you install it: download more code, exfiltrate
environment variables, drop a backdoor. npm install runs
these unconditionally.
phi treats lifecycle scripts as opt-in. Most packages don't need them;
the ones that do (esbuild, sharp, native-binding installs) you allow
per-package with --allow-scripts esbuild,sharp. This
single decision closes the most common supply-chain vector.
npm audit?
npm audit checks resolved dependencies against npm's
vulnerability database after installation. phi does that
plus:
npm audit draws from)
Yes — phi reads package.json like the others and writes a
separate phi.lock. You can keep your existing
package-lock.json / yarn.lock /
pnpm-lock.yaml for tools that expect them. phi never
modifies them.
phi installs into node_modules/ the same way the others
do, so any tool that resolves modules from
node_modules/ (your bundler, your test runner, Node
itself) just works.
REVIEW-flagged packages prompt you. Read the report card to see which detector fired and decide.
BLOCKED packages refuse to install entirely. If you genuinely trust the package, you can lower the verdict by inspecting and opening an issue — phi's defaults are conservative and detector tuning is a public process.
npm install for that specific
package while you investigate.
Network failures are non-fatal. phi prints a warning that advisory
data is unavailable and proceeds with just the static detectors. To
skip the network call entirely (e.g. air-gapped CI), pass
--no-advisories.
Yes. The scanner, lockfile, cache, and resolver all work identically
on Windows. node_modules/.bin/ shims are written as
.cmd files; workspace siblings link as junctions (no
admin privilege needed).
Pre-built binaries are published for darwin-arm64 and linux-arm64. The install script auto-detects.
Add a .npmrc to your project (or $HOME)
declaring the registry and an auth token. phi reads it the same way
npm does:
@my-org:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=${GITHUB_PAT}
The ${GITHUB_PAT} placeholder is substituted from your
environment, so you can commit the .npmrc safely.
It's a false positive. Defender's behavioral heuristics flag
nearly every unsigned Go-compiled CLI binary on first
encounter — same issue affects gh, cosign,
goreleaser, and most security tooling built in Go. The
pattern Defender flags (statically-linked binary, reads env vars,
spawns subprocesses, fetches from network) is also the pattern a wave
of Go-based Windows malware shares, so the heuristic catches both.
The phi install script verifies the binary's sha256 against the
release's checksums.txt before handing it to
Windows. If the install got past that check, the bytes are exactly
what we published — Defender is wrong about what they do, not lying
about a swap.
Unblock (run PowerShell as Administrator):
# Recommended: exempt the install location
Add-MpPreference -ExclusionPath "$env:LOCALAPPDATA\phi"
# Then re-run the installer
iwr -useb https://phi.philtechs.org/install.ps1 | iex
# Verify
phi version
Or, if you don't want a permanent exclusion, unblock the single binary after install:
Unblock-File "$env:LOCALAPPDATA\phi\phi.exe"
Run phi self-update. It downloads the latest GitHub
release archive for your platform, verifies the sha256 against
checksums.txt, and replaces the running binary in place.
--check reports whether an update is available without
installing it; --version v0.X.Y pins to a specific tag;
--yes skips the confirmation prompt.
phi update. That command
re-resolves your project's package.json dependencies.
phi self-update replaces phi itself.
If self-update can't run (no internet, write-protected install dir, or
you'd rather not trust it), re-running the install one-liner always
pulls the latest release:
curl -sSL https://phi.philtechs.org/install.sh | sh on
Linux/macOS,
iwr -useb https://phi.philtechs.org/install.ps1 | iex on
Windows.
npm audit fix?
Yes — phi audit fix. Three differences from npm's
version:
loadsh → lodash)
and deprecated-package swaps (vm2 →
isolated-vm, request →
undici, node-uuid → uuid, …).
--apply (safe fixes) or
--force (everything, including breaking changes).
--apply only takes the safe
set.
After applying, run phi install to materialize the new
versions.
Use phi ci (v0.4.1+) as your install step. It's sugar for
phi install --frozen-lockfile --yes — designed for
non-interactive environments where there's no stdin for phi's REVIEW
prompt. The frozen lockfile is the audit record: any package in
phi.lock was already reviewed and approved by a developer
locally, so prod can auto-approve REVIEW verdicts without hanging.
BLOCKED verdicts still abort unless --force is explicit.
# Dockerfile
COPY package.json phi.lock ./
RUN phi ci --omit=dev
# GitHub Actions
- run: phi ci --omit=dev
--omit=dev skips devDependencies (test
runners, linters, type defs, build tooling) — npm parity. Both
--omit=dev and --omit dev are accepted. Pass
-y/--yes standalone on
phi install or phi update when you want
auto-approve without the rest of phi ci's defaults.
If phi.lock is missing or doesn't cover
package.json, phi ci errors loudly — there's
no silent fallback to a fresh resolve, which is the whole point in CI.
npx equivalent?
Yes — phi x (in v0.3.0+). It's the same alias
phi exec has always been, but extended to fetch + scan +
run when the bin isn't already in node_modules/.bin:
phi x cowsay "hello phi" # fetch, scan, run
phi x prettier --write src/ # args go to the bin verbatim
phi x cowsay@1.5.0 # pin a specific version
phi x -p typescript tsc --version # bin name (tsc) ≠ package name (typescript)
Three meaningful differences from npx:
npx happily runs unscanned code from the registry —
historically the largest live attack surface in Node tooling.
npx runs
postinstall on the fetched package by default. Phi
treats the staged install the same way phi install
does — scripts off unless explicitly allowed.
$UserCacheDir/phi/run/<name>@<ver>/, not
into your project's node_modules. Repeat invocations of
the same resolved version skip the scan and run from cache.
For strict local-only behavior (the pre-v0.3.0 contract), pass
--no-install. --rescan invalidates the
cache; -y auto-approves review verdicts for CI /
scaffolding; -f overrides blocked verdicts.
phi create work?
phi create <framework> <name> scaffolds a new
project. Five frameworks ship in v0.2.0: react,
next, express, fastify,
nest. For four of them (everything except
express), phi installs the canonical scaffolder package
(create-vite, create-next-app,
fastify-cli, @nestjs/cli) into a temp
directory through phi's normal scan + extract pipeline (lifecycle
scripts off — same as phi install), then invokes the
scaffolder binary in your current directory. The temp directory is
wiped after a single use.
For express, no canonical npm scaffolder exists, so phi
ships a minimal Express template embedded in the binary itself via
Go's embed.FS — no network fetch is needed.
Pass-through args after -- reach the scaffolder verbatim,
and user flags override phi's baked-in defaults on flag-name
collisions:
phi create react my-app
phi create next my-site -- --typescript --app
phi create express my-server
git+https://..., file:./local,
https://x/y.tgz) — phi warns and skips them in
resolution
package-lock.json) — phi
reads phi.lock only
Likely either it's planned (open an issue), or it produces too many
false positives at the regex/AST level (phi is conservative about what
fires). The thirteen detectors are tuned against a real-world corpus
(including discord.js, undici,
grammy, ajv, fastify,
axios, …) to keep noise low; expanding the set requires
the same tuning work.
v0.6.x is feature-complete for typical Node.js
and Go projects: 19 total detectors (13 for the npm
ecosystem, 6 Go-specific), OSV layer for both ecosystems,
project scaffolding (phi create for Node
frameworks, phi init --go for new Go modules),
npx-equivalent phi x for npm and binary-install
(phi install <tool>@latest) for Go,
and built-in updates (phi self-update). The
real-world npm corpus and Go module corpus both install
cleanly with no false-positive blocks. Expect breaking
changes in 0.x as the detection model evolves — pin to a
specific version in CI.
As of v0.6.0, phi covers two:
package.json and any npm/yarn/pnpm
workspace shape including the workspace:
protocol. Writes a phi-owned phi.lock
alongside the npm one.
go.mod + go.sum (and
go.work for multi-module monorepos). Shells
out to Go's own MVS resolver for version selection;
phi owns the scan-before-disk and audit trail.
phi auto-detects the ecosystem from your cwd. When both
manifests are present in one directory, pass
--ecosystem=npm or
--ecosystem=go (or set
PHI_ECOSYSTEM) to disambiguate.
Other ecosystems (PyPI, crates, Maven) are out of scope for v0.6.x — they'd each need a per-language detector suite, and we'd rather ship two languages well than four poorly.
Pass --force (or -f) to
phi install / phi update. The scan still
runs and phi-report.json is still written — phi's audit
trail is preserved — but the install proceeds despite BLOCKED
verdicts. A loud warning lists every forced package. Use this when
you've reviewed the report and accepted the risk (e.g.
discord.js ships an internal _eval() method
that fires phi's Arbitrary Code Execution detector — a legitimate
detection on a legitimately trusted library).
Email bugs@phi.philtechs.org, or open an issue at github.com/philtechs-org/phi/issues. Especially valuable: false-positive reports against real-world packages, and missed-malware reports against known-bad packages. Include the package name, version, and what phi said vs what you expected.