phi auto-detects the ecosystem from your cwd:
package.json → npm mode,
go.mod or go.work → Go mode. Each
tab below documents that ecosystem's surface; the security
model, lockfile schema, and trust-override flags are
identical across both.
In a project with a package.json:
phi install
phi resolves the full transitive tree and scans every package before
extraction. SAFE packages install first. REVIEW packages install after
approval (or automatically with --yes), and accepted
REVIEW packages recorded in phi.lock are not re-prompted
on later installs. BLOCKED packages are skipped by default and only
materialized with --force. Unresolvable packages are
listed under skipped in phi-report.json while
the rest of the install continues.
To add a new dep:
phi install chalk@^5.0.0
Updates the dependencies field, scans, installs.
To audit without installing:
phi audit
| Command | Aliases | Behavior |
|---|---|---|
phi init |
— |
Create package.json + .gitignore +
README.md. --yes for non-interactive;
--force to overwrite.
|
phi create <fw> <name> |
— |
Scaffold a new project. fw: react,
next, express, fastify,
nest. Pass-through args after --. See
§ 02.5.
|
phi install [pkg…] |
i, a |
Best-effort scan and install. Args are added to
package.json (or to devDependencies
with --save-dev). SAFE packages install even when
another requested package cannot resolve; REVIEW/BLOCKED
packages are gated after the safe extraction step.
|
phi update [pkg…] |
u |
Re-resolve and install fresh, ignoring phi.lock.
|
phi ci |
— |
Production install for non-interactive environments
(Docker, GitHub Actions). Sugar for
phi install --frozen-lockfile --yes. See § 02.8.
|
phi remove <pkg…> |
rm |
Drop from package.json, prune from lockfile +
node_modules.
|
phi audit |
— |
Resolve + scan, no extraction. Writes
phi-report.json.
|
phi audit fix [--apply|--force] |
— |
Propose fixes for fixable issues. Preview by default;
--apply writes safe fixes to
package.json; --force allows breaking
changes (replacement packages, major-version bumps). See § 02.6.
|
phi check |
verify |
Verify package.json, phi.lock, and
node_modules. Detects lockfile drift, missing
packages, and changed installed files when the cached tarball is
available.
|
phi do <script> [args…] |
d |
Run a script from package.json with
node_modules/.bin on PATH. Pre/post hooks
honored. --filter <glob> (repeatable)
runs the script in every matched workspace with a
per-workspace header. See § 02.15.
|
phi exec <pkg>[@<ver>] [args…] |
x |
Run a binary;
auto-fetch and scan like npx if not in
node_modules/.bin. Flags:
-p <pkg>, --no-install,
-y, --rescan, -f. See §
02.7.
|
phi dev | build | start | … |
— |
Direct shortcuts to phi do <name>. Available:
dev, build, start,
test, lint, preview,
prod.
|
phi outdated |
— |
Show direct deps with newer versions available, color-coded by
major bump. --filter <glob> for per-workspace
tables in monorepos. Live spinner during packument checks. See
§ 02.13.
|
phi upgrade-interactive
|
ui |
Walk every outdated direct dep with a per-package
y/n/a/q prompt. --allow-major to
include major bumps; --yes for non-interactive
bulk upgrades. Preserves the existing range prefix
(^, ~, exact). See § 02.13.
|
phi view <pkg>[@<ver>] |
info |
Packument inspector with a security scan by default —
runs phi's detectors + OSV check on the resolved tarball and
prints score / verdict / detections / advisories alongside the
standard metadata. --no-scan for the fast
metadata-only path; field-targeted queries
(phi view react license) auto-skip the scan.
--json includes a "scan" object. See
§ 02.10.
|
phi ls [pkg] |
list |
Print the dependency tree from phi.lock in
npm-style ASCII. --depth N caps depth;
--prod drops devDependencies;
--json emits a stable JSON shape; bare
[pkg] prunes to only branches that reach a match.
See § 02.11.
|
phi why <pkg> |
— |
Print every dep chain from a root that reaches
<pkg>.
|
phi link [pkg] |
— |
Global package linking with Windows directory junctions
(no admin required). In a package directory: registers it
under ~/.config/phi/links/<name>. In a
consumer directory: phi link <name> creates
node_modules/<name> pointing at the
registered target. --list shows registered
links. See § 02.12.
|
phi unlink [pkg] |
— | Reverse either side based on cwd: in a registered package directory, drops the global entry; in a consumer with a link present, removes the symlink/junction. |
phi pkg get|set|delete|fix
|
— |
Read and write package.json with dot-path
semantics. Preserves indent style, BOM, and key order on
round-trip. Conservative coercion:
true/false/null/literal
{}/[] become JSON; bare numeric
strings stay strings (so engines.node = "20"
round-trips cleanly). See § 02.14.
|
phi config get|set|delete|list
|
ls |
Read and write ~/.config/phi/config
(or %APPDATA%\phi\config on Windows) with comment
and blank-line preservation. Secret-shaped keys
(containing token, password,
secret, _authtoken) are
auto-redacted when stdout is a terminal; non-tty pipes
get raw values. See § 02.14.
|
phi cache stat |
— |
Prints both cache sections — the tarball cache (reused
across installs) and the stage cache
($XDG_CACHE_HOME/phi/runs/, populated by
phi x and similar one-shot binaries).
|
phi cache clean |
— |
Prune the cache. --older-than 30d default;
--all wipes everything;
--list prints contents without deleting;
--runs targets the stage cache instead of
tarballs; --all-caches applies the prune to
both in one pass.
|
phi self-update |
— |
Update phi itself to the latest GitHub release.
--check just reports availability;
--version v0.X.Y pins; --yes skips the
confirmation prompt. Distinct from phi update which
updates project dependencies.
|
phi version |
— | Print version, e.g. phi 0.6.4. |
phi help |
— | Print available commands and flags. |
phi audit fixNew in v0.2.3. Propose actionable fixes for direct dependencies. Three sources, in order of confidence:
loadsh
→ lodash). Always safe — different package, same
intent.
fixed in version. Same-major bumps are safe;
cross-major bumps are flagged as breaking.
vm2
→ isolated-vm, request →
undici, node-uuid → uuid, …).
Always breaking — public API differs.
Three modes:
phi audit fix # preview only — no filesystem changes
phi audit fix --apply # write SAFE fixes to package.json
phi audit fix --force # write EVERY fix, including breaking changes
After applying, run phi install to materialize the new
versions.
npm audit fix: phi handles
typosquats and deprecations (not just CVE bumps); phi shows the
proposal before applying; phi explicitly distinguishes safe-to-apply
fixes from breaking changes — they're rendered in separate color-coded
groups.
phi x (npx-equivalent)
New in v0.3.0. phi x (the alias for
phi exec) is now a drop-in replacement for
npx: if the bin isn't already in
node_modules/.bin, phi resolves the providing package,
runs every detector + the OSV layer over the transitive tree, then
runs the bin from a per-version cache. Repeat invocations of the same
resolved version skip the scan and run instantly. The user's
node_modules is never touched.
phi x cowsay "hello phi" # fetch + scan + run, like npx
phi x prettier --write src/ # args after the pkg go to the bin verbatim
phi x cowsay@1.5.0 # pin a version (always fetches)
phi x -p typescript tsc --version # bin name (tsc) ≠ package name (typescript)
phi x -y create-react-app my-app # auto-approve review verdicts (CI / scaffold)
phi x --no-install eslint . # strict-local: fail if not already installed
phi x --rescan cowsay # invalidate the cached scan, redo it
| Flag | Behavior |
|---|---|
-p, --package <pkg> |
Package name when bin name differs (npx parity for
typescript ↔ tsc).
|
--no-install |
Strict-local: fail if the bin isn't already in
node_modules/.bin. The pre-v0.3.0 behavior.
|
-y, --yes |
Auto-approve review-verdict packages instead of prompting.
Blocked still aborts unless --force.
|
--rescan |
Invalidate the cached scan-passed marker for the resolved version, refetch + rescan. |
-f, --force |
Override blocked verdicts and proceed anyway. Loud warning printed. |
Cache layout:
$UserCacheDir/phi/run/<name>@<version>/
node_modules/<name>/… # extracted package + transitive deps
node_modules/.bin/<bin-name> # shim, on PATH for the bin's lifetime
.phi-scan-passed # marker; presence means "trusted, run"
phi x differs from npx: every
package goes through phi's full scanner + advisory pipeline before the
bin runs — the same gate that protects phi install.
Lifecycle scripts stay off (npx runs them by default). Blocked
verdicts abort unless --force; review verdicts prompt
unless -y. Pinned versions and scoped packages match npx
semantics; git/GitHub/tarball URLs are not yet supported
(registry-only for now).
phi ci
New in v0.4.1.
Non-interactive environments (Docker builds, GitHub Actions) have no
stdin for phi's REVIEW prompt — without help, an install hangs
forever. phi ci solves this without losing the audit:
phi ci # production: --frozen-lockfile + --yes preset
phi ci --omit=dev # also skip devDependencies (npm parity)
phi ci --force # explicit override for blocked verdicts
phi ci --no-advisories # offline / air-gapped builds
phi ci is sugar for
phi install --frozen-lockfile --yes. The frozen lockfile
is the audit record — any package in phi.lock was already
reviewed and approved by a human during their local
phi install. Prod can therefore auto-approve REVIEW
verdicts without a prompt. BLOCKED verdicts still abort unless
--force is explicit. A missing or drifted lockfile errors
clearly (phi.lock not found /
phi.lock is out of sync with package.json) — there's no
silent fallback to a fresh resolve.
phi ci is built on two new flags that work standalone
too:
| Flag | Behavior |
|---|---|
-y, --yes |
Auto-approve REVIEW verdicts instead of prompting. BLOCKED still
aborts unless --force. Works on
phi install, phi update,
phi ci.
|
--omit=dev |
Skip devDependencies during resolution — npm
parity. Useful for prod-deploy bundles where test runners,
linters, type defs aren't needed. Both
--omit=dev and --omit dev accepted.
|
# Dockerfile
COPY package.json phi.lock ./
RUN phi ci --omit=dev
# GitHub Actions
- run: phi ci --omit=dev
--frozen-lockfile is the right default for
prod:
the lockfile pins exact resolved versions and integrity hashes.
Without it, a transient registry change could ship a different tree to
prod than dev tested against. With it, prod refuses to install
anything the dev hasn't already seen and approved.
phi view
New in v0.5.0; scan-by-default in v0.5.1. Pointed at any
public package, phi view fetches the packument and the
tarball, runs phi's detector pipeline, queries OSV for known
advisories, and prints the human metadata block alongside a
security: section showing
score / verdict / detections / advisories. Same engine
phi install and phi audit use — so you
get the same verdict you'd get if you installed the package, before
you add it to package.json.
phi view react # latest version, full security scan
phi view react@18.2.0 # version-pinned scan
phi view lodash@4.17.10 # known-vuln older version → BLOCKED + 9 GHSA hits
phi view react --json # JSON output, including the "scan" object
phi view react --no-scan # fast metadata-only path (no tarball fetch)
phi view react license # one field via gjson dotpath (auto-skips scan)
phi view react 'versions[0]' # array index queries
Sample output (truncated) — a vulnerable older
lodash:
lodash@4.17.10
A modern JavaScript utility library delivering modularity, performance, & extras.
license: MIT
homepage: https://lodash.com/
published: 2018-04-24 (8 years ago)
versions: 117 total · latest 4.18.1
tarball: https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz
security:
score: 100/100
verdict: BLOCKED
files scanned: 1047
advisories (9):
- [CRITICAL] GHSA-jf85-cpcp-j695 — Prototype Pollution in lodash (fix: 4.17.12)
- [HIGH] GHSA-35jh-r3h4-6jhm — Command Injection in lodash (fix: 4.17.21)
- [HIGH] GHSA-4xc9-xhrj-v574 — Prototype Pollution in lodash (fix: 4.17.11)
- [HIGH] GHSA-p6mc-m468-83gw — Prototype Pollution in lodash (fix: 4.17.19)
- [HIGH] GHSA-r5fr-rjxr-66jc — Code Injection via _.template (fix: 4.18.0)
- [MODERATE] GHSA-29mw-wpgm-hmr9 — ReDoS (fix: 4.17.21)
- [MODERATE] GHSA-f23m-r3pf-42rh — Prototype Pollution via array path (fix: 4.18.0)
- [MODERATE] GHSA-x5rq-j2xg-h7qm — ReDoS (fix: 4.17.11)
- [MODERATE] GHSA-xxjr-mmjv-4gpg — Prototype Pollution in _.unset/_.omit (fix: 4.17.23)
npm view. Showing tarball
URL and license without showing the verdict on those bytes would
undersell the tool. The fast metadata path is one flag away
(--no-scan) for scripting, and the
field-targeted form auto-skips the scan because pipelines
rarely need it.
phi ls
New in v0.5.0. npm-style ASCII dependency tree, sourced
from phi.lock (falls back to a fresh resolve when
no lockfile exists).
phi ls # full tree, all direct deps
phi ls --depth 0 # direct only (no transitives)
phi ls --depth 1 # direct + one level
phi ls --prod # drop devDependencies
phi ls lodash # prune to branches that reach lodash
phi ls --json # stable JSON shape for scripts
Honors NO_COLOR for plain ASCII output (no bold
escapes) — useful when piping to less or capturing
to a file.
phi link
New in v0.5.0. Two-step npm link-style flow,
but cross-platform without admin: macOS / Linux get real
symbolic links; Windows gets directory junctions
(mklink /J) so no developer mode or elevation is
required.
# 1. In the package being developed
cd ~/work/my-lib
phi link # registers ~/.config/phi/links/my-lib → cwd
# 2. In the consumer
cd ~/work/some-app
phi link my-lib # creates node_modules/my-lib → ~/work/my-lib
phi link --list # show registered links and their targets
phi unlink my-lib # consumer side: drop node_modules/my-lib
cd ~/work/my-lib && phi unlink # package side: drop the global entry
Storage:
~/.config/phi/links/<name> on Linux/macOS,
%APPDATA%\phi\links\<name> on Windows.
Scoped names round-trip through one extra directory level
(links/@org/utils).
New in v0.5.0; live spinner in v0.5.1.
phi outdated queries every direct dep against the
registry's latest dist-tag and prints a color-coded
table — yellow for minor / patch bumps, red for major. Workspace
siblings declared with workspace: are stripped from
the table (they don't have a registry version to be "behind").
phi outdated # table for root direct deps
phi outdated --filter '@org/*' # per-workspace tables, filtered
phi outdated --filter './apps/**' # filter by workspace path
phi upgrade-interactive (alias phi ui)
walks each outdated package and asks
[y]es / [n]o / [a]ll / [q]uit. The existing range
prefix is preserved when rewriting package.json —
"^1.2.0" + new latest 1.3.0 becomes
"^1.3.0", not a bare exact pin.
phi upgrade-interactive # walk minor/patch bumps with prompts
phi ui --allow-major # also propose major-version bumps
phi ui --yes # accept every proposal (CI-safe)
phi ui --allow-major --yes # upgrade everything to latest
package.json and config
New in v0.5.0. Two scriptable round-trippers replace shell sed/awk:
phi pkg — edit package.jsonphi pkg get name # → "monorepo"
phi pkg get dependencies.lodash # → "^4.17.20"
phi pkg set scripts.lint "eslint ." # add or replace a script
phi pkg set engines.node "20" # stays the STRING "20", not coerced
phi pkg set private true # JSON true (boolean)
phi pkg delete scripts.lint
phi pkg fix # sort deps, dedupe dev→prod
Preserves indent style, leading BOM, trailing newline, and key
order on round-trip. Conservative coercion: only
true / false / null and
literal {} / [] parse as JSON.
npm pkg parity for the common flows.
phi config — edit ~/.config/phi/configphi config set registry "https://registry.npmjs.org/"
phi config set //npm.pkg.github.com/:_authToken "ghp_..."
phi config get registry
phi config list # comments preserved; secrets redacted on tty
phi config delete //npm.pkg.github.com/:_authToken
Secret-shaped keys (containing token,
password, secret,
_authtoken) are auto-redacted in
phi config list when stdout is a terminal; piping
to another command gets raw values for scripting. Comments and
blank lines survive round-trips so hand-edited files stay
readable.
--filter
New in v0.5.0. pnpm-style filter syntax for running a
script in a subset of a monorepo. Accepted by
phi do and phi outdated.
| Selector | Matches |
|---|---|
@org/api |
Exact workspace name. |
@org/* |
Name glob across one segment. |
./apps/** |
Path glob, relative to cwd. Matches against the workspace's resolved directory. |
!@org/api |
Negation. Combine with positive selectors to exclude. |
phi do build --filter '@org/*' # build every @org/* workspace
phi do build --filter '@org/*' --filter '!@org/web' # everything except @org/web
phi do test --filter './apps/**' # only workspaces under apps/
# Each match runs in its own dir with a header:
# === @org/api (1/3) ===
# === @org/billing (2/3) ===
# === @org/queue (3/3) ===
Missing scripts in a filter run are skipped silently (npm
parity) — so phi do test --filter '@org/*' works
even when only a subset of workspaces have a test
script.
phi createBootstrap a new project end-to-end. The scaffolder package itself is fetched and audited through phi's safe pipeline (lifecycle scripts off) before its binary runs in your current directory. The temp install is wiped after one use.
| Framework | Underlying scaffolder | Output |
|---|---|---|
phi create react <name> |
create-vite (template react-ts) |
React + Vite + TypeScript |
phi create next <name> |
create-next-app (with --skip-install)
|
Next.js (App Router) |
phi create express <name> |
built-in template (no network fetch) |
Minimal Express server, embedded in the phi binary via
embed.FS
|
phi create fastify <name> |
fastify-cli generate |
Fastify HTTP server |
phi create nest <name> |
@nestjs/cli new (with --skip-install)
|
NestJS application |
List available frameworks at any time:
phi create
Pass-through flags after -- go straight to the
scaffolder. User flags override phi's defaults on flag-name
collisions:
phi create react my-app
phi create express my-server
phi create next my-site -- --typescript --app --no-tailwind
phi create nest my-api -- --package-manager npm
Why --skip-install by default for next and nest?
Both scaffolders auto-run npm install after writing
files, which would bypass phi's safe pipeline for the project's actual
dependencies. Phi disables it so the user runs
phi install in the new directory instead, getting all
transitive deps scanned.
phi install afterwards.
| Flag | Effect |
|---|---|
--allow-scripts a,b |
Run lifecycle scripts only for the named packages. Default: never run. |
--frozen-lockfile |
Require phi.lock to exactly cover
package.json. CI mode.
|
--no-lockfile |
Ignore phi.lock and resolve fresh. |
--no-advisories |
Skip OSV vulnerability database query (offline mode). |
--json |
Emit phi-report.json to stdout, suppress UI.
Non-interactive mode skips unapproved REVIEW packages while keeping them in the report.
|
--save-dev / -D |
Write to devDependencies, moving from
dependencies if needed.
|
--save-peer |
Write to peerDependencies. |
--save-exact / -E |
Pin without caret prefix. |
--force / -f |
Install BLOCKED verdicts that are skipped by default. The scan still
runs and phi-report.json is still written — phi's
audit trail is preserved, you've just chosen to install
regardless. Implies auto-approval of REVIEW packages. Use case:
a known-trusted package that phi has flagged (e.g. a library
that uses eval() internally for a documented
private method).
|
--omit=dev,optional,peer
|
Skip dependency classes during resolution (npm parity).
Comma-separated values accepted in one flag.
dev drops devDependencies at the
root and in every workspace.
optional / peer are accepted as
no-ops because phi already skips those during install
resolution — the flags exist so the same install command
works across npm, yarn, pnpm, and phi.
|
--filter <selector>
|
Workspace selector for phi do and
phi outdated. Repeatable. See § 02.15.
|
CI and Dockerfile users often can't thread flags through every call site, so the most useful flags also resolve from environment variables. Flag values still win if both are set.
| Variable | Equivalent |
|---|---|
PHI_CI=1 |
Translates phi install into phi ci
(frozen lockfile + auto-approve REVIEW).
|
PHI_YES=1 |
Same as --yes. |
PHI_OMIT=dev,optional,peer |
Same as --omit=dev,optional,peer. |
PHI_FROZEN_LOCKFILE=1 |
Same as --frozen-lockfile. |
PHI_FORCE=1 |
Same as --force. Use sparingly — bypasses
BLOCKED verdicts globally.
|
PHI_REGISTRY_TIMEOUT=5m |
Override npm registry HTTP timeout for slow networks. |
PHI_REGISTRY_ATTEMPTS=5 |
Override registry retry attempts for packument/tarball fetches. |
PHI_REGISTRY_RETRY_DELAY=1s |
Override the first registry retry delay; later retries back off. |
phi.lock is generated on every install. Format mirrors
npm's package-lock.json shape with extra
score and verdict fields per entry. When
present and it covers package.json, phi reuses it without
re-resolving.
$XDG_CACHE_HOME/phi/tarballs/ (Linux/macOS) or
%LOCALAPPDATA%\phi\tarballs\ (Windows), keyed by sha512
integrity. Repeat installs are near-instant.
If your root package.json declares a
workspaces field, phi aggregates dependencies from every
workspace package, installs the union into the root
node_modules/, and links each workspace into
node_modules/<workspace-name> as a junction
(Windows) or symlink (Unix).
{
"workspaces": ["packages/*"]
}
Both array form and {packages: [...]} object form are
supported. Sibling references ("@org/utils": "*" from
inside @org/app) bypass the registry — they resolve
through the link.
workspace: protocol (v0.5.0)
For unambiguous sibling references that won't accidentally
fall through to the registry, phi supports the same
workspace: protocol pnpm and yarn-berry use:
| Spec | Behavior |
|---|---|
"workspace:*" |
Match any version of the sibling workspace. Never produces a version warning. |
"workspace:^1.0.0" /
"workspace:~1.0.0" |
Match a sibling whose version satisfies the
semver range. A version mismatch produces a single
consolidated warning per workspace — no silent
fallthrough to the registry.
|
"workspace:1.2.3" |
Exact version match against the sibling. |
"workspace:./packages/lib" /
"workspace:../lib" |
Path-relative form (yarn-berry parity). Resolves relative to the declaring workspace's directory. |
A missing sibling for any workspace: spec is an
explicit warning — phi does not fall through to npm
and fetch a package with a colliding name from the public
registry, which would otherwise be a stealth supply-chain
risk in a monorepo.
phi do --filter and
phi outdated --filter let you scope a command to
a subset of workspaces. See § 02.15 for the full selector
syntax. phi link works inside or outside a
monorepo and is unrelated to workspace: resolution
— use it when the package being linked lives outside the
repo.
phi reads .npmrc from $HOME and the project
root (project wins on conflict). Supported settings:
registry=https://... — default registry override@scope:registry=https://... — scoped registry routing
//host/path/:_authToken=... — bearer token, sent as
Authorization: Bearer <token> to matching URLs
${ENV_VAR} substitution — keep secrets out of committed
files
//npm.pkg.github.com/:_authToken=${GITHUB_PAT}
@my-org:registry=https://npm.pkg.github.com/
# GitHub Actions
- run: phi install --frozen-lockfile
- run: phi audit --json > phi-report.json
- run: jq '.summary.blocked == 0 and .summary.review == 0' phi-report.json
--frozen-lockfile requires phi.lock to
exactly cover package.json; non-zero exit on drift.
--json emits the full scan report to stdout and exits
non-zero on any blocked or review-flagged package.
phi switches to Go mode when the current directory (or any
ancestor) contains a go.mod or go.work.
The same command names you already know — install,
audit, outdated, view,
build — route to the Go pipeline automatically.
In an existing Go project:
phi install
What happens:
go.mod + go.sum. If
go.sum doesn't cover go.mod,
phi fails loudly and asks you to run
go mod tidy first.
go mod download runs into a
phi-controlled staging directory
(~/.cache/phi/go-staging/<run-id>) via the
GOMODCACHE=<staging> env override. Bytes do
not land in your real $GOMODCACHE yet.
.zip is h1-verified against
go.sum (constant-time compare via
golang.org/x/mod/sumdb/dirhash), then scanned by
phi's detector pipeline + OSV.
phi.lock still
writes (audit trail). Approved modules get atomically moved
into the real $GOMODCACHE per-file.
go build, go test, and the rest of
the toolchain see a normal cache and proceed as usual.
To start a brand new module from scratch:
phi init --go github.com/you/myproject
# creates go.mod, .gitignore (with Go-tuned entries), README.md
| Command | Aliases (Go-toolchain dialect) | Behavior |
|---|---|---|
phi init --go <module-path> |
phi mod init |
Run go mod init <path>, write a Go-tuned
.gitignore and a starter
README.md. --force overwrites
pre-existing files; --no-readme skips the
README.
|
phi install |
phi mod tidy · phi mod download |
Hybrid replace pipeline:
go mod download → staging →
scan → materialize. Writes
phi.lock.
|
phi install <mod>[@<ver>] ... |
phi get <mod>[@<ver>] |
Shells out to go get to add the modules to
go.mod, then runs the install pipeline to
scan + materialize the resulting tree. Accepts SHAs:
phi install github.com/foo/bar@abc1234
resolves to a pseudo-version.
|
phi install <pkg>@<ver> (no go.mod) |
— |
Binary install. When run outside a Go project, phi
treats this as the analog of
go install foo@latest: fetches into staging,
scans, then runs go install for real. Flags:
--force, --allow <pattern>,
--no-advisories,
--gobin <dir>. See § 06.
|
phi update [mod...] |
phi get -u [mod...] |
go get -u with phi's scan + lock on the new
versions.
|
phi remove <mod> |
phi mod edit -droprequire <mod> |
Drop a require + run go mod tidy. Does not
automatically re-scan; run phi install
afterwards if you want the new lockfile state.
|
phi audit |
phi mod verify |
Read-only scan over go.sum. Reports
transparency-log (sum.golang.org) status in the summary
footer.
|
phi audit fix [--apply [--force]] |
— |
Per-direct-dep OSV query. Splits proposed upgrades into
safe (same-major) and breaking (cross-major).
--apply rewrites go.mod via
go get; --force includes
breaking bumps. See § 04.
|
phi outdated |
phi list -m -u all |
Table of direct + indirect modules with newer versions. Major bumps in red, minor/patch in yellow, indirect deps in muted text. |
phi upgrade-interactive (alias phi ui) |
— |
Walks each outdated module, prompts y/n/a/q. Selected
upgrades batched into a single go get
call. --allow-major includes major bumps;
--yes for non-interactive bulk upgrades.
|
phi ls [filter] |
phi list -m all |
Flat module list with // indirect markers.
Filter substring narrows the output; --direct
drops indirects; --json emits a stable shape.
|
phi view <mod>[@<ver>] |
— |
Metadata + on-demand security scan. Shows version list,
publish time, replace target if any, deprecation /
retraction status. --no-scan for the fast
metadata-only path; --json for the
structured shape.
|
phi why <mod> |
phi mod why <mod> |
Print the import chain that pulls a module into your tree. |
phi doc [pkg [symbol]] |
— |
go doc with a phi-verdict footer when the
queried package is in your dep tree. Pure passthrough
for stdlib lookups.
|
phi build [args] ·
phi test · phi run ·
phi vet · phi fmt |
phi go <subcmd> |
Toolchain passthroughs with a phi.lock
freshness check upfront. PHI_STRICT_BUILD=1
or --strict turns the warning into a refusal.
See § 07 for --cross.
|
phi replace <old> <new>[@<ver>] |
phi go mod edit -replace=... |
Write a replace directive. Fires the
replace-with-fork detector if the redirect
crosses VCS orgs. phi replace --list shows
current directives. See § 09.
|
phi unreplace <mod> |
phi go mod edit -dropreplace=... |
Drop a replace directive. |
phi mod vendor |
go mod vendor |
Materialize vendor/ from
already-scanned modules in
$GOMODCACHE. Useful for offline / reproducible
/ air-gapped builds.
|
phi go <anything> |
— |
Universal Go toolchain escape hatch. Forwards every
argument to go with the user's environment
intact.
|
phi cache stat · clean |
— |
Reports four caches: npm tarballs, phi-x run stages,
Go verdict cache, Go's $GOMODCACHE.
clean --verdicts /
--gomodcache /
--all-caches. See § 11.
|
The architectural promise is the same as on the npm side: bytes never reach your real cache before phi has a verdict on them. Implementation differs because Go's threat model is different from npm's.
| npm | Go | |
|---|---|---|
| When can hostile code run? | postinstall at install |
init() at program start;
//go:generate during go generate;
cgo at build |
| Are package bytes themselves dangerous? | Tarball can include auto-running scripts | Zip is inert — source code only |
| What does phi quarantine? | Extraction into node_modules/ |
Materialization into $GOMODCACHE |
| What's the CI gate? | phi ci (frozen lockfile, non-interactive) |
phi audit --strict (refuses any un-overridden
BLOCKED entry) |
phi.lock writes on
every run, including when there are BLOCKED entries
(which are recorded with the verdict reason but not materialized).
CI tooling reads phi.lock as the truth surface — never assumes
its absence means "nothing was scanned."
Six per-module detectors fire as the analyzer walks each
module's source tree. One project-level detector fires on
your own go.mod.
| Detector | Pattern | Severity |
|---|---|---|
go-init-network |
init() calls net. or net/http. —
outbound HTTP/DNS on import |
HIGH |
go-init-fs-write |
init() writes to the filesystem outside
os.TempDir() |
HIGH |
go-cgo-shellexec |
import "C" alongside os/exec.Command
or syscall.Exec in the same file |
CRITICAL |
go-build-tag-gated |
Restrictive //go:build directive
(3+ tokens or negation) wrapping network/exec/fs calls |
MEDIUM |
go-go-generate-curl |
//go:generate shells out to
curl / wget / arbitrary URLs |
HIGH |
go-unsafe-import |
Use of the unsafe package |
LOW (signal, not blocker) |
go-replace-with-fork (project-level) |
Your go.mod has a replace
directive pointing to a different VCS org than the
original |
MEDIUM |
Per-file 5 MiB cap, per-module 10s wallclock timeout, parser
panic recovery. The analyzer reads zips via
archive/zip without extracting to disk.
Three escalating escape hatches when phi flags a module you've decided to ship anyway. Same shapes as the npm side.
--force — single-shot blanket overridephi install --force
# All BLOCKED entries admitted, with `forceAllowed: true`,
# timestamp, and reason recorded per entry in phi.lock.
--allow <pattern> — scoped per-installphi install --allow github.com/sirupsen/logrus
phi install --allow 'github.com/sirupsen/*'
phi install --allow github.com/sirupsen/logrus@v1.9.3
.phi-allow — persistent project-levelCommit this to your repo, like .gitignore:
# .phi-allow
github.com/sirupsen/logrus # uses init() for log-level setup
github.com/spf13/cobra@v1.* # cgo for terminal detection
github.com/foo/bar # FALSE POSITIVE: $HOME read, not network
phi reads .phi-allow at the start of every gate
check. Each line is <pattern> # <reason>;
comments after the pattern are retained for audit logs.
Precedence: --force > --allow >
.phi-allow.
When you run phi install <pkg>@<ver>
outside a Go project, phi treats it as a global tool
install (the analog of go install foo@latest)
and runs the scan-then-install flow:
# Outside any go.mod directory:
phi install golang.org/x/tools/cmd/goimports@latest
phi install github.com/cosmtrek/air@v1.49.0
phi install --gobin ~/bin golang.org/x/perf/cmd/benchstat@latest
Pipeline:
go list -m resolves the version
(handles @latest, SHAs, pseudo-versions)--force /
--allow)go install <pkg>@<resolved-ver>
runs for real, writing the binary to
$GOBIN (or wherever --gobin
points)
phi build / test / run /
vet / fmt forward to the matching
go subcommand. Before doing so, phi checks
phi.lock freshness vs go.sum:
phi install first"--strict: refuse)Default is warn-and-proceed. To make warnings fatal:
phi build --strict
PHI_STRICT_BUILD=1 phi build ./...
--cross GOOS[/GOARCH] sets the corresponding env
vars for this invocation only (no leak to your shell):
phi build --cross linux/arm64 -o myapp-arm64
phi build --cross darwin/amd64 ./cmd/server
phi build --cross windows -o myapp.exe
Anything phi doesn't wrap natively, hand to
phi go <anything>:
phi go env GOMODCACHE
phi go tool pprof cpu.prof
phi go bug
go.work
phi detects Go workspaces by walking from cwd up to the
filesystem root looking for a go.work file. When
found, phi install runs the pipeline for every
use directive in source order, writing a
per-member phi.lock. The aggregate verdict
summary prints at the end.
# In a workspace root containing go.work:
phi install # walks every member module
# Each member gets its own phi.lock alongside its go.sum.
use directives that escape the workspace root
(e.g. use ../outside) produce a review warning
but don't abort — they're allowed by Go but unusual enough
to flag.
Adding new modules to a specific member still requires
you to be in that member's directory (so go get
knows which go.mod to edit):
cd packages/api
phi install github.com/gin-gonic/gin@v1.9.1
replace in go.mod redirects a
require to a different module path or local directory. phi
wraps the toolchain commands:
phi replace --list # show current replaces
phi replace github.com/foo/bar github.com/myorg/bar@v1.2.3
phi replace github.com/foo/bar ../local-fork # local replacement
phi unreplace github.com/foo/bar # drop the directive
On write, the replace-with-fork detector fires
if the redirect crosses VCS orgs (e.g.
github.com/foo/bar →
github.com/quux/bar) — not blocking, but
surfaced as a review warning. Local-path replaces are
assumed intentional (you wrote the path yourself).
Go-mode phi.lock uses the v3 schema with
ecosystem: "go" and per-entry
source.type = "go-module". Each entry records:
name + versionsource.sum — h1 hash of the zip (matches go.sum)source.modSum — h1 hash of the .mod filescore & verdictindirect flag (mirrors // indirect)forceAllowed + forcedAt +
forcedReason, or allowedBy
(cli-flag / phi-allow)
In CI, phi audit --strict is the canonical
gate. It exits non-zero if any BLOCKED entry in
phi.lock lacks an override.
# GitHub Actions example
- run: phi install
- run: phi audit --strict
- run: go build ./...
- run: go test ./...
phi cache stat reports every cache phi touches:
$ phi cache stat
Tarball cache (npm): $LOCALAPPDATA/phi/tarballs
entries: 1182
size: 134.7 MiB
Run stages (phi x): $LOCALAPPDATA/phi/run
files: 0
size: 0 B
Verdict cache (Go): $LOCALAPPDATA/phi/verdicts/go
entries: 28
size: 8.4 KiB
Go module cache: ~/go/pkg/mod
size: 507.8 MiB (managed by `go`)
phi cache clean takes target flags. The verdict
cache is content-addressed by h1 hash, so wiping it is
always safe — phi just re-scans next install (which fills
it back up).
phi cache clean --verdicts # wipe phi's verdict cache
phi cache clean --gomodcache # shells out to `go clean -modcache`
phi cache clean --all-caches # tarballs + runs + verdicts (not GOMODCACHE)
phi cache clean --older-than 14d # default target: tarballs
| Variable | Effect |
|---|---|
PHI_ECOSYSTEM=go|npm |
Force ecosystem; useful when both go.mod
and package.json exist in the same dir. |
PHI_STRICT_BUILD=1 |
phi build refuses on stale
phi.lock instead of warning. |
GOPROXY · GOPRIVATE ·
GOSUMDB · GOFLAGS |
Honored transitively — phi shells out to
go which respects them. |
GOBIN |
Where phi install <bin>@<ver>
writes the binary. Defaults to
$GOPATH/bin or ~/go/bin. |
phi delegates module fetching to the Go toolchain, which
performs the transparency-log cross-check against
sum.golang.org by default. We never independently
re-verify — duplicating Google's reference implementation
would risk divergence.
phi audit's footer surfaces the resolved state:
audit: 47 modules scanned · safe=46 review=1 blocked=0 (3.2s)
transparency: ON via sum.golang.org
When GOSUMDB=off, GONOSUMCHECK=1, or
-insecure appears in GOFLAGS, the
footer reads transparency: OFF with the reason.
phi.lock's audit trail is still complete; the user just
knows the public log wasn't consulted.