This versioned reference ships inside @patchstack/connect and documents each setup command and its project changes.
Every command at a glance — what it does, whether it reads your source, what it writes, and what leaves your machine. Full behavior, flags, and edge cases follow in the sections below.
| Command | What it does | Reads your source? | Writes to your project | Sends over the network |
|---|---|---|---|---|
scan |
Provision (or reuse) the site and POST the dependency list for vulnerability matching. Also runs automatically via setup and the install/build hooks. |
No — lockfile only (bun: enumerates node_modules/) |
.patchstackrc.json; the widget <script> tag in the root HTML shell — only after a successful post |
Package names + versions |
setup |
One bounded command: scan → manage the widget → install + verify protect → wire the install/build scans. Never runs the project build. |
No | Config, widget tag, guard files, package.json scripts |
Package names + versions (via scan) |
map |
Local, read-only attack-surface analysis (entry points → inputs → sinks → evidence-backed flows). Never run by another command. | Yes — via the app's own TypeScript | Nothing (only the file named by --out) |
Nothing — unless --upload: structure only (routes, parameter names, the package behind each sink, file:line). Never source code or env values |
protect |
Install the always-on runtime guard; auto-wire known stacks, or scaffold a generic guard + print a wiring plan. --check verifies the guard is wired (exit 1 if not); --demo seeds a broad sample rule set. Runs automatically only via setup — never by scan, guide, status, or mark-build. |
No — writes guard files, does not analyze your code | Guard/framework files (e.g. middleware.ts, src/patchstack/) |
Nothing |
demo node-serialize |
Production-backed walkthrough: confirm the vulnerable package is present, scan, wait for live rule 18843, install + verify the guard, print test requests. Does not install the package or start/restart the app. |
No | Same files as scan + protect |
scan payload; polls the public Pulse rules endpoint (never the printed test requests) |
demo-guide node-serialize |
Read-only companion: explains the prepare/run/prove/cleanup sequence and prints the next command. | No | Nothing | Nothing |
guide |
Print this project's live setup status (done/missing, with tailored commands), then the full guide. --full prints it even when setup is complete. |
No | Nothing | Nothing |
status |
Re-print the site UUID + dashboard URL and check whether the site still exists (active / removed / could not verify). | No | Nothing | Site-existence check |
init <site-uuid> |
Optional: pre-seed .patchstackrc.json with an existing UUID. |
No | .patchstackrc.json only |
Nothing |
mark-build |
Stamp built HTML with a production flag + build fingerprint and ensure the widget tag in built pages. Run as a postbuild step. |
No | Build output only (dist/ build/ out/ .output/public) — never source |
Nothing |
login |
Recover a lost credential for an existing site: print an owner-approval link and poll (10 min). Approving rotates the credential. Not usable in CI. | No | New credential into .patchstackrc.json on approval |
Device-code request + approval poll |
uninstall |
Signal Patchstack that the package is being removed: an unclaimed record is deleted, a claimed one is flagged. Does not touch local files. | No | Nothing local | Removal signal |
Only map reads your source, and only map --upload sends anything derived from it. scan transmits nothing but package names + versions — never source code, env var values, file paths, or git history.
- Package:
@patchstack/connect, MIT-licensed, source at https://github.com/patchstack/connect.npm view @patchstack/connectshows the live registry metadata. - What is sent to Patchstack is the dependency list only — read from the lockfile (
package-lock.json,pnpm-lock.yaml,yarn.lock) or, on bun projects (bun.lock/bun.lockb), by enumerating the installed packages undernode_modules/— package names + versions, for vulnerability matching. No source code, no env var values, no file paths, no git history is ever transmitted. (mark-buildadditionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable names — e.g.VERCEL,CF_PAGES— never their values.) - One command reads source files:
map(see below) parses your server source to report your app's attack surface. It runs only when you invoke it and prints to stdout. It transmits nothing unless you explicitly pass--upload, which sends that description of your app's structure to your own site's Patchstack endpoint — never source code, and never without that flag. No other command reads source (protectwrites guard files but does not analyze your code). scanmakes one source edit, and only after a successful post: it adds (or updates) the disclosure widget's<script>tag in the project's root HTML shell — the first ofindex.html,public/index.html, orsrc/app.htmlthat exists. It touches no other file, never edits on--dry-runor after a failed post, leaves any pre-existing manual widget tag untouched, and is disabled entirely by"widget": falsein.patchstackrc.json.mark-buildwrites to build output only (dist/,build/,out/,.output/public), never to source.guide,status, andinitwrite nothing exceptinit's own.patchstackrc.json.setuprunsscan, thenprotect, then editspackage.jsonscripts: provisioning happens first so the runtime guard can bake the real site UUID. It verifies the resulting framework seam, preserves existing commands, addsscanafter dependency installs and before builds, addsmark-buildafter builds, and uses a direct build chain for Bun. It never runs the project build. If the widget or runtime guard needs a framework-specific manual merge, it prints the exact remaining step instead of overwriting user code.- The package also exposes
protectdirectly (runtime exploit guard; its templates live underdist/protect/).setupinvokes it automatically;scan,guide,status, andmark-builddo not. It writes only local files and auto-wires known stacks — TanStack Start + Supabase (patches the Supabase client +src/start.ts), Next.js (scaffoldsmiddleware.ts), SvelteKit (src/hooks.server.ts), Astro (src/middleware.ts), Nuxt (server/middleware/), NestJS (app.use(patchstackMiddleware)in the bootstrap), Fastify (app.register(patchstackFastify)), and Express (app.use(patchstackMiddleware)). On any other stack it scaffolds a framework-agnostic guard undersrc/patchstack/and prints a wiring plan — then you finish the install by importing that guard into your server entry (protectFetch(handler)for a Web-Fetch server, orapp.use(patchstackMiddleware)for Node/Express) and runningpatchstack-connect protect --checkto confirm it is wired (exit 1 until it is). Passing--demoseeds a broad sample rule set (for demonstrations, not production). demo node-serializeis an explicit production-backed walkthrough. It requiresnode-serialize@0.0.4to already be present in the lockfile; it does not install the vulnerable dependency. It runs the same productionscan, polls the configured site's public Pulse rules endpoint until rule18843is served, runsprotect, verifies the generated guard, and prints exploit/benign test requests. It writes the same manifest/widget and guard files as those underlying commands. It does not start/restart the app and does not send the printed requests.mapis a local, read-only analysis command. It walks the project's server source (skippingnode_modules, build output and dot-directories; it does not follow symlinks out of the project unless you pass--follow-symlinks), parses it with the project's owntypescript, and prints JSON describing the attack surface: entry points, the inputs each reads, the sinks they can reach (database / file system / process / outbound HTTP) with the npm package behind each, and evidence-backed input→sink flows, each labelled with how the link was established — from an exact read at the sink's own call site, through a transformed or cross-module link, down to the two being present together with no proven link. Static analysis is best-effort, so the output reports the detected surface with coverage counters — not a completeness guarantee. It writes nothing (except the file you name with--out) and is never invoked byscan,setup,guide,protect, ormark-build.map --uploadis the only command that sends a description of your source. (The runtime guard can also report rule detections, which carry route paths and parameter names — see "Runtime guard reporting" below.) It POSTs the same JSON document tomonitor/pulse/input-map/<your site uuid>so Patchstack can pin protection rules to your app's own parameter names instead of guessing them. What is sent is exactly whatmapprints — a structural description: route paths, parameter/field names, the dependency behind each sink, and file paths with line numbers. No source code, no file contents, no environment variable values. It never runs without the flag, it is skipped when no entry points are detected, and a failure to reach Patchstack is reported and ignored rather than failing your build. Omit the flag and the command stays entirely local.demo-guide node-serializeis the read-only companion. It checks the Host-created site configuration and vulnerable lockfile entry, explains the complete local prepare/run/restart/prove/cleanup sequence, and prints the next exact command. It does not require a deployment and does not change files or contact Patchstack.- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
npx @patchstack/connect guideprints a read-only live checklist showing which steps are already done in the current project (install, provisioning, build hooks, widget, runtime protection).- If
.patchstackrc.jsoncontains asiteUuidkey, the project is already provisioned. Reuse that UUID; runnpx @patchstack/connect statusto re-print it and the dashboard URL. Do not delete the file and provision a second site. (A.patchstackrc.jsonwith other keys — e.g. anendpointoverride — but nositeUuidis not provisioned yet; scan normally.) - If
@patchstack/connectis already independencies, skip the install command. If it is only indevDependencies, move it with the matching package manager so production runtimes that prune dev dependencies can load the generated guard. - If the widget script tag (
cdn.patchstack.com/patchstack-widget.js) is already in the layout, don't add a second one —scanalso respects an existing tag: it updates its own managed tag in place and leaves a manual one untouched.
-
Install (skip if already present), matching the project's package manager:
npm install --save @patchstack/connect # package-lock.json pnpm add @patchstack/connect # pnpm-lock.yaml yarn add @patchstack/connect # yarn.lock bun add @patchstack/connect # bun.lock / bun-managed platforms -
Run bounded setup:
npx @patchstack/connect setupThis provisions or reuses the site, manages the widget, installs and verifies runtime protection, wires dependency-install and build scans, prints a dashboard link, and finishes with the same status shown by
guide. Re-running it reuses existing configuration, widget tags, guards, and commands rather than duplicating them.In a hosted builder, run setup with
PATCHSTACK_ENVIRONMENT=sandboxscoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist"environment": "sandbox"in.patchstackrc.json: deployed builds use the same committed files and should default toproduction. A client-only SPA has no server request path to guard; do not call it protected unlessprotect --checksucceeds after a real server or edge seam is wired.
-
First scan — provisions a Patchstack site automatically, writes the UUID to
.patchstackrc.json, and installs the disclosure widget's<script>tag into the root HTML shell (index.html,public/index.html, orsrc/app.html) when one exists. No signup, dashboard step, or UUID is needed up front:npx @patchstack/connect scanIt prints a dashboard link but never opens it. Open that link in a browser to view reports. It also prints what it did about the widget — if it added the tag, reload the preview and confirm the "Report a vulnerability" button appears.
-
Wire builds in
package.json:If a lifecycle hook already exists, chain instead of replacing it, e.g.
"prebuild": "existing-command && patchstack-connect scan". Thepostinstallscan reports dependencies added during an iterative sandbox session and covers applications with no build command.Bun-managed projects:
bun rundoes not execute npm-stylepre/postscripts, so wire the build script directly instead:"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build". -
Verify the disclosure widget — a floating "Report a vulnerability" button.
scaninstalls it automatically into a plain HTML shell, andmark-buildcarries it into built HTML. Only whenscanreported that it found no editable shell (frameworks whose root layout is code, e.g. Next.js/Nuxt/Astro) add the one-liner it printed to the root layout yourself, just before</body>(never a JS entry point), readingsiteUuidfrom.patchstackrc.json:<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The site UUID is public by design — it ships in client-side HTML and is not a secret. The credential is the opposite, and
scanwrites it for you — there is no manual step, and you should never invent or ask the user for this value:
apiKey(alsoPATCHSTACK_API_KEY, WP format{secret}-{oauth.id}) — one credential for both paths. It authenticates Pulse ingest (manifest, attack-surface map, package removal, rule detections), where it is exchanged for a short-lived token rather than sent directly, and block-log reporting through the connectorPOST /api/logs/log, so "Threats blocked" fills in the dashboard.
It is server-only. Never put it in the widget tag, client bundles, or public env vars (NEXT_PUBLIC_*, etc.). Prefer PATCHSTACK_API_KEY in production; .patchstackrc.json is fine for local DX. If it is lost, npx @patchstack/connect login recovers it via dashboard approval — do not delete the file and re-provision, which would create a second site. Opt out of reporting with PATCHSTACK_TELEMETRY=off. If the project must not carry the widget, persist "widget": false in .patchstackrc.json; otherwise the next scan re-adds it.
A pulseAuth field is still honoured if a project has one, and PATCHSTACK_PULSE_AUTH still overrides it, for deployments that authenticate Pulse ingest with a different credential from block-logs. Do not add either yourself: they are unnecessary when the two share one credential, which is the default.
-
Install and verify runtime protection:
npx @patchstack/connect protect npx @patchstack/connect protect --checksetupperforms both steps automatically. The explicit commands are for manual setup or repair. If verification reports a generic or existing framework seam, complete the printed source edit and re-run--check; do not report protection as active until it exits successfully. -
Commit
.patchstackrc.json, the updatedpackage.json, the guard/framework source changes, and the layout/HTML file carrying the widget tag, so every developer and CI run reports to the same site. -
Open the dashboard link from the scan in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after connecting it to an account. The same connection flow is available from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending
#patchstackto the live URL.
- Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
- The CLI never opens the dashboard link and never asks for Patchstack credentials.
- Label hosted workspace scans with
PATCHSTACK_ENVIRONMENT=sandboxin that process only. Leave production builds unset (the default isproduction) and never commit a sandbox label into files shared with production. - If a step fails, stop and report it. Don't proceed with placeholders.
- In CI where
.patchstackrc.jsoncan't be committed, setPATCHSTACK_SITE_UUIDandPATCHSTACK_API_KEYas env vars instead. Precedence: CLI flag → env var →.patchstackrc.json.loginis interactive and refuses to run in CI, so CI always takes its credential from the environment.
The runtime guard (protect) can report the rules that matched, so the dashboard can show what a rule
would have stopped while it is still in dry-run. Two separate paths, with different triggers:
- Blocked requests go to the connector
POST /api/logs/log, the same path the WordPress plugin uses, and fill in "Threats blocked". This runs when the guard is holding anapiKeyand a rule blocked a request. The credential is first exchanged atPOST /oauth/token(client credentials) for a bearer token; theapiKeyitself is not sent to the log endpoint. Disable withPATCHSTACK_TELEMETRY=off, orreportFirewallLog: falseincreateProtection. - Every rule that matched goes to
monitor/pulse/detections/<your site uuid>— including matches that blocked, which are reported on both paths. This is off unless you passreportDetections: truetocreateProtection; the scaffolded guard does not pass it. It also requires a provisioned site UUID and is disabled byPATCHSTACK_TELEMETRY=off. It exists because a rule carryingdry-runblocks nothing, so without it nothing distinguishes a rule that is protecting from one that is quietly wrong.
What a detection report contains, per matched rule: the rule id, the request path with any query string removed, the parameter names that rule reads (from the rule's own definition), which phase matched, whether it was enforced, the identifier of the rule bundle in use, and a timestamp. Each batch also carries a count of reports dropped when traffic outran the flush, so a partial sample is not read as a complete one.
The parameter names are identifiers, and they name the request region they refer to — post.title,
get.redirect_to, cookie.session, server.HTTP_AUTHORIZATION. So a rule that inspects a cookie or an
Authorization header sends that cookie's or header's name. They are read from the rule's own
definition, not from your traffic, so they describe what is being screened rather than what any request
contained.
What it does not contain: no values of any kind. Not the value that matched, not the request body, and not the value of any header, cookie or query-string parameter — including those of the parameters named above. Reports are batched, capped in memory, and dropped rather than retried if Patchstack cannot be reached — a reporting failure never delays or fails a request.
Two more endpoints the package can call, for completeness:
GET monitor/widget/settings/<your site uuid>— howstatustells "this site was deleted on Patchstack" apart from "still active". It sends no credential and nothing about your project; the site UUID in the path is the whole request.GET api/get-rules/3— the older rules path, used only when the guard is configured with atokeninstead of a site UUID. The zero-configuration flow provisions a site UUID and usesmonitor/pulse/rules/<uuid>instead, so this is unreachable unless you passtokenyourself.
npx @patchstack/connect statusre-prints the site UUID and dashboard URL, and checks whether the site still exists on Patchstack (Site status: active / removed / could not be verified).npx @patchstack/connect protect --checkverifies the runtime guard is connected to the request path.- Load the site in a browser — the "Report a vulnerability" button should appear.
These are two independent states — never conflate them:
- The site record on Patchstack (remote). Deleting the site in the dashboard or through the widget's uninstall flow removes it. Reporting stops and the widget stops rendering, but nothing in the project changes.
- The local integration (this repo): the widget
<script>tag,.patchstackrc.json, the@patchstack/connectdependency, the runtime guard files, and the build hooks.
Local files alone cannot tell you whether the site was removed from Patchstack. Run npx @patchstack/connect status and read the Site status line, then answer with both states. For example, when the site was removed but the local files remain, say: "The site itself was removed from Patchstack — reporting has stopped and the widget no longer renders. The local integration code (widget tag, .patchstackrc.json, the dependency) is still in the project; want me to remove it?" — not "Patchstack is still installed."
Use this when the project already has a site but its credential is gone or rejected: .patchstackrc.json was deleted or never committed, the repo was cloned without it, a container was recycled, or ingest started failing with 401.
Do not "fix" a missing credential by deleting
.patchstackrc.jsonand runningscanagain. That provisions a second site, and the original — with all its history and its widget tag already live on the deployed page — is orphaned.loginrecovers the existing one.
npx @patchstack/connect login
Your code: WDJB-MJHT
Approve at: https://api.patchstack.com/monitor/pulse/device?code=WDJB-MJHT
Waiting for approval… ✓ Credential restored
The command asks Patchstack for a short code, prints a link, and polls until the site's owner approves it in the dashboard. On approval it writes the new credential into .patchstackrc.json and exits. The link opens the approval page with the code already filled in, so the person only has to confirm.
The command exits immediately when you run it. It detects that its output is being captured rather than watched by a person, prints the link, and returns. It does not block waiting for approval, because you would not see the link until it exited — by which time the code would have expired, and it would look like the command had hung.
1. npx @patchstack/connect login → prints the link, exits straight away
2. give the user the link, verbatim → they approve it in the browser
3. npx @patchstack/connect login → the SAME command again, after they confirm.
It resumes the request and finishes the flow
- Never wrap step 1 in a timeout or kill it — it returns on its own. If you find yourself waiting on it, something else is wrong.
- Step 3 is the same command. While a request is still valid it resumes rather than restarting, so running
loginagain never invalidates the link the user is looking at. If they have not approved yet it tells you so, with the time remaining, and exits. - Nothing changes until step 3 runs. Approving only marks the request; the credential is rotated and written when the CLI redeems it. So an abandoned flow is harmless — the site keeps working — but the credential is not restored until you come back.
- Surface the link verbatim. Approval requires the user's signed-in Patchstack account, which you do not have and must never ask for.
- Report the outcome. On success, say the credential was restored and that the previous one no longer works — see the warning below.
login --wait is the blocking variant: it polls until approved instead of returning. Prefer the plain re-run — it keeps each command short, which is what fits a conversation.
You cannot complete this alone. It is deliberately a human-in-the-loop step: starting the flow proves nothing about who is running it, so the only authorisation is an owner approving in the browser.
(In an interactive terminal the same command prints the link and then waits, since a person can watch it stream. You get the two-step form; a human at a shell gets the one-step form.)
Approving rotates the credential — the old one stops working immediately. Anywhere it was configured needs the new value: CI secrets, hosting-platform env vars, preview environments, other developers' checkouts. Say this before they approve, not after.
| Situation | What happens | What to do |
|---|---|---|
| Site was never claimed | 409 — no owner exists to approve |
Ask the user to claim the site in the dashboard first, or, if the site is disposable, delete .patchstackrc.json and scan to provision a fresh one |
| Running in CI | Refuses to start | CI takes its credential from PATCHSTACK_PULSE_AUTH; login is for a developer machine |
No siteUuid configured |
Refuses to start | There is no site to recover — run scan |
| Code expired | --wait ends after 10 minutes |
Start again from step 1 for a new code |
--wait with nothing pending |
"No login is waiting for approval" | Run step 1 first; --wait resumes a request, it does not start one |
Remove only the pieces that are actually present — check for each first. If none are present, Patchstack isn't installed; report that and stop. If the user asked to remove only one piece (e.g. "just the widget"), remove only that piece.
- Read the site UUID from
.patchstackrc.jsonbefore deleting anything. It is the only local record of the provisioned site — report it to the user at the end so they can identify the site in their dashboard. - Remove the widget snippets from the layout/template: the
<script src="https://cdn.patchstack.com/patchstack-widget.js">tag and anyPatchstackWidget.init(...)call (which may live in a separate client component/plugin/effect). Afterwards, grep the repo forpatchstack-widgetandPatchstackWidgetto confirm nothing remains. - Remove runtime protection before uninstalling the package. Delete the connector-managed guard/rules files and remove only their managed imports, middleware registrations, tunnel code, and
#region patchstack…blocks from the framework/server files. Preserve unrelated middleware and application code. Runrg "patchstack|x-ps-target"(or the available equivalent) afterwards and inspect every remaining source hit. - Remove the hooks from
package.jsonscripts. If a hook was chained (e.g."postbuild": "existing-command && patchstack-connect mark-build"), remove only thepatchstack-connect …part and keep the rest; if removal leaves a script empty, delete the key. - Signal Patchstack that the package is being removed: run
npx @patchstack/connect uninstall(while the package is still installed and.patchstackrc.jsonstill exists). If the site was never claimed, this deletes its anonymous record on Patchstack; if the site is claimed, it is only flagged — the record stays until its owner removes it in the dashboard. A failed signal must not stop the uninstall; continue with the remaining steps. - Uninstall the package with the manager matching the lockfile:
npm uninstall/pnpm remove/yarn remove/bun remove@patchstack/connect. Don't hand-editnode_modulesor the lockfile. - Delete
.patchstackrc.jsonand removePATCHSTACK_SITE_UUID,PATCHSTACK_API_KEY(and public-prefixed variants likeNEXT_PUBLIC_PATCHSTACK_SITE_UUID) from env files and CI variables. - Commit the changes. Reporting stops immediately. The
window.__PATCHSTACK_PROD__flag thatmark-buildinjected lives only in build output, never in source — the next build simply won't contain it (rebuild if build output is committed).
The uninstall signal is the only account-side effect local removal can have: it deletes an unclaimed (anonymous) record and merely flags a claimed one. A claimed site keeps using a site slot until its owner removes it in the dashboard at https://app.patchstack.com — end your report by telling the user this, alongside the site UUID from step 1. Never attempt to authenticate or remove a claimed site on the user's behalf.
The reverse also holds: removing the site on Patchstack's side (dashboard delete or the widget's uninstall flow) does not touch these local files — they must still be removed with the steps above. npx @patchstack/connect status shows Site status: removed from Patchstack in that state.
{ "scripts": { "prebuild": "patchstack-connect scan", "postbuild": "patchstack-connect mark-build", "postinstall": "patchstack-connect scan" } }