diff --git a/craft/scripts/e2e-capture-security-entry.ts b/craft/scripts/e2e-capture-security-entry.ts
new file mode 100644
index 0000000..ba83388
--- /dev/null
+++ b/craft/scripts/e2e-capture-security-entry.ts
@@ -0,0 +1,16 @@
+// Bundled by scripts/e2e-capture-security.mjs and run in real Firefox + Chromium.
+import { captureHtml } from '../src/utils/self-review-capture';
+import { captureCsp } from '../src/utils/sanitize-capture-html';
+
+(window as any).captureHtml = captureHtml;
+/** Defence-in-depth probe: the capture frame WITHOUT the sanitiser (sandbox + CSP only). */
+(window as any).rawFrame = (html: string) => new Promise((resolve) => {
+ const f = document.createElement('iframe');
+ f.setAttribute('sandbox', 'allow-same-origin');
+ const CSP = captureCsp(location.origin);
+ f.setAttribute('csp', CSP);
+ f.style.cssText = 'position:fixed;left:-9999px;width:800px;height:600px';
+ f.addEventListener('load', () => setTimeout(resolve, 800), { once: true });
+ f.srcdoc = html.replace('', ``);
+ document.body.appendChild(f);
+});
diff --git a/craft/scripts/e2e-capture-security.mjs b/craft/scripts/e2e-capture-security.mjs
new file mode 100644
index 0000000..11fa801
--- /dev/null
+++ b/craft/scripts/e2e-capture-security.mjs
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+/**
+ * Real-browser proof (Firefox + Chromium) that the self-review capture cannot run or leak hostile page
+ * markup. A malicious fixture goes through the WHOLE capture path; sentinels: window.__pwned in the
+ * parent, any request to /beacon or evil.example, parent cookie/localStorage reachability, frame
+ * navigation. A second probe feeds the SAME hostile page to the frame WITHOUT the sanitiser to show the
+ * sandbox + CSP alone also hold. Benign pages must still capture.
+ * Run: node scripts/e2e-capture-security.mjs (CHROMIUM_PATH / FIREFOX_PATH override the binaries)
+ */
+import { build } from 'esbuild';
+import { chromium, firefox } from '@playwright/test';
+import http from 'node:http';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const out = await build({
+ entryPoints: [path.join(root, 'scripts/e2e-capture-security-entry.ts')],
+ bundle: true, write: false, format: 'iife', platform: 'browser', logLevel: 'error',
+ define: { 'process.env.NODE_ENV': '"production"' },
+});
+const js = out.outputFiles[0].text;
+
+const PWN = "(function(){try{parent.__pwned=1;top.__pwned=1}catch(e){}try{fetch('/beacon?js')}catch(e){}try{top.postMessage('pwned','*')}catch(e){}try{new Image().src='/beacon?img-js'}catch(e){}})()";
+const EVIL = `
+
+
+
+
+
+Hostile but readable
+
+
+js link
+
+
">',
+ '