diff --git a/.gitignore b/.gitignore
index 27650c9..c53e637 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,6 @@ coverage
# Field-test harness output (agent reports, request logs, scorecards)
field-test/results/
+
+# test-build throwaway fixture (recreated on every run)
+test-build/.work/
diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md
index af10a02..da0dae7 100644
--- a/AGENT-INSTALL.md
+++ b/AGENT-INSTALL.md
@@ -9,7 +9,8 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
- The connector is a normal npm package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. Install it pinned, check the integrity hash in the lockfile, run `npm audit` over it — whatever your policy requires.
- First published May 2026. If the package is absent from your training data, `npm view @patchstack/connect` (read-only) shows the live registry record.
- It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
-- The package also bundles an **opt-in** `protect` command (runtime exploit guard, currently for TanStack Start + Supabase apps; its templates live under `dist/protect/`). It patches the app's Supabase client to route traffic through a same-origin guard — and it runs **only** when you explicitly run `patchstack-connect protect`. Nothing in this document, and none of `scan`, `guide`, `status`, or `mark-build`, modifies application source.
+- **`scan` makes one source edit, and only after a successful post:** it adds (or updates) the disclosure widget's `
-
+
```
- Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The `userToken` is public by design — it ships in client-side HTML and is not a secret.
+ 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. If the project must not carry the widget, persist `"widget": false` in `.patchstackrc.json`; otherwise the next scan re-adds it.
-5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget snippet, so every developer and CI run reports to the same site.
+5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget tag, so every developer and CI run reports to the same site.
6. **Tell the user to claim the site** (free): open the claim URL from step 2 in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after claiming. They can also claim directly from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL.
diff --git a/GETTING-STARTED.md b/GETTING-STARTED.md
index 2f2d08e..d1c89dd 100644
--- a/GETTING-STARTED.md
+++ b/GETTING-STARTED.md
@@ -28,11 +28,11 @@ If you hit a refusal with a *new* reason (the agent quotes a specific phrase or
```bash
npm install --save-dev @patchstack/connect # bun-managed projects (Lovable, Bolt): bun add -d @patchstack/connect
-npx @patchstack/connect scan # registers the project, prints the claim URL — open it in your browser
+npx @patchstack/connect scan # registers the project, installs the widget tag, prints the claim URL — open it in your browser
npx @patchstack/connect guide # prints what's left, with the exact snippets for your project
```
-`guide` tailors its output to your project — right package manager, real site UUID, framework-specific widget placement — so finishing setup is copy-paste: the `prebuild`/`postbuild` hooks into `package.json`, and the report-a-vulnerability widget snippet into your HTML/layout file.
+`scan` also drops the report-a-vulnerability widget tag into your root HTML file (`index.html`-style shells) automatically. `guide` tailors the rest to your project — right package manager, real site UUID, framework-specific widget placement when your root layout is code instead of HTML — so finishing setup is copy-paste: the `prebuild`/`postbuild` hooks into `package.json`, plus the widget one-liner if `scan` couldn't place it itself.
## 4. You're done when
diff --git a/README.md b/README.md
index 163cca5..5eab958 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,8 @@ That's it. The first `scan`:
2. POSTs the package list to Patchstack with **no** UUID.
3. Patchstack provisions a fresh site and returns its UUID.
4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
-5. The connector prints a claim URL — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.
+5. The connector installs the disclosure widget's `
+ ```
+
+ Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as connector-managed); a pre-existing manual widget tag is left untouched. `--dry-run` and failed posts never edit anything. Projects whose root layout is code rather than HTML (Next.js, Nuxt, Astro, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
+
+- **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell the connector couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL).
+
+- **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both passes (dependency scanning only). Without it, the next successful scan re-adds the managed tag.
## Programmatic API
diff --git a/src/cli.ts b/src/cli.ts
index 7b6158e..1b3366a 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -15,18 +15,25 @@ import { collectGuideState, countRemainingSteps, renderGuideChecklist } from './
import { runProtect } from './protect/install.js';
import { detectStack, type StackDescriptor } from './stack.js';
import { PatchstackError } from './types.js';
+import { buildWidgetTag, ensureSourceWidget, ensureWidgetInHtml } from './widget.js';
const HELP = `@patchstack/connect — scan your lockfile and report packages to Patchstack.
Usage:
patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
If no UUID is configured, the server
- provisions one and we persist it.
+ provisions one and we persist it. After a
+ successful post it also adds/updates the
+ disclosure-widget `));
- const token = state.siteUuid ?? '';
- lines.push(detail(` `));
- lines.push(detail('The userToken is public by design — it ships in client-side HTML.'));
+ lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
+ lines.push(detail('The site UUID is public by design — it ships in client-side HTML.'));
}
// 5. Claim — the conversion moment; always the loudest line.
diff --git a/src/index.ts b/src/index.ts
index 77fb4b6..f8b7175 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,6 +14,17 @@ export {
isEmptyStack,
type StackDescriptor,
} from './stack.js';
+export {
+ WIDGET_SCRIPT_URL,
+ WIDGET_MARKER_ATTR,
+ buildWidgetTag,
+ ensureWidgetInHtml,
+ ensureSourceWidget,
+ findSourceShell,
+ type WidgetEnsureAction,
+ type WidgetEnsureResult,
+ type SourceWidgetResult,
+} from './widget.js';
export {
PatchstackError,
type Config,
diff --git a/src/types.ts b/src/types.ts
index 9c8883c..fd9d200 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -37,6 +37,12 @@ export interface Config {
timeoutMs: number;
/** Environment to report the manifest under. Defaults to 'production'. */
environment: Environment;
+ /**
+ * Whether the connector manages the disclosure-widget tag (source shell on
+ * `scan`, built HTML on `mark-build`). Defaults to true; persist
+ * `"widget": false` in .patchstackrc.json for dependency-scanning only.
+ */
+ widget: boolean;
}
export interface StoreManifestResponse {
diff --git a/src/widget.ts b/src/widget.ts
new file mode 100644
index 0000000..2bf668d
--- /dev/null
+++ b/src/widget.ts
@@ -0,0 +1,120 @@
+// Managed disclosure-widget tag — the connector installs the widget for you.
+//
+// After a successful scan the connector ensures the site's root HTML shell
+// carries the widget's one-liner CDN tag (the canonical install form from the
+// widget docs: a single `;
+}
+
+export type WidgetEnsureAction =
+ /** No widget present — the managed tag was inserted before