Single source of truth for planned work. Tiered by execution priority. Completion history lives in git history and
CHANGELOG.md; consolidated research conclusions live inRESEARCH.md. Legacy planning passes are reference-only underdocs/archive/.Roadmap version: Round 99 - Greasy Fork handoff cleanup hardening 2026-06-11. Shipped baseline: v3.11.0 (2026-05-19, tag pushed).
mainhas additional unreleased hardening, TS promotion, Firefox validation, and release-trust commits through 2026-06-11. Test suite: 1582 Vitest cases green;npm audit --audit-level=high --omit=optionalclean; 28/28 TS-promoted runtime entries; 0 mirrored; 0 divergent. Source floor: 400+ external URLs across Rounds 1-40. Every Now/Next item carries source IDs from the Appendix.Last researched: Round 92 - 2026-06-07.
Tampermonkey was briefly removed from CWS in July 2025 [S01] but returned and now sits at v5.5.0 with 12M users — the dominant incumbent. Violentmonkey remains MV2-only (v2.41.0, actively developed) but permanently blocked on Chrome 133+ (MV2 fully removed in Chrome 139) [S02, S03]. ScriptCat (4.5K stars) is MV3-native but niche [S04]. New entrants include Tweeks (YC W25, AI-native) and lightweight MV3 managers (OrangeMonkey, Vanilla Pudding, BareScript). Greasemonkey and FireMonkey remain Firefox-only MV2 [S05, S06].
ScriptVault is the only MV3-native, open-source, full-featured userscript manager with zero telemetry on the Chrome Web Store. Tampermonkey's return narrows the MV3 exclusivity window, but its closed-source and freemium model remain trust liabilities. The roadmap prioritises differentiation: ship Firefox AMO, land Edge, close GM API parity gaps, and capitalize on trust advantages (Ed25519 signing, AST analysis, update review, MIT license).
Correction — 2026-08-06. The paragraph above is out of date and the exclusivity claim no longer holds. Violentmonkey shipped MV3 stable in v2.43.0 on 2026-07-14 (first MV3 beta v2.41.2, 2026-07-07) and is at v2.47.0 as of 2026-08-06, with S3 sync, an opt-in "Alternative page mode" for true
document-starttiming, and Firefox CSP bypass. ScriptCat is at v1.4.0 stable with Firefox MV3 support, and FireMonkey revived with v3.0–v3.6 (2026-07). The durable differentiators are now Ed25519 signing (answers the still-open Violentmonkey #1558), the shipped update-review triad (answers the still-open #1023 +37 and #500 +30), AST risk analysis, and the accessibility gate — not MV3 itself. SeeRESEARCH.md(2026-08-06) and the P1 item "Refresh the README comparison table" below.
| Tier | Meaning | Timeframe |
|---|---|---|
| Now | Active queue. Ship in v3.12.0. | Days to weeks |
| Next | Planned for v3.13.0-v3.14.0 after Now clears. | Weeks to months |
| Later | Validated direction, not yet scheduled. | Months to quarters |
| Under Consideration | Promising but needs more research or user signal. | Unscheduled |
| Rejected | Evaluated and declined, with reasoning. | N/A |
Priority labels within tiers: P0 safety/security/data-loss, P1 core workflow gaps, P2 polish, P3 nice-to-have.
(All Now-tier items are credential/compliance blocked — see Roadmap_Blocked.md.)
Findings from a six-agent deep audit (workbench redesign UI, typed action
dispatch, recent security commits, UserCSS/language service, locale/controllers,
secondary surfaces). ~30 confirmed items were fixed across commits c62c19e
(UserCSS engine), c02e26d (source maps), c13f7f8 (UserCSS UI + LSP),
780c837 (error-log URL retention), b419671 (workbench UI), e230d54
(secondary surfaces), and 259536b (controllers + a11y gate). The items below
are lower-priority findings deferred for a later pass.
Findings from a six-agent deep audit (correctness, security, cloud-sync/storage,
background core, dashboard UX, dashboard modules, non-dashboard UI). The P0/P1
data-safety and background-hardening items were fixed in commits 17f45bf
(sync data-safety + restrict-to-site scope expansion) and e233c94 (update
lock + SRI @resource + scam-detector precision) — do NOT re-open those. The
items below are the remaining unfixed findings.
- Authoritative source is
src/**(TypeScript).background.js,background.core.js,modules/*.js, andbg/*.jsare GENERATED. The live service-worker logic lives insrc/background/core.ts(a ~13k-line bridge); the focused modules undersrc/background/(e.g.registration.ts,update-checker.ts,resource-loader.ts) are EXTRACTION TARGETS that are NOT wired into the runtime — editing them alone changes nothing at runtime. Grep the generatedbackground.jsfor a symbol to confirm which copy is live before editing; usually you must fix BOTHcore.ts(runtime) and the matching extraction module (drift-cleanliness). The@matchReDoS and restrict-to-site fixes both had to touch two copies for this reason. - After any
src/**edit:npm run ts-runtime:generate→npm run build:bg→npm run typecheck→ focusednpx vitest run <files>→ thennpm run checkbefore committing. Never edit generated artifacts by hand. pages/dashboard.jsis plain JS (no build step) and is LF-pinned. Edit with the Edit tool only — PowerShellSet-Contentrewrites CRLF and breaks the\n-literal source-pin tests (support-snapshot-redaction). Many dashboard styles are duplicated between inlinepages/dashboard.html<style>andpages/dashboard.css— check both.- Theme tokens: the 4 themes (dark/light/catppuccin/oled) are defined in
pages/theme-tokens.cssand the dashboard theme blocks. Prefer semantic tokens over raw hex; verify every change in all four modes. - Add a regression test for every fix (static source-pin at minimum; functional where a harness exists). Commit in logical batches with conventional messages; no AI authorship in commits.
- UC-1. Safari via Native App Container [S40, S41] — Safari lacks
userScriptsAPI. Requires separate Swift project. Reconsider when user demand justifies it. - UC-2. Script Marketplace [S42] — ScriptVault is a manager, not a platform. GreasyFork integration serves the same need.
- UC-3. AI-Assisted Script Editing [S43] — Must be opt-in, local-first. Reconsider when on-device LLMs are practical.
- UC-4. Collaborative/Team Sharing — Small user base. Existing sync covers the 90% case.
- R-1. WXT Migration [S45] — Conflicts with concatenated service worker architecture. Revisit only if codebase moves to multi-file workers.
- R-2. GM3 API Compatibility [S05] — Synchronous APIs conflict with MV3 isolation. TM/VM have moved on.
- R-3. Safari via Polyfill [S40] — Safari lacks
userScriptsAPI. A polyfill cannot bridge this. - R-4. Chrome Sync Provider [S46] — 100KB quota unsuitable for scripts. Seven other providers exist.
- R-5. Built-in Ad Blocking — uBlock Origin's domain.
@webRequestDNR rules cover per-script blocking.
| Cycle | Angle | Local evidence | External signal | Roadmap changes |
|---|---|---|---|---|
| 45 | Setup/onboarding reliability | pages/dashboard.js, pages/popup.js, src/background/registration.ts, README.md support matrix |
Chrome 138+ per-extension Allow User Scripts; Firefox MV3 optional-only userScripts; user-script registrations are cleared on extension update [S48, S74] |
Added N-7 setup doctor and rehydration audit |
| 46 | Developer workflow | Monaco editor, generated GM typings, install provenance, local-save receipts | Tampermonkey external-editor FAQ, Tampermonkey Editors vscode.dev extension, current user complaints about copy-save-reload loops [S77, S78, S79] | Promoted local file/watch work into X-8 |
| 47 | Distribution and publishing | Greasy Fork/OpenUserJS discovery and install provenance in dashboard/install UI | Greasy Fork documents read-only API plus prefilled update form requiring user session cookie [S76] | Added X-9 and corrected L-10 from direct API to publish handoff |
| 48 | Implementation decomposition | pages/dashboard.js editor save/install paths, pages/dashboard.html editor toolbar, src/background/core.ts, docs/release-runbook.md, trust receipt tests |
Chrome File System Access user-gesture/support constraints, Greasy Fork prefill form rules, Chrome/Firefox userScripts update clearing, CWS remote-code policy [S47, S74, S75, S76, S81] | Added N-8 and expanded X-8/X-9 into implementation slices |
| 49 | Compliant background execution architecture | src/background/background-runner*.ts, src/background/background-wrapper.ts, offscreen.js, scripts/check-cws-remote-code.mjs, docs/background-scripts-design.md, docs/cws-remote-code-compliance.md |
Chrome offscreen limits, remote-hosted-code sandbox guidance, ScriptCat background/cron docs [S82, S83, S84] | Expanded X-2 with sandbox-runner candidate, rejected alternatives, and prototype acceptance gates |
| 50 | SPA navigation support | src/background/wrapper-builder.ts, src/background/registration.ts, content.js, tests/wrapper-dom-security.test.js, tests/match-top-39-11.test.js, README/dashboard examples |
Navigation API route events, same-origin scope, and extension webNavigation permission tradeoffs [S85, S86, S87, S88] |
Expanded X-3 from feature note into implementation-ready URL-change contract and verification gates |
| 51 | Local-save trust receipts | pages/dashboard.js, src/background/core.ts, src/background/trust-receipt.ts, src/types/messages.ts, src/types/script.ts, src/modules/sync-easycloud.ts, tests/trust-receipt*.test.js, tests/install-source.test.js, tests/support-snapshot-redaction.test.js, tests/reregister-script.test.js |
File System Access user-gesture/handle-storage limits and CWS user-data/privacy disclosure expectations [S75, S81, S89, S90] | Expanded N-8 with local-source override, autosave coalescing, export/sync redaction, and receipt-operation test gates |
| 52 | Developer workspace/local file binding | pages/dashboard.js, pages/dashboard.html, pages/dashboard.css, src/types/script.ts, src/background/core.ts, src/background/import-export.ts, src/background/cloud-sync.ts, src/modules/sync-easycloud.ts, src/storage/script-db.ts, cloud-sync/export tests |
File System Access stored-handle, permission-persistence, user-gesture, secure-context, and CWS user-data disclosure constraints [S81, S89, S90, S91, S92] | Expanded X-8 with local-only binding store, permission-state UI, refresh-only first release, and export/sync redaction gates |
| 53 | Setup rehydration evidence | src/background/core.ts, background.core.js, background.js, src/types/messages.ts, tests/local-health-report.test.js, dashboard support snapshot export |
Chrome userScripts docs, Chrome 138+ Allow User Scripts, Firefox optional-only userScripts, and update-time registration clearing [S47, S48, S74] |
Implemented aggregate last-registration-sweep evidence in local health reports and support snapshots |
| 54 | Local-save receipt implementation | pages/dashboard.js, src/background/core.ts, src/background/trust-receipt.ts, src/types/messages.ts, src/types/script.ts, tests/trust-receipt.test.js, tests/local-save-trust-receipt.test.js |
CWS user-data disclosure and File System Access user-gesture/stored-handle constraints [S81, S89, S91] | Shipped explicit local editor receipt payloads and metadata-fallback suppression for manual saves/autosaves |
| 55 | Autosave receipt coalescing | pages/dashboard.js, src/background/core.ts, src/types/messages.ts, tests/local-save-trust-receipt.test.js, tests/reregister-script.test.js |
CWS local-storage disclosure plus File System Access handle permission recheck and persistent-permission behavior [S81, S89, S91] | Added ephemeral autosave coalescing keys, in-memory rollback-history reuse, and reregister/export-safety source guards |
| 56 | Local workspace binding store | src/storage/idb.ts, src/storage/script-db.ts, src/modules/storage.ts, src/background/core.ts, modules/storage.js, tests/storage.test.js, export/sync/support tests |
File handles are serializable to IndexedDB; permission must be rechecked with queryPermission()/requestPermission(); local sensitive data remains disclosure-relevant [S81, S89, S91] |
Added local-only binding storage plus JSON export, CloudSync, EasyCloud, and support-snapshot redaction fixtures |
| 57 | Dashboard local file binding | pages/dashboard.html, pages/dashboard.js, tests/local-workspace-dashboard.test.js |
File picker support must be feature-detected and invoked from a user gesture; stored handles require permission-state rechecks; local sensitive data still needs clear disclosure [S81, S89, S91] | Added the feature-detected Bind File control, local IndexedDB handle persistence, permission summary chip, and no-save/no-code binding tests |
| 58 | Local file refresh review | pages/dashboard.html, pages/dashboard.js, tests/local-workspace-dashboard.test.js |
Stored handles may need requestPermission() from a user gesture before reads; local files should be reviewed before applying executable code [S81, S89, S91] |
Added Refresh File and Unbind, review-diff apply, permission reconnect/error summaries, no-change handling, and local-file receipt tests |
| 59 | Deep audit security | src/background/wrapper-builder.ts, src/background/core.ts, generated runtime artifacts, tests/wrapper-dom-security.test.js, docs/research-deep-audit-2026-06-06.md |
Local audit found srcdoc bypass in GM_addElement; iframe srcdoc is raw HTML rather than a normal URL attribute |
Blocked srcdoc for direct attrs and sanitized innerHTML, regenerated runtime artifacts, and pinned the bypass regression |
| 60 | Crontab execution isolation | src/background/core.ts, generated runtime artifacts, tests/crontab-next-fire.test.js, docs/research-deep-audit-2026-06-06.md |
Local audit found scheduled scripts running through chrome.scripting.executeScript in ISOLATED world, which can expose extension APIs to userscript code |
Moved scheduled execution to chrome.userScripts.execute in USER_SCRIPT world with a MAIN-world fallback only, removed the scheduled new Function path, and pinned the isolation regression |
| 61 | PublicAPI internal-host parity | src/modules/public-api.ts, modules/public-api.js, background.js, tests/public-api.test.js, tests/source-hardening-parity.test.js |
Local audit found PublicAPI's private internal-host copy missing .localhost, TEST-NET, benchmarking, Class E, and IPv4-mapped IPv6 hex cases already enforced by InternalHostGuard |
Reused the canonical isInternalHost guard for trusted origins, web install URLs, and webhook URLs, regenerated runtime artifacts, and pinned behavior/source parity regressions |
| 62 | S3 settings validation | src/config/settings-schema.json, pages/dashboard.html, pages/dashboard.js, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
WCAG 2.1 SC 3.3.1 error identification, MDN constraint validation, and aria-invalid guidance still favor field-specific text errors and custom validity [S07, S08] |
Added S3 endpoint/region/bucket/object-key validation metadata, native hints, accessible error nodes, blur hooks, and focused a11y/schema coverage |
| 63 | Sync credential validation | src/config/settings-schema.json, pages/dashboard.html, pages/dashboard.js, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
WCAG 2.1 SC 3.3.1, MDN constraint validation, and MDN aria-invalid guidance still support custom validity plus field-specific error messages [S07, S08] |
Added WebDAV, sync passphrase, and S3 credential validation metadata, error nodes, native length limits, blur hooks, and an encryption toggle guard |
| 64 | Editor select validation | src/config/settings-schema.json, pages/dashboard.html, pages/dashboard.js, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
MDN documents HTMLSelectElement.setCustomValidity() and option values as the select contract; WCAG error identification still requires text errors for invalid choices [S07, S08] |
Added validation metadata, error nodes, and save-blocking allowed-option checks for editor font size, indentation width, and tab size |
| 65 | Interval select validation | pages/dashboard.html, pages/dashboard.js, src/config/settings-schema.json, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
MDN select option values plus HTMLSelectElement.setCustomValidity() support the same allowed-option validation contract [S07, S08] |
Added validation metadata/error nodes for update check, notification delay, and externals intervals; preserved 0/"Never" with nullish fallbacks and validator conversion |
| 66 | Security select validation | pages/dashboard.html, pages/dashboard.js, src/config/settings-schema.json, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
MDN select option values plus HTMLSelectElement.setCustomValidity() support the same allowed-option validation contract, and WCAG error identification expects text feedback for invalid choices [S07, S08] |
Added validation metadata, error nodes, and save-blocking allowed-option checks for content script API, sandbox mode, CSP modification mode, and HTTP header modification mode |
| 67 | Action behavior select validation | pages/dashboard.html, pages/dashboard.js, src/config/settings-schema.json, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
MDN select option values plus HTMLSelectElement.setCustomValidity() support the same allowed-option validation contract, and WCAG error identification expects text feedback for invalid choices [S07, S08] |
Added validation metadata, error nodes, and save-blocking allowed-option checks for default tab type, local file, cookie, communication, SRI, include, @connect, incognito, page filter, block severity, strict mode, and top-level await selects |
| 68 | Remaining select validation | pages/dashboard.html, pages/dashboard.js, src/config/settings-schema.json, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
MDN select option values plus HTMLSelectElement.setCustomValidity() support the same allowed-option validation contract, and WCAG error identification expects text feedback for invalid choices [S07, S08] |
Added validation metadata, error nodes, and save-blocking allowed-option checks for all remaining schema-backed selects; popup columns now validates before numeric conversion |
| 69 | Custom CSS validation | pages/dashboard.html, pages/dashboard.js, src/config/settings-schema.json, scripts/check-settings-schema.mjs, tests/dashboard-a11y.test.js |
WCAG error identification and MDN constraint validation support text errors, native length constraints, and custom validity for malformed free-form text [S07, S08] | Added validation metadata, maxlength, accessible error text, and save-blocking validation for custom CSS; whitespace is preserved while unsafe control characters and overlarge CSS are rejected |
| 70 | Settings validation acceptance gate | scripts/check-settings-schema.mjs, tests/settings-schema.test.js, src/config/settings-schema.json, pages/dashboard.html, pages/dashboard.js |
WCAG error identification and MDN constraint validation still require user-facing errors for malformed input controls [S07, S08] | Added the durable dashboard-backed validation-metadata requirement and closed N-1 after the repository schema passed it |
| 71 | Guarded GM.fetch | src/background/core.ts, src/background/wrapper-builder.ts, background.core.js, background.js, scripts/generate-gm-types.mjs, lib/scriptvault.d.ts, tests/gm-namespace-parity.test.js, tests/gm-types.test.js, docs/gm-namespace-parity.md |
Tampermonkey/Violentmonkey GM API parity still depends on promise-style network helpers, but ScriptVault must keep network access behind GM_xmlhttpRequest policy [S10, S11, S12] |
Added GM.fetch/GM_fetch over the existing XHR bridge, pinned that no background GM_fetch action exists, and closed N-2 |
| 72 | SPA URL-change proof | src/background/core.ts, src/background/wrapper-builder.ts, generated runtime artifacts, tests/urlchange-wrapper.test.js, README.md |
Navigation API route events plus history/popstate/hashchange fallbacks remain the least-permission page-level path for SPA userscript reruns [S85, S86, S87, S88] | Added a shared URL-change scheduler, microtask/frame rechecks, duplicate suppression, jsdom coverage, and README author examples for window.onurlchange |
| 73 | Monaco package guard | scripts/check-monaco-package-contract.mjs, tests/monaco-package-contract.test.js, package.json, docs/monaco-esm-migration-plan.md |
Monaco AMD remains deprecated, but the current v3.12 packaging contract must stay local and Firefox-safe until the ESM bundle is deliberately switched [S17, S24] | Added a static gate for local AMD Chromium packaging, remote/CDN sandbox rejection, Firefox Monaco exclusion, npm-check wiring, and plan drift |
| 74 | Monaco ESM prototype | src/editor/monaco-esm-entry.ts, esbuild.config.mjs, scripts/check-monaco-esm-prototype.mjs, tests/monaco-esm-build.test.js, docs/audit/monaco-esm-prototype-2026-06-06.json |
Monaco ESM needs a local bundled editor plus file-backed workers before the sandbox can leave AMD [S17, S24] | Added an ignored lib/monaco-esm/ prototype build, deterministic worker outputs, font loader handling, post-build evidence checks, and size/layout evidence |
| 75 | Monaco ESM size budget | scripts/check-monaco-esm-prototype.mjs, tests/monaco-esm-prototype-check.test.js, docs/audit/monaco-esm-prototype-2026-06-06.json, docs/monaco-esm-migration-plan.md |
The full JavaScript/TypeScript worker preserves userscript language features, but package growth must be explicit before switching from AMD [S17, S24] | Selected the full-worker Chromium strategy, added total/compressed/per-file budgets, recorded gzip evidence, and pinned budget regressions |
| 76 | Monaco ESM sandbox switch | pages/editor-sandbox.html, esbuild.config.mjs, scripts/check-monaco-package-contract.mjs, tests/monaco-esm-plan.test.js, docs/cws-remote-code-compliance.md |
The sandbox can leave deprecated AMD only if it imports packaged ESM assets and keeps fallback behavior [S17, S24] | Switched the sandbox to local ESM CSS/import loading, removed AMD copy steps, rejected AMD loader regressions, and added sandbox script parsing coverage |
| 77 | Monaco ESM fallback harness | tests/monaco-esm-sandbox-loader.test.js, pages/editor-sandbox.html |
Browser-profile proof can be blocked, but the loader/fallback contract still needs deterministic coverage [S17, S24] | Added a VM/DOM harness that executes the sandbox script, validates ESM path requests, ready posting, and missing-bundle fallback routing |
| 78 | Monaco ESM Chromium sandbox smoke | tests/e2e/monaco-esm-sandbox.spec.js, tests/e2e/helpers/extension-fixture.js |
The ESM sandbox switch must run in a real Chromium extension page, not only jsdom/VM harnesses [S17, S24] | Added a Playwright extension-page smoke for the packaged sandbox ready path and routed missing-bundle fallback path |
| 79 | Monaco adapter dashboard smoke | tests/e2e/monaco-adapter-dashboard.spec.js |
The ESM editor must persist real dashboard edits through the CodeMirror-compatible Monaco adapter [S17, S24] | Added a Playwright dashboard smoke for edit-open, adapter readiness, toolbar save, reload, and adapter value persistence |
| 80 | browser namespace alias |
src/shared/utils.ts, shared/utils.js, scripts/generate-ts-runtime-modules.mjs, pages/dashboard-firefox-compat.js, focused tests |
Chrome 148 exposes browser, so Chrome/Firefox code should converge without exposing extension APIs to userscript/page worlds [S25] |
Added a generated extension-context alias, fixed Chromium-vs-Firefox detection when browser.runtime exists, and pinned wrapper boundary coverage |
| 81 | Trusted Types author docs | README.md, pages/dashboard.html, tests/trusted-types-docs.test.js |
MAIN-world scripts on Trusted Types pages need explicit author guidance, while ScriptVault's default USER_SCRIPT path should stay documented as the safer default [S26] | Added README and Help guidance plus a static test that pins the docs and confirms no runtime policy shim was introduced |
| 82 | Subscription refresh scheduling | src/background/core.ts, settings defaults/schema/types, dashboard subscription controls, generated runtime artifacts, focused tests |
Chrome alarms remain the MV3-safe periodic work primitive, with same-name replacement and a 30-second minimum interval in current Chrome [S93] | Added a managed subscription refresh alarm, visible auto-refresh/interval controls, feed health labels, scheduler resync on add/remove/settings changes, and static contract coverage |
| 83 | Local workspace refresh status | pages/dashboard.js, src/storage/script-db.ts, modules/storage.js, focused dashboard/storage tests |
File System Access handles can persist in IndexedDB, but permission/read state must be visible because access can revert to prompt or fail between sessions [S81, S91, S92] | Added explicit bound/unchanged/applied/cancelled/error refresh state summaries for the editor local-file chip without exposing handles or absolute paths |
| 84 | Local workspace health evidence | src/background/core.ts, pages/dashboard.js, focused health/support tests |
Support snapshots need aggregate local-workspace evidence without file handles, paths, or local names [S81, S91, S92] | Added local workspace health counts, refresh-age buckets, and support-snapshot sanitization unless script inventory is opted in |
| 85 | Local refresh acceptance hardening | pages/dashboard.js, src/background/core.ts, focused local-workspace/local-health tests |
File System Access reads must reject oversized executable files before loading text, and parse/apply failures need distinct operator feedback [S81, S91, S92] | Added the 5 MB bound-file read cap, too-large and parse-failed status paths, and regression coverage for normal local apply registration |
| 86 | Greasy Fork publish preflight | pages/dashboard.html, pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Greasy Fork documents read-only APIs plus multipart prefilled update forms, not a credentialed write API [S76] | Added editor publish preflight, metadata validation, new/update target detection, exact code preview, copy/download fallback, and static guards against background publish endpoints |
| 87 | Greasy Fork publication receipt | pages/dashboard.html, pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Store-safe handoff needs a local receipt only after user-confirmed submission, without storing submitted code or account/session material [S76] | Added post-handoff confirmation, local-only publication receipt storage, Info-panel reload display, receipt trimming, and Chromium dashboard smoke coverage |
| 88 | Greasy Fork receipt history | pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Greasy Fork still documents read-only APIs plus session-cookie prefilled form POSTs, so local fallback handling must not claim a credentialed publish API [S76] | Added per-script publication receipt history display, local-only clear-history management, source/account redaction copy, focused static coverage, and Chromium extension smoke coverage |
| 89 | Greasy Fork receipt summary fallback | pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Clipboard writes are a user-initiated local action, while Greasy Fork publication still happens only through the user-reviewed prefilled form [S76, S94] | Added copyable sanitized receipt summaries for local publication history and Chromium smoke coverage that proves copied text omits source/account/session data |
| 90 | Greasy Fork receipt export fallback | pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Local Blob URLs plus anchor download preserve a browser-native export path for sanitized local receipt text [S76, S95, S96] |
Added downloadable sanitized receipt summaries, safe receipt filenames, object URL revocation, focused static coverage, and Chromium smoke coverage for exported text redaction |
| 91 | Greasy Fork session-check polish | pages/dashboard.js, tests/greasyfork-publish-handoff.test.js |
Greasy Fork publication still depends on the user's browser session, so the preflight modal should let users open Greasy Fork without posting script data [S76] | Added a user-initiated Open Greasy Fork modal action that opens only the base URL with noopener/noreferrer, plus focused static coverage and Chromium smoke proof |
| 92 | Enterprise policy provisioning | manifest.json, managed-storage-schema.json, src/background/core.ts, pages/dashboard.js, docs/enterprise-policy-provisioning.md, focused manifest/runtime tests |
Chrome managed storage requires a manifest-declared schema; managed storage is read-only policy data and can be restricted to trusted contexts [S28, S97, S98] | Added schema wiring, trusted-context access narrowing, deterministic managed install tags, a dashboard Managed badge, and administrator docs |
| 93 | Enterprise policy diagnostics | src/background/core.ts, src/types/messages.ts, local health/support snapshot tests |
Chrome policy deployment is normally inspected at chrome://policy, but ScriptVault support snapshots need only aggregate extension-side evidence [S97, S98, S99] |
Added managed policy support/read/configuration/install counts and warning signals to local health without exposing policy values |
| 94 | GM value-sync support snapshot allowlist | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Chrome extension diagnostics should keep privileged userscript/storage state behind extension-controlled messaging and export only support-safe aggregates [S47, S98] | Added an explicit dashboard-side allowlist for GM value-sync local-health export data, including clamped retry-ready last-result evidence and known warning IDs only |
| 95 | GM value-sync support summary polish | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Support operators need pre-export visibility into aggregate local-health diagnostics while the privacy envelope keeps script/value data out of the UI and export path [S47, S98] | Cached local health during utilities refresh/export and added aggregate GM value-sync retry readiness to the support snapshot summary |
| 96 | GM value-sync retry-age diagnostics | src/background/core.ts, src/types/messages.ts, generated runtime artifacts, pages/dashboard.js, focused local-health/support tests |
Support diagnostics should distinguish fresh retry-ready write failures from stale ones while still exporting only aggregate local-health evidence [S47, S98] | Added sanitized retry-age minutes/buckets to last-result health and support-summary output |
| 97 | GM value-sync bounded retry history | src/background/core.ts, src/types/messages.ts, generated runtime artifacts, pages/dashboard.js, focused local-health/support tests |
Chrome storage quota guidance favors small JSON-serializable local state, and CWS user-data policy favors aggregate/anonymized operational diagnostics [S47, S98] | Added a five-entry aggregate retry-history store, local-health summary counts, support-snapshot allowlisting, and clear-all cleanup |
| 98 | GM value-sync stale retry cleanup | src/background/core.ts, src/types/messages.ts, generated runtime artifacts, pages/dashboard.js, focused local-health/support tests |
Chrome storage APIs support extension-local JSON state, and CWS user-data guidance favors minimal aggregate diagnostics for support exports [S89, S97] | Added seven-day retry-history retention, stale-entry pruning on sync persistence, local-health retained/stale counts, and support-snapshot allowlisting |
| 99 | GM value-sync retry resolution drill | tests/source-cloud-sync.test.js |
Empty-local-only retries should prove a transient write failure resolves on a later sync without widening stored or exported diagnostics [S89, S97] | Extended the write-failure fixture through a second sync that applies the preserved remote bundle and keeps retry result output identifier/value-free |
| 100 | GM value-sync retry-resolution health summaries | src/background/core.ts, src/types/messages.ts, generated runtime artifacts, pages/dashboard.js, focused local-health/support tests |
Support diagnostics need aggregate proof that retry-ready failures later cleared without exporting scripts, values, account data, or provider errors [S89, S97] | Added local retry-resolution records, support-safe health/export summaries, age buckets, and clear-all cleanup for resolution evidence |
| 101 | GM value-sync retry-resolution stale cleanup | src/background/core.ts, generated runtime artifacts, tests/local-health-report.test.js |
Local diagnostic records should keep the same retention boundary as support-safe history evidence [S89, S97] | Removed stale or malformed retry-resolution records during sync result persistence when no fresh resolution is written |
| 102 | GM value-sync resolution-history support evidence | src/background/core.ts, src/types/messages.ts, generated runtime artifacts, pages/dashboard.js, focused local-health/support tests |
Support diagnostics need bounded recent recovery evidence without exporting script/value/provider details [S89, S97] | Added a five-entry aggregate retry-resolution history, local-health/support summaries, stale-count reporting, and clear-all cleanup |
| 103 | GM value-sync retry-resolution export hardening | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Support exports should not overstate retry-resolution evidence from malformed local-health input [S47, S98] | Rejected malformed resolution records without prior retry-ready evidence, zeroed retained-history totals when entries sanitize to zero, and normalized history timestamp ranges |
| 104 | GM value-sync retry-resolution source invariants | tests/local-health-report.test.js |
Retry-resolution evidence must remain tied to successful clean retries after prior retry-ready history [S89, S97] | Added source-contract coverage for resolution record gates and persistence-time stale pruning of retry-resolution history |
| 105 | GM value-sync retry-resolution support summary polish | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Operators should see aggregate stale/excluded recovery evidence before exporting support snapshots [S47, S98] | Added support-card labels for historical retry-resolution applies and stale retry-resolution-history events excluded |
| 106 | GM value-sync support-summary clamp hardening | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Pre-export support summaries should format only non-negative integer aggregate counts [S47, S98] | Re-clamped all displayed GM value-sync support-summary counts before formatting |
| 107 | GM value-sync retry-resolution stale-history evidence | tests/local-health-report.test.js |
Stale retry-resolution history should stay visible only as aggregate exclusion evidence [S89, S97] | Added local-health source coverage for include-stale reads, stale exclusion counts, retained-entry filtering, typed output, and privacy flags |
| 108 | GM value-sync support-summary schema drift coverage | tests/support-snapshot-redaction.test.js |
Pre-export summaries should read only reviewed sanitized fields, not raw local-health input [S47, S98] | Pinned the exact sanitized GM value fields read by the support summary and rejected raw local-health field access |
| 109 | GM value-sync support export schema drift coverage | tests/support-snapshot-redaction.test.js |
Support exports should expose only reviewed aggregate diagnostic fields at every nested level [S47, S98] | Pinned the exact returned sanitizer keys for GM value sync, last-result, retry-resolution, retry-resolution-history, and retry-history exports |
| 110 | GM value-sync support privacy schema drift coverage | tests/support-snapshot-redaction.test.js |
Support export privacy metadata should stay explicit and false for sensitive data classes [S47, S98] | Pinned the exact nested privacy keys and false values for GM value sync support exports |
| 111 | GM value-sync support warning-count schema drift coverage | tests/support-snapshot-redaction.test.js |
Support exports should allow only reviewed GM value warning identifiers [S47, S98] | Pinned the exact warning-count allowlist and rejected raw warning-count key iteration |
| 112 | GM value-sync retry-age bucket schema drift coverage | tests/support-snapshot-redaction.test.js |
Retry-ready and retry-resolution support exports should share reviewed age buckets [S47, S98] | Pinned the exact retry-age bucket allowlist and unknown fallback for support exports |
| 113 | GM value-sync retry-resolution cleanup guard | tests/local-health-report.test.js |
Stale or malformed local retry-resolution records should be removed without writing null diagnostics [S89, S97] | Pinned the single-record cleanup helper and persistence-time remove path |
| 114 | GM value-sync retry-resolution history storage contract | tests/local-health-report.test.js |
Local retry-resolution history should retain only aggregate recovery evidence [S89, S97] | Pinned the exact stored retry-resolution history entry keys and rejected privacy/raw identifier fields |
| 115 | GM value-sync support summary phrase drift coverage | tests/support-snapshot-redaction.test.js |
Pre-export summaries should use only reviewed aggregate diagnostic wording [S47, S98] | Pinned fallback, opt-in, retry, history, stale, and capped-value summary phrases while rejecting raw identifier labels |
| 116 | GM value-sync support summary count-order coverage | tests/support-snapshot-redaction.test.js |
Pre-export summaries should present aggregate diagnostics in reviewed order [S47, S98] | Pinned baseline, retry, resolution, history, stale, warning, and final join order |
| 117 | GM value-sync support summary warning-total coverage | tests/support-snapshot-redaction.test.js |
Warning totals should be computed only from sanitized support-export warning counts [S47, S98] | Pinned sanitized warning-count reduction, shared count clamping, and capped/excluded aggregate wording |
| 118 | GM value-sync retry-resolution history type schema coverage | tests/local-health-report.test.js |
Typed local-health responses should expose only reviewed aggregate retry-resolution history fields [S89, S97] | Pinned retry-resolution-history response fields, privacy keys, and raw identifier exclusions |
| 119 | GM value-sync support summary fallback-state coverage | tests/support-snapshot-redaction.test.js |
Pre-export summaries should return reviewed fallback states before formatting counts [S47, S98] | Pinned sanitize-first unchecked/unavailable fallback order before aggregate count formatting |
| 120 | GM value-sync retry-resolution typed privacy coverage | tests/local-health-report.test.js |
Typed local-health responses should expose only reviewed aggregate retry-resolution fields and privacy keys [S89, S97] | Pinned single retry-resolution response fields, privacy keys, and raw identifier exclusions |
| 121 | GM value-sync retry-history typed privacy coverage | tests/local-health-report.test.js |
Typed local-health responses should expose only reviewed aggregate retry-history fields and privacy keys [S89, S97] | Pinned retry-history response fields, privacy keys, and raw identifier exclusions |
| 122 | GM value-sync typed privacy coverage | tests/local-health-report.test.js |
Typed local-health responses should keep the top-level GM value sync diagnostic envelope reviewed [S89, S97] | Pinned top-level GM value sync response fields, privacy keys, and raw identifier exclusions |
| 123 | GM value-sync last-result typed schema coverage | tests/local-health-report.test.js |
Typed local-health responses should keep persisted sync result diagnostics aggregate-only [S89, S97] | Pinned last-result response fields, retry-age fields, and raw identifier/privacy exclusions |
| 124 | GM value-sync support unavailable-state wording coverage | tests/support-snapshot-redaction.test.js |
Unavailable pre-export summaries should stay generic and support-safe [S47, S98] | Pinned generic unavailable wording and rejected provider/account/credential/script/key/error detail |
| 125 | GM value-sync last-result support export clamp coverage | tests/support-snapshot-redaction.test.js |
Support exports should not overstate retry-ready last-result evidence [S47, S98] | Pinned retry-ready clamping to sanitized failure/preserved counts and retry-age gating |
| 126 | GM value-sync support unchecked-state wording coverage | tests/support-snapshot-redaction.test.js |
Unchecked pre-export summaries should stay generic and support-safe [S47, S98] | Pinned generic unchecked wording and rejected provider/account/credential/script/key/error detail |
| 127 | GM value-sync last-result timestamp sanitizer coverage | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Support exports should normalize last-result timestamps through the reviewed helper [S47, S98] | Routed last-result timestamp export through sanitizeSupportSnapshotTimestamp() and pinned the shared-helper path |
| 128 | GM value-sync retry-age unknown bucket coverage | src/background/core.ts, background.core.js, tests/local-health-report.test.js |
Retry-ready diagnostics with missing timestamps should not be labeled fresh [S47, S98] | Classified null/undefined retry ages as unknown and pinned the local-health last-result gating path |
| 129 | GM value-sync support nested-field coverage | tests/support-snapshot-redaction.test.js |
Pre-export support summaries should read only reviewed nested sanitized fields [S47, S98] | Pinned nested last-result, retry-resolution, retry-resolution-history, and retry-history field allowlists |
| 130 | GM value-sync retry-history timestamp retention coverage | tests/support-snapshot-redaction.test.js |
Support exports should not retain retry-history timestamps when retained history is empty [S47, S98] | Pinned retained-history timestamp helper use for retry and retry-resolution histories |
| 131 | GM value-sync retry-resolution timestamp range coverage | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Support exports should not expose impossible retry-resolution timestamp ranges [S47, S98] | Clamped retry-resolution latest retry timestamp to the resolution timestamp before export |
| 132 | GM value-sync retry-resolution age-bucket gating | pages/dashboard.js, tests/support-snapshot-redaction.test.js |
Support exports should not retain retry-resolution age buckets without age minutes [S47, S98] | Gated retry-resolution age bucket export on retained age minutes and defaulted missing evidence to unknown |
| Capability | ScriptVault | Tampermonkey | Violentmonkey | ScriptCat |
|---|---|---|---|---|
| MV3 native | Yes | Yes | No (dead on Chrome) | Yes |
| Open source | MIT | No | MIT | GPL-3.0 |
| Chrome Web Store | Published | Published (12M) | Blocked (MV2) | Published |
| GM API (35+) | Yes | Yes | Yes | Yes |
| Monaco editor | Yes | No | Yes (MV2) | No |
| DevTools panel | Yes | No | No | No |
| Side panel | Yes | No | No | No |
| Script signing (Ed25519) | Yes | No | No | No |
| AST analysis (31 detectors) | Yes | No | No | No |
| Cloud sync providers | 7 | 6 | 4 | Built-in |
| Update diff + review inbox | Yes | No | No (most-requested) | No |
| Version rollback | Yes (3) | No | No | No |
| Import quarantine | Yes | No | No | No |
| Sigstore provenance | Yes | No | No | No |
| Workspaces | Yes | No | No | No |
| Command palette | Yes | No | No | No |
| Performance budgets | Yes | No | No | No |
| Background/cron scripts | Partial | No | No | Yes |
| Enterprise provisioning | Yes (Chrome/Edge) | Yes (v5.5) | No | No |
| MCP/AI integration | No | Yes (v5.5) | No | No |
| Firefox published | No (AMO-ready) | Yes | Yes | Yes |
| Edge published | No (pkg-ready) | Yes | No | Yes |
Items below were identified by exhaustive repo walk + 35+ external sources. Duplicates against existing Now/Next/Later/UC/Rejected tiers were filtered. Each item carries impact (1-5), effort (S/M/L/XL), and tier recommendation.
Items below identified by exhaustive repo walk + 40+ external sources across competitors (Tampermonkey v5.5.0, Violentmonkey MV3 status, ScriptCat v1.4.0-beta.4), Chrome 140-148 APIs, Firefox 149-153, security landscape (NSA MCP guidance, EU CRA Sep 2026, Trusted Types Baseline), and dependency changelogs. Deduplicated against all existing tiers (Now through Rejected) and all prior RD-1..RD-14 additions.
Verified-but-unfixed items from the 2026-07-02 deep audit. The audit shipped fixes for GM handler auth binding, attribute-injection XSS, the Chrome-as-Firefox misdetection, editor keystroke/cursor/undo bugs, trash-restore + backup-import data loss, and the cloud-sync tombstone-resurrection data loss (see CHANGELOG v3.16.0). The items below were confirmed reachable but deferred as higher-risk or larger than an audit fix.
Added 2026-07-01. Items below are net-new from the 2026-07-01 research pass and do not duplicate the existing Now/Next/Later/N-/X-/L-/UC- items. Sources in the Research-Driven Sources (2026-07-01) appendix.
Net-new from the 2026-07-02 pass (v3.16.0). Verified as not already implemented in code and not duplicating the Next tier, the Deep Audit Findings (2026-07-02), or the 2026-07-01 Research-Driven Additions. Cross-references: the Next-tier "editor cursor position stuck at Ln 1, Col 1" item is RESOLVED in v3.16.0 (monaco-adapter now caches the real cursor) — treat as done. One-click GreasyFork/OpenUserJS publish (VM #2425) is already covered by X-9 (publish handoff) — not re-added. UC-3 (AI-Assisted Script Editing) precondition "on-device LLMs practical" is now MET (Chrome Prompt API stable for extensions since Chrome 138) — see the P2 AI item below to promote it.
| ID | Source | URL |
|---|---|---|
| RD28-01 | TM per-script isolated cookie jars | Tampermonkey/tampermonkey#2815 |
| RD28-02 | TM fake crypto-exploit userscripts | Tampermonkey/tampermonkey#2783 |
| RD28-03 | TM streaming GM.fetch request | Tampermonkey/tampermonkey#1278 |
| RD28-04 | TM MV3 GM_xhr DNR serialization | Tampermonkey/tampermonkey#2215 |
| RD28-05 | ScriptCat AI Agent | scriptscat/scriptcat#1324 |
| RD28-06 | ScriptCat request-scoped DNR | scriptscat/scriptcat#1377 |
| RD28-07 | ScriptCat v1.4.0 release notes | https://github.com/scriptscat/scriptcat/releases/tag/v1.4.0 |
| RD28-08 | GreasyFork SRI enforcement request | greasyfork-org/greasyfork#1070 |
| RD28-09 | Chrome Prompt API (Gemini Nano) | https://developer.chrome.com/docs/ai/prompt-api |
| RD28-10 | Chrome built-in AI overview | https://developer.chrome.com/docs/ai/built-in |
| RD28-11 | VM restrict-to-current-site cluster | violentmonkey/violentmonkey#2410 |
| RD28-12 | GreasyFork account-takeover propagation | greasyfork-org/greasyfork#682 |
| RD28-13 | VM local-directory sync | violentmonkey/violentmonkey#2125 |
| RD28-14 | VM git-server sync | violentmonkey/violentmonkey#2176 |
| RD28-15 | File System Observer API | https://developer.chrome.com/blog/file-system-observer |
| RD28-16 | VM hold-execution-until-sync | violentmonkey/violentmonkey#2067 |
| RD28-17 | TM partitioned-cookie/CHIPS download | Tampermonkey/tampermonkey#2419 |
| RD28-18 | TM SR-inaccessible delete controls | Tampermonkey/tampermonkey#2813 |
| RD28-19 | TM Firefox container force-enable | Tampermonkey/tampermonkey#2792 |
| RD28-20 | Same-document View Transitions Baseline | https://web.dev/blog/same-document-view-transitions-are-now-baseline-newly-available |
Net-new from the 2026-07-09 pass. Deduped against ROADMAP.md, Roadmap_Blocked.md, RESEARCH.md, README v3.18.0, and existing RD28 sources.
Net-new from the 2026-07-16 pass (baseline v3.20.0). The prior RESEARCH.md's 10
opportunities all shipped (telemetry auth, vendor-import quarantine, URL
minimization, typed dispatch, source maps, WCAG 2.2 gate, locale/plural catalogs,
fail-closed smokes, unified release preflight, TS7). A competitive sweep confirmed
the 2026-H2 GM-API deltas competitors added — GM_audio, request-scoped cookie
partitioning + anonymous downloads, @unwrap, Navigation-API onurlchange,
CAT_userConfig, on-disk auto-reload (FileSystemObserver) — are ALREADY shipped
(verified in wrapper-builder.ts/parser/core.ts/dashboard.js), so they are
not re-added. SBOM (check-cra-sbom.mjs) already exists — not re-added. Items
below are verified against source and deduped against ROADMAP.md, Roadmap_Blocked.md,
CLAUDE.md audit history, and RESEARCH.md rejected ideas.
- ScriptCat niche directives —
CAT_fileStorage(per-script file storage),@storageName(shared cross-script namespace),@definition(.d.tseditor hints),@early-start. Verified absent; low demand and marginal over Chrome's existingdocument_start+ shippedlib/scriptvault.d.ts. Reconsider on user signal. Source: docs.scriptcat.org/docs/dev/meta. - On-device "explain this script" via Chrome Prompt API (Chrome 148 stable) — could summarize what an installed/updating script does using the on-device model, strictly opt-in and local. Philosophy tension with any data flow; gate behind explicit per-use consent. Source: https://developer.chrome.com/docs/ai/prompt-api
Net-new from the 2026-07-22 research pass (baseline v3.22.0). Verified against source and deduped against ROADMAP.md, Roadmap_Blocked.md, and the shipped v3.21.0/v3.22.0 work. The prior RESEARCH.md top-10 (persistent UserCSS, esbuild CVE bump, SECURITY.md, permission-drift gate, npm-worm hardening, template-token sanitization, backup gzip, update-body AST re-scan) all SHIPPED — not re-added. Firefox 153 shipped 2026-07-21, unblocking four items previously parked in Roadmap_Blocked.md; those are re-surfaced here as actionable (P2 FF153 cluster).
Additional verified findings from a same-day analyzer/storage/scope re-pass. Not duplicates of the items above.
Verified-by-observation audit pass against v3.23.1. Baseline before this pass:
npm run check green (227 files / 2434 tests), npm run smoke:firefox green,
npm run smoke:dashboard and npm run smoke:editor green, working tree clean —
no pre-existing failures. Live verification used Firefox Developer Edition
154.0b1 via geckodriver 0.37.1 and headless Chromium via the repo's
puppeteer-core. Audit-only: no source file was modified.
-
P3 — Pending-updates size cap measures UTF-16 code units, not bytes, and cites a quota that does not apply Category: reliability Where:
src/background/core.ts(UpdateSystem._MAX_PENDING_TOTAL_BYTESand the eviction loop); generated atbackground.core.js:1799andbackground.core.js:2149-2155Problem: The cap added in commit405a0f6computesJSON.stringify(normalized).length, which counts UTF-16 code units, and compares it against a budget named_MAX_PENDING_TOTAL_BYTES. For non-ASCII script bodies the real UTF-8 footprint is up to ~3x the measured value, so a store the code believes is 8 MB can exceed 20 MB on disk — the eviction never fires when it is most needed. The warning string also reports the wrong unit. Separately, the justifying comment says "keep the store safely under the default chrome.storage.local 10 MB quota", but both manifests declareunlimitedStorage, so that quota does not apply and the stated rationale is wrong. Evidence:node -eon a representative non-ASCII payload:JSON.stringifylength 101 vsBuffer.byteLength(...,'utf8')281 — a 2.78x undercount.grep -n unlimitedStorage manifest.json manifest-firefox.jsonconfirms the permission is declared in both. Fix: Measure real bytes withnew TextEncoder().encode(json).byteLength(the codebase already uses this in_scriptSourceByteLength), and correct the comment to state the actual reason for the bound (bounding service-worker memory and write cost underunlimitedStorage), not a quota that is not in force. Acceptance: A test queues pending updates whose UTF-8 size exceeds the budget while their UTF-16 length does not, and asserts eviction occurs. Confidence: Verified Effort: S -
P3 — Light-theme skip link falls just under AA contrast Category: a11y Where:
pages/dashboard-a11y.js:30-48— the.a11y-skip-linkrule inside the injectedSTYLEStemplate (color: var(--accent-blue)onbackground: var(--bg-header)); the element itself is created atpages/dashboard-a11y.js:198Problem: The "Skip to main content" link rendersrgb(37,99,235)onrgb(228,228,228)for a 4.07:1 ratio at 14px, below the 4.5:1 AA requirement for normal text. This is the first control a keyboard user reaches on the page. Evidence: Computed live in the loaded extension in light theme by walking to the first ancestor with an opaque background and applying the WCAG relative-luminance formula: ratio 4.07, required 4.5, font-size 14px. It was the only genuine failure the sweep found in light theme — the other flagged elements were false positives from gradient-backed buttons whosebackgroundColoris transparent. Fix: Darken the skip-link foreground in light theme to a blue meeting 4.5:1 against its actual background (or give the link an opaque high-contrast background of its own when focused), using the theme's existing accent tokens rather than a new literal. Acceptance: The focused skip link measures at least 4.5:1 in light theme; the other three themes stay unchanged. Confidence: Verified Effort: S
Scope not covered by the 2026-08-02 pass. Not findings; each needs its own audit.
-
P3 — Unaudited: cloud sync providers end to end against live services Category: testing Where:
modules/sync-providers.js,src/modules/sync-providers.ts,modules/cloud-sync.jsProblem: The 2026-08-02 pass verified only the WebDAV path thatsmoke:firefoxexercises against a local stub server. Google Drive, Dropbox, OneDrive, Easy Cloud, and S3 were not exercised against real endpoints, so OAuth refresh, quota, and conflict behavior on those providers is unverified by this pass. Evidence: The Firefox smoke'swebdavscenario is the only provider with live coverage in the harness output. Fix: Audit each provider's token refresh, error mapping, and 3-way merge against a real or high-fidelity fake endpoint. Acceptance: Each provider has an observed pass/fail record rather than an inference from shared code. Confidence: Needs-repro Effort: L -
P3 — Unaudited: Monaco editor interaction surface and the DevTools panel in a real DevTools host Category: testing Where:
pages/editor-sandbox.html,pages/monaco-adapter.js,pages/devtools-panel.jsProblem:smoke:editorwas run and passes (overlay geometry, 14 controls hit-tested, close works), but editor behaviors beyond that harness — find/replace history, undo across tab switches, Vim mode, large-file handling — were not exercised. The DevTools panel was loaded as a bare page, wherechrome.devtools.inspectedWindowis absent; its real behavior inside an attached DevTools host is unverified. Evidence: The panel rendered its empty state correctly as a standalone page, which does not exercise the inspected-window code paths. Fix: Drive the editor through its documented interactions, and load the DevTools panel through an actual DevTools session. Acceptance: Both surfaces have observed coverage of their primary interactions. Confidence: Needs-repro Effort: M -
P3 — Unaudited: install/update flow driven from a real
.user.jsnavigation Category: testing Where:pages/install.js, install interception insrc/background/core.tsProblem: The install page was audited only in its no-pending-install error state, which is handled well ("No userscript was found ... Download the userscript again from its source page"). The populated review flow — permission rendering, downgrade detection,@requireprobing, trust-card provenance — was not exercised in this pass. Evidence: Loadingpages/install.htmldirectly yields the empty state by design. Fix: Drive a real.user.jsnavigation through interception into the review UI and audit the populated states, including a malformed and an oversized script. Acceptance: The populated install review has observed coverage across valid, malformed, and downgrade cases. Confidence: Needs-repro Effort: M
-
P2 — Add a bounded local-file watcher fallback with explicit watcher health Why: FileSystemObserver is optional and its current error path stops watching and falls back to manual refresh; Chrome’s documented API history and unknown/errored event semantics require a polling fallback for reliable local editing. Evidence: pages/dashboard.js localWorkspaceFileObservers and observer error handling; tests/local-workspace-dashboard.test.js; https://developer.chrome.com/blog/file-system-observer; existing File System Access contract https://developer.chrome.com/docs/capabilities/web-apis/file-system-access. Touches: pages/dashboard.js, pages/dashboard.html, src/storage/script-db.ts, src/background/core.ts only where message/state contracts require it, modules/storage.js/generated artifacts, and local-workspace/dashboard tests. Acceptance: When FileSystemObserver is unavailable or reports unknown/errored state, an active bound editor performs debounced, bounded metadata polling and routes detected changes through the existing permission check and review-diff apply flow; unbound/hidden editors do not poll, permission/read/oversize failures are visible, and tests cover unavailable, healthy, unknown, errored, unchanged, changed, and rebind cases. Complexity: M
-
P2 — Add a partial-locale and RTL regression ratchet Why: The locale gate currently proves generated parity but permits eight partial runtime catalogs at roughly 2.2%–6.1% coverage, so new UI can remain English-only or break RTL without failing CI. Evidence: scripts/check-locales.mjs, docs/locale-coverage.md, _locales/*, modules/i18n.js, and the 2026-08-02 locale:check:gate output; localization backlog quoid/userscripts#415; Stylus translation workflow https://github.com/openstyles/stylus. Touches: scripts/check-locales.mjs, docs/locale-coverage.md, tests/check-locales-report.test.js, tests/dashboard-i18n-removal.test.js, tests/sidepanel-rtl-layout.test.js, and the dashboard/popup/sidepanel localization harness. Acceptance: CI fails on a coverage regression or missing locale status, a deterministic pseudo-locale exercises the major dashboard/popup/sidepanel/editor labels, and RTL checks cover direction, accessible names, overflow, and control order while keeping partial translations explicitly labeled rather than requiring immediate full translation. Complexity: M
- P3 — Make browser-support and release-artifact references version-derived Why: README.md’s generated support matrix and release-preflight example still reference version 3.22.0 and 2026-07-16 while the live manifests are 3.23.1; readme:check passes without checking those generated references. Evidence: README.md:382, README.md:387-391, README.md:591, manifest.json, manifest-firefox.json, scripts/generate-browser-support-matrix.mjs, and scripts/check-readme-claims.mjs. Touches: README.md generated block, scripts/generate-browser-support-matrix.mjs, scripts/check-readme-claims.mjs, release preflight/version checks, and focused documentation tests. Acceptance: Regenerating the support matrix derives the version and verification date from the current release inputs, artifact filenames and examples agree with the same version, historical examples are marked as historical or removed, and the documentation gate fails on stale version/date references. Complexity: S
Deep multi-pass audit against v3.23.1. Baseline: after npm ci, npm run check is green (227 files / 2434 tests, tsc clean, all gates pass) — no pre-existing failures (an earlier red run was only an empty node_modules). npm audit --omit=optional --audit-level=high reports 16 vulns (10 high, 4 critical) — see the still-open P0 "Restore the blocking high-severity dependency-audit gate" above; not re-logged. Findings were traced against the SHIPPED source of truth: src/background/core.ts (inline, generated into background.core.js) and the promoted src/modules/*.ts / src/background/gm-*.ts handlers. Several src/background/*.ts files (wrapper-builder.ts, import-export.ts, update-checker.ts, install-handler.ts, trust-receipt.ts, parser.ts) are UNSHIPPED mirrors — mirror-only issues were dropped or folded into the mirror-drift item (last P3 below).
-
P2 — Restore/import persist their undo receipt only AFTER all mutations, so a service-worker death mid-restore leaves half-restored state with no rollback record Category: correctness Where:
src/modules/backup-scheduler.ts:1484-1491,1740-1783(restoreBackup);src/background/core.ts:5353,5497-5533,5853-5889(importScripts/importFromZip) Problem:restoreBackupsnapshots pre-restore state in memory, runs the full mutation chain (importFromZip→ N IDB writes → settings → folders → workspaces), and calls_pushReceiptonly at the very end. An MV3 SW can die at any await; on restart the user has mixed restored/pre-restore data and the receipts ledger — the only undo — holds nothing. This is exactly the multi-key-write / SW-died-between-A-and-B class the receipts feature exists to protect against. Evidence: Verified — tracedrestoreBackupend to end; receipt push is the last step. Fix: Persist the receipt (with snapshot) BEFORE the first mutation, markedpending, and finalize afterward; on scheduler init surface any still-pendingreceipt as "restore may be incomplete — roll back?". Cheap alt: arestoreInProgressjournal key written before mutating. Acceptance: Killing the SW mid-restore leaves a recoverable receipt; a failure-injection test asserts a receipt exists before mutations begin. Confidence: Verified Effort: M -
P2 — Custom themes and extra presets apply only to the dashboard; popup, side panel, install, and DevTools ignore them Category: visual Where:
pages/dashboard-theme-editor.jspersistssv_active_custom_theme; onlypages/dashboard.js:5667-5695reads it.pages/popup.js:709-713,pages/sidepanel.js:503-507,pages/install.js:786-791,pages/devtools-panel.js:295-302setdata-themefromsettings.layoutonly and never readsv_active_custom_themeProblem: The Theme Editor's extra presets (nord/dracula/solarized/etc.) and custom themes are stored as CSS-var overrides undersv_active_custom_theme(they leavesettings.layouta built-in). The dashboard injects those vars; every other surface renders the base built-in theme, so a user's chosen theme applies to the dashboard alone — a cross-surface inconsistency that undercuts cohesion. Evidence: Verified —grep -rln sv_active_custom_theme pages/returns onlydashboard.js+dashboard-theme-editor.js; the four secondary surfaces read onlysettings.layout. Fix: Extract a sharedapplyTheme()(e.g.pages/theme-apply.js) that setsdata-themeAND reads+sanitizessv_active_custom_theme(replicate the dashboard's/^--[\w-]+$/key regex +[{};]-in-value rejection), called from popup/sidepanel/install/devtools. Also removes the 5× duplicated auto/layout logic. Acceptance: Applying a custom/extra-preset theme then opening popup/sidepanel/install/devtools renders the custom vars; a test asserts each surface reads and sanitizessv_active_custom_theme. Confidence: Verified Effort: M -
P2 — The debugger identifies scripts by raw
script_<uuid>everywhere a name should appear Category: ux Where:pages/dashboard-debugger.js:298(console selector option text = id),:425(variables selector),:386(live-reload row label),:396(togglearia-label) Problem: All four render the internal id ("script_" + crypto.randomUUID()), so users pick betweenscript_3f6a1c2e-…entries and the debugger is effectively unusable with more than one script; screen-reader users hear a UUID. The module receives only ids and never resolves names, thoughdashboard.js:16085builds theinitoptions and holdsstate.scriptswithmetadata.name. Evidence: Verified — traced id origin (generateId) and all four render sites. Fix: Pass agetScriptName(id)callback inScriptDebugger.initoptions and use it for option text, live-reload labels, and aria-labels, falling back to the id. Acceptance: The debugger's selectors and rows show names; a test asserts names render when a resolver is provided. Confidence: Verified Effort: S -
P2 — Deleting a custom theme is a 16px hover-only control with no confirmation and no undo Category: ux Where:
pages/dashboard-theme-editor.js:924-929(deleteCustomThemedeletes+persists+toasts immediately), CSS:575-593(.sv-te-delete-custom { width:16px; height:16px; display:none }revealed on hover) Problem: A custom theme is 21+ hand-picked tokens; the delete trigger is a 16×16 CSS-px "x" appearing on hover (below the repo-enforced 24×24 WCAG 2.2 SC 2.5.8 minimum), with noshowConfirmModal(available viaScriptVaultDashboardUI.confirm, used by every other module's delete) and no undo. One mis-click destroys the theme permanently. Evidence: Verified — read the delete handler (stopPropagation →deleteCustomThemedirectly) and the hover-reveal CSS. Fix: Route throughScriptVaultDashboardUI.confirm('Delete Theme?', …, {tone:'danger'})like chains/profiles/templates, and raise the button to ≥24px. Acceptance: Deleting a custom theme requires confirmation and the control is ≥24×24; a test asserts the confirm path. Confidence: Verified Effort: S -
P2 — Both worldId fallback paths silently restore the exact Firefox failure mode af0bfb3 fixed Category: correctness Where:
src/background/registration.ts:646-697(mirrorsrc/background/core.ts:11928-11973) Problem: IfconfigureWorldthrows (:654-656) orregisterrejects with aworldIdmessage (:687-693), the script registers into the SHARED default world with no warning, no_registrationError, no error-log entry — precisely the "scripts 2..n fail silently" stateaf0bfb3fixed. The catch is deliberately broad (world-count limit, reserved-id rejection, transient failure all land here). Related:worldId = script.id, and Chrome reserves ids with leading_; imported-backup ids come from the file, so an id starting with_silently downgrades to the shared world. Evidence: Verified (code path); Likely (which engine hits the fallback determines user impact). Fix: WhenworldConfiguredis false (or the retry fires) on Firefox, record a diagnostic (_registrationWarning/ErrorLog) so a silent single-script-per-page regression is observable; sanitize/prefix the worldId so it can never start with_. Acceptance: A world-config failure is visible in the error log; an id starting with_still gets an isolated world; tests cover both. Confidence: Likely Effort: M -
P2 — The Firefox worldId feature probe tests for
configureWorld, not for worldId support Category: correctness Where:src/background/registration.ts:147-157(supportsUserScriptsWorldIdreturnstypeof chrome.userScripts.configureWorld === 'function') Problem:configureWorldshipped in Firefox before per-worldworldIdsupport (dated to 153); on 136–152 the probe is true and the code relies on the engine THROWING on the unknownworldIdproperty. If Firefox instead ignores the unknown property,worldConfiguredis true, the retry never fires, and every script silently shares one world again — the original bug, on the Firefox range most users are on, with a suite that only proves behavior on 154.0b1.tests/firefox-per-script-world.test.js:44-51mocksconfigureWorldas a no-op and asserts the probe returns true — encoding the same assumption under audit. Evidence: Verified (code + test); Likely (needs a Firefox 152 run to close). Fix: Probe the capability, not the symbol —configureWorld({worldId:'sv-probe', messaging:true})thengetWorldConfigurations()(or a one-time two-script co-execution check), cached per session; fall back to the shared world only when the probe proves worldId is absent. Acceptance: On a Firefox build without worldId support the probe returns false and the code degrades knowingly; a capability-probe test covers it. Confidence: Likely Effort: M
-
P3 — The pending-updates count badge/chip renders
#93c5fdon a near-white tint = 1.52:1 in light theme Category: a11y Where:pages/popup.html:694-701(.menu-item-badge, element#pendingUpdatesBadgeat:1472) andpages/sidepanel.html:44-51(.sp-update-chip, element#btnPendingUpdatesat:463) Problem: Both usebackground: rgb(from var(--sv-info) r g b / 0.12)(theme-aware, near-white in light) with hardcodedcolor:#93c5fd. In light theme--sv-infois#2563eb, so the background is ~rgb(229,236,253)and#93c5fdtext measures 1.52:1 — far below AA 4.5:1. These badges show the queued-updates count (un-hidden when >0). Unlike the dashboard's.script-health-badge/.script-tag(which havehtml[data-theme="light"]overrides), popup/sidepanel have none for these. Evidence: Verified — computed via the WCAG relative-luminance formula (1.52:1); no light-theme override exists for either selector (one#93c5fdper file). Fix: Replace the hardcoded#93c5fdwith a theme-aware token meeting 4.5:1 on the tint in all four themes (plainvar(--sv-info)is only 4.37:1 in light — use a darker info-on-tint token such as a new--sv-info-strong, orvar(--sv-text)). Acceptance: Both badges measure ≥4.5:1 in light theme (other themes unchanged); an assertion added totests/accessibility-surface-pass.test.js. Confidence: Verified Effort: S -
P3 — Restore writes an unvalidated
folders.json; a non-array value bricksFolderStorage(all folder ops throw) Category: correctness Where:src/modules/backup-scheduler.ts:1671-1716(writes parsed archive JSON straight tochrome.storage.local.set({ scriptFolders: folders }));src/modules/storage.ts:872-873(this.cache = (data['scriptFolders'] as Folder[]) || []) Problem: Backups can be arbitrary external files via "import backup ZIP".restoreBackupwritesfolders.jsonwith noArray.isArraycheck;FolderStorage.initassigns any truthy non-array (object/string/number) tocache, and every subsequentcache.find/push/filterthrows — folder create/update/delete all TypeError until another restore overwrites the key.verifyBackuponly checks it parses as JSON, and verify is optional. Same gap forworkspaces.json. Evidence: Verified — read the restore write (no validation) andFolderStorage.init(|| []guards only falsy). Fix: ValidateArray.isArray(folders)(and minimally each entry'sid/scriptIds) before writing; hardenFolderStorage.initto coerce non-arrays to[]. Acceptance: Restoring a backup whosefolders.jsonis an object leaves folders empty rather than broken; a test asserts a malformedfolders.jsonis rejected/coerced. Confidence: Verified Effort: S -
P3 — Imported/restored settings have no key/type allowlist beyond credential redaction, so a hostile backup can flip security gates Category: security Where:
src/background/core.ts:4512-4520,5462(prepareSettingsForPortableImport) andsrc/modules/backup-scheduler.ts:604-620,1653(_prepareSettingsForRestore) — both strip onlySETTINGS_CREDENTIAL_KEYSthen pass the object toSettingsManager.setProblem: JSON import and full restore apply the backup's settings after removing only the ~11 credential keys. Security-posture keys —allowInternalXhr,allowInternalSyncEndpoints,allowHighPrivilegeScriptApis,trustedSigningKeys,deniedHosts,blacklist,scopedHostPermissions— and arbitrary unknown keys are applied verbatim with no type validation. A shared/hostile backup can silently flip SSRF/privilege gates and seed the signing trust store; malformed types (e.g.deniedHostsas a string) flow into consumers. (Prototype pollution is not possible — spread uses CreateDataProperty; verified.) Evidence: Verified — both redaction helpers delete onlySETTINGS_CREDENTIAL_KEYS;SettingsManager.setshallow-merges any object. Fix: Validate imported settings againstsrc/config/settings-schema.json: drop unknown keys, type-check known ones, and require explicit confirmation (or always skip) for the security-relevant subset, mirroring the credentials treatment. Acceptance: Importing a backup that setsallowInternalXhr:true/trustedSigningKeysdoes not silently apply them; a test asserts unknown/security keys are dropped or gated. Confidence: Verified Effort: M -
P3 — Local-library
sha256is format-checked but never re-verified against the code it labels (false provenance on import) Category: security Where:src/background/local-libraries.ts:78-114(normalizeLocalLibrarySnapshotsaccepts any 64-hexsha256without recomputing overcode) +src/background/core.ts:5386-5388(JSON import runs settings through it) and:13012-13024(getLocalLibraryRequireScriptsembedscodewith the stored hash as a#sha256=label) Problem: The hash is decoration on every path except the originalcreateLocalLibrarySnapshotintake. An imported script can carry up to 8×512 KB of arbitrary library code wearing a "reviewed" hash of, say, real jQuery — false provenance in any UI that shows the hash as review evidence. (The script is quarantine-disabled on import, so this is a provenance-integrity gap, not direct execution.) Evidence: Verified —sha256is recomputed only increateLocalLibrarySnapshot(:62);normalizeLocalLibrarySnapshotsonly regex-checks format (:87-90). Fix: Recomputesha256at registration/wrap time and drop mismatched snapshots; or striplocalLibrariesfrom imported settings and require re-review. Acceptance: An imported snapshot whose bytes don't match itssha256is rejected; a test asserts recomputation. Confidence: Verified Effort: S -
P3 —
@requirefallback CDNs silently substitute a different library (jQuery core for a plugin) and cache it for 7 days Category: correctness Where:src/background/core.ts:12041-12071(getFallbackUrls, substring matchlowerUrl.includes('jquery')/'gm_config'/'mutation-summary') and:12321+ the cache-store block Problem: A require ofhttps://mysite.com/jquery-plugin-custom.jsthat 5xx's falls through tohttps://code.jquery.com/jquery-3.7.1.min.js, and the bytes are cached under the ORIGINAL URL key (memory +chrome.storage.local, 7 days). The script runs jQuery core in place of its plugin with no error. Version sniffing is also wrong (includes('2.')routesjquery-1.12.4.min.jsto the jQuery-2 list). Evidence: Verified —getFallbackUrlssubstring branches and the jQuery CDN fallback arrays read in shippedcore.ts. Fix: Apply a fallback only when the requested URL host is the known CDN being replaced (not any URL containing the substring), or drop generic-name fallbacks; fix version sniffing to parse the version token. Acceptance: A custom-named script whose URL merely contains "jquery" is not substituted; a test covers a plugin URL that 5xx's. Confidence: Verified Effort: S -
P3 —
@require/GM_loadScriptexecute code fetched over plaintext HTTP with optional SRI Category: security Where:src/background/core.tsrequire/loadScript fetch paths pass['http:', 'https:']toInternalHostGuard(e.g.:10139);verifySRIreturns true for a missing/md5-hash Problem: Because the fetch happens in the service worker, no mixed-content blocking applies and SRI is opt-in. Any on-path attacker can replace an@require http://…body or aGM_loadScript('http://…')response with arbitrary JS, evaluated in the script's world. Evidence: Verified — require/loadScript/usercss fetch guards pass['http:', 'https:']; SRI enforcement is opt-in (sri === 'require'). Fix: Forhttp:requires/loadScript, require a verifiable SRI hash (reject un-pinnedhttp:) or surface an install/update warning; prefer upgrading known https mirrors. Acceptance: An un-pinned@require http://…is refused (or warned) at install; a test covers the http-without-SRI case. Confidence: Likely Effort: S -
P3 — The network XHR proxy drops
XMLHttpRequest's static constants (DONE/OPENED/…) Category: correctness Where: shippedbackground.core.js:15440(_WrappedXHR.prototype = _OrigXHR.prototypeonly) vs:15463(the WebSocket wrapper doesObject.assign(_WrappedWS, {CONNECTING…CLOSED})) Problem: After the proxy installs,XMLHttpRequest.DONE/.OPENED/.UNSENT(static) areundefinedfor every userscript, so the very commonxhr.readyState === XMLHttpRequest.DONEcomparison silently never matches. The sibling WebSocket wrapper copies its constants, proving the asymmetry. Evidence: Verified — read both wrapper installs in shippedbackground.core.js. Fix:Object.assign(_WrappedXHR, { UNSENT:0, OPENED:1, HEADERS_RECEIVED:2, LOADING:3, DONE:4 })after assigning the prototype. Acceptance:XMLHttpRequest.DONE === 4inside a wrapped script; a wrapper test asserts the constants. Confidence: Verified Effort: S -
P3 — Storage-cache refresh clobbers
GM_setValue/GM_deleteValuewrites made during the startup window Category: correctness Where: shippedbackground.core.js:13593(_cache = { ..._cache, ...freshValues }) Problem: The comment says "merge fresh values with local changes made before refresh completed", but the spread order lets the (possibly pre-write) background snapshot overwrite them. AGM_setValueduring the startup window is persisted but the local cache reverts to the stale value, so subsequent synchronousGM_getValuereturns the old value for the page's life; aGM_deleteValueis likewise resurrected in the cache. Evidence: Verified — read the merge line in shippedbackground.core.js. Fix: Track keys mutated locally before the refresh lands and exclude them from thefreshValuesoverwrite (or spreadfreshValuesfirst then re-apply the local mutation log). Acceptance: AGM_setValueduring startup is readable synchronously afterward; a test drives a write racing the initial fill. Confidence: Verified Effort: S -
P3 — Notification ownership check fails open and records an unauthenticated owner Category: security Where:
src/background/gm-notification-handler.ts:138-146(callerOwnsNotificationreturns true when no_notifCallbacksentry exists) and:176,187-193(id = caller-chosentag, owner =data.scriptId) Problem:callerOwnsNotificationreturns true whenever there is no callback record — true for every fire-and-forget script notification AND ScriptVault's own internal notifications (update-available, error digests; ids likeupdate-batch-${Date.now()}/error-${scriptId}-${Date.now()}, brute-forceable within a second). Any script canGM_closeNotification/GM_updateNotificationa guessed id to suppress or fully rewrite (arbitrary title/text/image) the extension's own notifications under its identity. The owner is stored as the unauthenticateddata.scriptId, and a caller-chosentagbecomes the Chrome id, so two scripts sharing a tag overwrite each other. Evidence: Verified — the fail-open branch is documented in a comment; owner stored asdata.scriptId. Fix: Always record an owner (sender.userScriptId || data.scriptId) at create time, namespace caller tags per script (${scriptId}:${tag}), and fail closed when no owner record exists for a caller-supplied id. Acceptance: A script cannot update/close another script's or the extension's notification; a test covers a forged-id update attempt. Confidence: Verified Effort: S -
P3 — Cookie-routing DNR session rule is not scoped to the extension's own request Category: security Where:
src/background/core.ts:4842-4878(session rule condition{ regexFilter: <exact url>, resourceTypes: ['xmlhttprequest'] }— notabIds/initiatorDomains) Problem: For the lifetime of a cookie-routedGM_xmlhttpRequest, any XHR from any tab to that exact URL gets itsCookieheader replaced with the extension-computed value (possibly from a partitioned jar the page cannot otherwise reach).regexFilteris case-insensitive by default, widening the match. A page racing a fetch to the same URL can harvest the injected header server-side or strip its own session cookie. Evidence: Verified — rule construction read; the URL lock serializes ids but not applicability. Fix: Addcondition.tabIds: [chrome.tabs.TAB_ID_NONE](extension-initiated only) or an extension-scopedinitiatorDomains, and setisUrlFilterCaseSensitive: true. Acceptance: The cookie-routing rule applies only to the extension's own request; a test asserts the condition scoping. Confidence: Likely Effort: S -
P3 —
.user.js/.user.cssURLs with a fragment bypass install interception Category: correctness Where:src/background/core.ts:10176,10217(JS regex/\.user\.js(\?.*)?$/i+urlMatchesfilter) and:10253,10272(.user.css) Problem:webNavigationdetails.urlincludes fragments, sohttps://example.com/x.user.js#anythingnever matches (theurlMatchesfilter means the listener isn't even invoked) and the raw script renders in the tab instead of the install review. Same for.user.css. Evidence: Verified — regex evaluated; the RE2 filter gates listener invocation. Fix: Extend both patterns to(\?[^#]*)?(#.*)?$(JS check and the RE2urlMatchesfilter). Acceptance: A.user.js#fragURL is intercepted into the review page; a test covers the fragment case. Confidence: Verified Effort: S -
P3 — Install trust/provenance records the pre-redirect URL; the final
response.urlis discarded (source-badge laundering) Category: security Where:src/background/core.ts:10141-10157(_fetchPendingUserscriptstores the navigation URL),:10428-10451(installFromUrl) Problem:classifyResponseUrlrejects only internal-host redirects; the Source & Trust card,classifyInstallSourcebadge, and the persisted receiptinstallUrlall use the PRE-redirect URL. An open redirect on a "good"-tone registry host would display a trusted source for bytes served by an arbitrary external host.fetchScriptPreviewalready returnsfinalUrl, so the asymmetry is known. Evidence: Verified — tracedpendingInstall.url→installSourceUrl→ receiptsource.installUrl;response.urlis never captured on this path. Fix: Recordresponse.urlalongside the requested URL inpendingInstall; badge/classify from the final URL and flag a cross-host redirect as a review reason. Acceptance: A cross-host redirect shows the final host (and a warning); a test asserts the receipt records the resolved URL. Confidence: Likely (mechanism verified; end-to-end needs an open redirect on a trusted host) Effort: S -
P3 — Auto-update re-notifies every cycle for update URLs whose servers send no validators Category: ux Where:
src/background/core.ts:2506-2541(autoUpdatenotification gate),:1899(validators stored only if present),queueUpdatesrebuild Problem: When the update endpoint returns no ETag/Last-Modified, every cycle re-fetches, re-detects the same newer version, re-queues it (resettingqueuedAt), and — for review-required/non-apply-safe mode — fires "N updates ready" again. The user is re-notified every cycle until they act, and a dismissed pending update from such a server reappears next cycle. Evidence: Verified — traced the no-validator path throughqueueUpdatesto the notification gatequeued > 0. Fix: Suppress the notification when the queued set is id+version-identical to what was already pending; preserve originalqueuedAtfor unchanged entries. Acceptance: A validator-less server does not re-notify on an unchanged pending update; a test covers repeated cycles. Confidence: Verified (logic); real-world cadence Needs-repro Effort: S -
P3 —
checkForUpdatesdoesn't skip user-modified scripts, so they are fetched/receipt-built/queued/notified but can never apply and never clear Category: correctness Where: shippedsrc/background/core.ts:1859-1866(skips@nodownloadbut notsettings.userModified) vs:2001(applyUpdaterefuses non-force applies of user-modified);applyPendingUpdateclears the queue entry only on success Problem: A user-modified script with an update URL is fetched every cycle,_buildPendingUpdateruns full trust-receipt construction (network-fetching every@requirebody), the item is queued (possiblysafeToApply), the user is notified, and Apply returns{skipped:'user-modified'}without clearing the queue — so it sits there indefinitely. The tested mirrorupdate-checker.ts:256DOES have the skip, so the suite pins behavior the runtime lacks. Evidence: Verified —grep userModifiedin shippedcheckForUpdatesreturns nothing; mirror has the skip. Fix: Add theuserModifiedskip to the livecheckForUpdatesauto path (keep manual single-script checks reporting it), or mark such items un-appliable and exclude them from the "ready" count. Acceptance: A user-modified script is not re-queued/re-notified on auto-update; a test covers the auto path. Confidence: Verified Effort: S -
P3 — The primary install path (
saveScript) skips ESM bundling, so ESM scripts install unbundled and bypass the experimental-flag refusal Category: correctness Where:src/background/core.ts:6754-6916(saveScript— noESMUserscriptBundler.bundleIfNeeded) vsinstallFromCode(:10298) andapplyUpdate(:2001) which call it Problem:saveScriptis the path used by the install-review page and the dashboard editor. An@module 1/@inject-into modulescript is accepted raw: withexperimentalESMUserscriptsoff it installs where the other paths would refuse it, and either way the rawimportstatements land in the GM wrapper IIFE and fail at injection with no surfaced error. Evidence: Verified — read the fullsaveScriptbody; no bundler reference. Fix: RunbundleIfNeededinsaveScriptafter parse (same pattern asinstallFromCode). Acceptance: An ESM script installed via the review page is bundled or refused per the flag; a test covers both flag states. Confidence: Verified Effort: S -
P3 —
applyUpdateregisters new code before persisting; a persist failure leaves running and stored code divergent Category: correctness Where:src/background/core.ts:2091-2108(re-register, thenensurePersistentStorageForScriptWrite/ScriptStorage.set) Problem: Register-first is deliberate and registration failure is handled, but the reverse is not: if the persist throws after successful re-registration (e.g. quota), the browser runs the new code while storage holds the old version — dashboard, receipts, and version history disagree with what executes until the next SW restart re-registers the old code (a silent downgrade of running code). Evidence: Verified — read the ordering; no compensating re-register on persist failure. Fix: On persist failure, best-effort re-register the previous script (or unregister) before propagating the error. Acceptance: A persist failure after re-register restores the previously-running code; a test injects a rejectingScriptStorage.set. Confidence: Verified Effort: S -
P3 —
.user.csspending payloads have no TTL, cap, or dedup — unlike the.user.jspath — and leak when the tab never loads the dashboard Category: reliability Where:src/background/core.ts:10249-10262(writespendingUserStyle_<tabId>, up toMAX_SCRIPT_SIZE= 5 MB), consumed atpages/dashboard.js:13547-13554; the_storePendingInstallTTL/cap sweep (:10104-10131) covers onlypendingInstall*Problem: The.user.cssinterceptor writes the full untrusted CSS underpendingUserStyle_<tabId>with no TTL, no cap, and no in-flight dedup. The sibling.user.jspath has a 5-minute TTL, a 32-entry cap, and a_fetchPendingUserscriptin-flight map. Close the redirected tab before the dashboard loads and the entry is permanent; two tabs opening the same.user.cssfetch it twice. (Corroborated by two independent passes.) Evidence: Verified — the pending-install sweep prefix does not includependingUserStyle_; no dedup map on the UserCSS fetch. Fix: Reuse the_storePendingInstallTTL/cap eviction for thependingUserStyle_prefix and add the same in-flight dedup as_fetchPendingUserscript. Acceptance: An unconsumedpendingUserStyle_entry is swept after the TTL and concurrent same-URL opens fetch once; tests cover both. Confidence: Verified Effort: S -
P3 — EasyCloud debounce/periodic sync paths bypass the connected gate (status churn; possible post-disconnect resurrection) Category: correctness Where:
src/modules/sync-easycloud.ts:1185-1192(notifyScriptSaved/notifyScriptDeleted→_debouncedSync),_handleAlarm→_performSync(:980-1005) never checksKEYS.CONNECTED; contrast the gated storage listener:1220-1223;disconnectrevoke is fire-and-forget.catch(()=>{})(:1378-1382) Problem: For never-connected users, every script save schedules a doomed sync 5 s later that fails token acquisition and persistseasycloud_status='error'(churn). Worse edge: afterdisconnect(), if the revoke fetch fails, the Google grant survives,getAuthToken(false)still mints tokens, and the next save silently resumes uploading to Drive after the user disconnected. Evidence: Verified — tracednotifyScriptSaved→_debouncedSync→_handleAlarm→_performSync; noKEYS.CONNECTEDread on that path. Fix: ReadKEYS.CONNECTEDat the top of_performSync(or_handleAlarm) and return early when false. Acceptance: Saves while disconnected schedule no sync; a test asserts_performSyncearly-returns when disconnected. Confidence: Verified (churn); Likely (resurrection) Effort: S -
P3 — Refresh-token failure no longer clears stale tokens (regression vs the documented v2.0.2 fix) Category: reliability Where:
src/modules/sync-providers.ts:975-978,1413-1416,1764(Google/Dropbox/OneDrive refresh only log + return null; connected flags stay true) Problem: CLAUDE.md records a v2.0.2 fix "clear stale tokens on 400/401 refresh failure". The current TS source and generatedmodules/sync-providers.jsonly log and return null; with a revoked refresh token every cycle burns a probe+refresh and fails generically,googleDriveConnectedetc. stay true, and nothing tells the user to reconnect. With the autoSync-persistence gap above, the failure is fully silent. Evidence: Verified — behavior read in TS + generated runtime; intent regression inferred from the changelog note. Fix: On a definitiveinvalid_grant/400/401 refresh, clear the dead tokens and flip the provider's connected flag to "reconnect required"; keep network-level nulls non-destructive. Acceptance: A revoked refresh token flips the provider to disconnected and surfaces "reconnect"; a test coversinvalid_grant. Confidence: Verified Effort: S -
P3 — No 429/Retry-After handling; Drive rate-limit 403 is misread as an auth failure Category: reliability Where:
src/modules/sync-providers.ts:1009-1011and all providers Problem: No provider inspects 429/Retry-After; a rate-limited upload throws generically and the fixed-periodautoSyncalarm retries at the same cadence (no backoff). Google Drive signals rate limiting with HTTP 403 (userRateLimitExceeded), andgoogledrive.getValidTokentreats 403 as expired-token → a pointless refresh per cycle while rate-limited. Evidence: Verified — code paths read; Drive's 403-for-quota is documented API behavior. Fix: Distinguish 403 rate-limit bodies from auth 403s before refreshing; honorRetry-Afterand back off the autoSync alarm after consecutive failures. Acceptance: A 429/403-quota response backs off rather than refreshing every cycle; a test covers a rate-limit body. Confidence: Verified Effort: M -
P3 — Encrypt-side sync KDF iterations are uncapped while decrypt caps at 10M, so a large configured value produces undecryptable envelopes Category: correctness Where:
src/modules/sync-crypto.ts:97-102(resolveIterations, no upper bound) vs:221-225(decrypt rejects> MAX_KDF_ITERATIONS= 10M); setting atsettings-defaults.json:34+ theSettingstype (reachable via settings import) Problem: A settings import settingsyncEncryptionKdfIterationsto e.g. 20M produces uploads every device — including the author — refuses with "out-of-range KDF iteration count", bricking sync until the remote blob is manually deleted. Evidence: Verified — both bounds read; the setting is reachable via import. Fix: ClampresolveIterationsto[floor, MAX_KDF_ITERATIONS]. Acceptance: A configured value above the cap is clamped and the envelope decrypts; a test covers an over-cap value. Confidence: Verified Effort: S -
P3 — 30-day tombstone prune is defeated by the remote round-trip; the uploaded tombstone map grows unboundedly Category: maintainability Where:
src/background/cloud-sync.ts:1665andsrc/modules/sync-easycloud.ts:869(union remote tombstones unconditionally), upload includes the full merged map (:1872); local prunecore.ts:10975-10986Problem: The maintenance prune removes local tombstones >30 days, but the next sync unions the remote envelope's tombstones back in, detects a change, re-persists the pruned entries, and re-uploads them. Nothing prunes tombstones from the uploaded envelope, so the local prune is a no-op and the map grows with every deletion for the account's life (inflating every future encrypted payload). Evidence: Verified — both engines union remote tombstones; upload includes the full map. Fix: Apply the same 30-day age filter to the merged tombstones before persisting/uploading (safe: the resurrection guard is timestamp-based). Acceptance: Tombstones older than 30 days stop reappearing after a sync; a test covers prune survival across a round-trip. Confidence: Verified Effort: S -
P3 —
matchPatterndiverges from Chrome on scheme wildcard and host case (over-counts, needless reloads, missed matches) Category: correctness Where:src/background/url-matcher.ts:191(scheme === '*') and:203-212(case-sensitive host) Problem: Used for badge counts, popup listing,autoReloadMatchingTabs, andMatchSet.getMatching. (a)scheme === '*'accepts ANY protocol, so@match *://*/*reports a match onfile:///ftp:///chrome-extension://; Chrome's*means http/https only → over-counted badges and needlesschrome.tabs.reloadoffile://tabs on save (tab-reload.ts:41-45). (b) Host comparison is case-sensitive against the already-lowercasedurlObj.hostnamewhileisValidMatchPatternaccepts uppercase, so@match *://GitHub.com/*runs in Chrome (which canonicalizes) but ScriptVault's matcher says "no match" → the popup shows no scripts and auto-reload skips the page. Evidence: Verified — both divergences read in the shipped matcher. Fix: Restrict*to http/https (keep@include's broader semantics inmatchIncludePattern), and lowercase the pattern host before comparison. Acceptance:@match *://*/*does not matchfile://;@match *://GitHub.com/*matchesgithub.com; tests cover both. Confidence: Verified Effort: S -
P3 — The badge counts scripts that can never run on the page Category: correctness Where:
src/background/badge.ts:88-98(matchingScripts=script.enabled && doesScriptMatchUrl(...)) Problem: The count does not exclude the classesregisterScriptrefuses:meta.backgroundscripts (registration.ts:331-337),@run-at context-menu(:543-548),settings._importQuarantine(:192-194), or scripts left with_registrationError. A user with a couple of background/context-menu scripts sees a permanently inflated "running here" count. Evidence: Verified — read the counting predicate vs the registration exclusions. Fix: Filter throughisScriptEligibleForRegistrationplus the background/context-menu exclusions before counting. Acceptance: Background/context-menu/quarantined scripts are not counted as running on a page; a test covers the exclusions. Confidence: Verified Effort: S -
P3 —
key in metawalks the prototype chain in the UserCSS metadata parser Category: correctness Where:src/modules/userstyles.ts:545-547(else if (key in meta) { meta[key] = value; }) Problem:key in metais true for everyObject.prototypemember, so@toString evilin a==UserStyle==block stores an owntoStringstring on the persisted meta; any later string coercion throws. Impact is small (nothing currently coerces it) but the guard is not doing what it reads as. (Same class as theGM_getValueproto item.) Evidence: Verified — read the parse branch. Fix:Object.prototype.hasOwnProperty.call(meta, key). Acceptance:@toString/@constructordirectives are ignored; a parser test covers them. Confidence: Verified Effort: S -
P3 —
executeMenuCommandforwards a caller-suppliedscriptId, letting one script trigger another's menu-command callback Category: security Where:src/background/gm-menu-handler.ts:146-153(passesdata.scriptIdtochrome.tabs.sendMessage; every other branch usesownedScriptId) Problem: Unlike the sibling branches, this trustsdata.scriptId, so a script can fire another script's registered menu-command callback in the shared tab. (Also:menuCommandsread-modify-write viachrome.storage.sessionat:84-119is unserialized and keys under the literal"undefined"whenownedScriptIdis absent.) Evidence: Verified — read the handler; the asymmetry withownedScriptIdis clear. Fix: UseownedScriptId/sender.userScriptIdfor the target; serialize themenuCommandsread-modify-write and skip when the owner id is undefined. Acceptance: A script cannot trigger another script's menu command; a test asserts owner scoping. Confidence: Verified Effort: S -
P3 —
GM_audiowatch state is per-tab, not per-script — one script's unwatch stops events for all Category: correctness Where:src/background/gm-audio-handler.ts:86-101(_audioWatchedTabsis aSet<number>) Problem: The first script to callGM_audio_unwatchStatestops state events for every other script watching that tab. Evidence: Verified — read the handler; watch state keyed by tab only. Fix: Key watch state by${scriptId}:${tabId}and emit per-script. Acceptance: Two scripts watching one tab's audio are independent; a test covers isolation. Confidence: Verified Effort: S -
P3 —
__svEventHandleraccepts cross-framepostMessage(missing theevent.source !== windowguard its three siblings have) Category: security Where: shippedbackground.core.js:14910(window.addEventListener('message', function __svEventHandler(event) {…}with no source guard) vs the guarded handlers at:13347,13483,13556Problem: Any window holding a handle (opener, embedding frame) can injectnotificationEvent/downloadEvent/openedTabClosedand fire the script'sonclick/onload/onclosecallbacks.CHANNEL_IDderives from the public extension id; the required per-installscriptIdis leaked into the page DOM byGM_addStyle(style.setAttribute('data-scriptvault', scriptId)), so a same-window page script can read it and forge messages. Evidence: Verified — the three siblings guardevent.source !== window;__svEventHandlerat :14910 does not. Fix: Addif (event.source !== window) return;to__svEventHandler; stop stamping the rawscriptIdinto the page DOM (use a non-reversible marker). Acceptance: A cross-frame forged event does not fire the script's callbacks; a test covers a foreignevent.source. Confidence: Verified Effort: S -
P3 — Offscreen document accepts analysis/merge work from any extension context (content scripts, userscripts) with no input size bounds Category: security Where:
offscreen.js:10-33(gate is only_sender.id !== chrome.runtime.id);handleAnalyze/handleMerge/handleDiff/handleESMImportshave no length caps Problem:chrome.runtime.sendMessagereaches all extension contexts, so{type:'offscreen_analyze', code:<pathological>}from a content-script/userScript-world sender reacheshandleAnalyzeeven though the SW's action gate would reject the same message. A full Acorn parse +walkAST(orDiff.structuredPatch) on multi-MB inputs is CPU/memory abusable. Evidence: Verified — the only gate is the id check; no size caps in the handlers. Fix: Require a nonce or verify_sender.urlis the SW/extension page (content-script senders carry an http(s)sender.urland asender.tab), and cap input lengths before parsing. Acceptance: An oversized/foreign offscreen request is rejected; a test covers a content-script-shaped sender and an oversized input. Confidence: Verified (reachability); Needs-repro (practical impact) Effort: S -
P3 — Messaging-failure catch branches discard
e.message, leaving no actionable detail on SW-asleep/port-closed failures Category: maintainability Where:pages/dashboard.js:12587,13015,13121,13198,15338,16406(catch (e)toasts a bare fallback like "Rollback failed"/"Update failed") Problem: The siblingres?.error || 'X failed'path surfaces background-reported errors, but thecatch (e)branches drope.message; on a rejection the user gets no detail.btnCreateBackup(:17468) and the save path already preserve detail. Evidence: Verified — read all six sites. Fix:showToast(e?.message || 'X failed', 'error')in the catch branches. Acceptance: A rejected background call shows its message; consistent with the save/backup handlers. Confidence: Verified Effort: S -
P3 — CSP reporter's "workarounds" recommend approaches that cannot work in this product Category: ux Where:
pages/dashboard-csp.js:30-52Problem: The connect-src suggestion shipschrome.runtime.sendMessage({ action: 'fetch', url })— nofetchaction exists in the router and the userscript allowlist would reject it — and the script-src suggestions (chrome.scripting.executeScript, content-scriptnew Function) are extension-developer techniques unavailable to userscript authors. Users following the product's own advice hit dead ends. Evidence: Verified —action:'fetch'is the only sent action string with no router entry. Fix: Replace with ScriptVault-actionable guidance (GM_xmlhttpRequest+@grant/@connect,GM_addElement,@inject-intonotes) and drop the extension-dev samples. Acceptance: The CSP workaround cards reference only actions/APIs available to userscripts. Confidence: Verified Effort: S -
P3 — Live Reload empty state tells end users to call an internal JS function Category: docs Where:
pages/dashboard-debugger.js:380('No scripts registered. Call enableLiveReload(scriptId) to add one.') Problem:enableLiveReloadis a module API, not a UI action; developer copy leaked into a user-facing empty state, and the real path (open the debugger from a script's editor) is never mentioned. Evidence: Verified — readrenderLiveReloadPanel; population is via the editor debug button / console ingestion. Fix: Replace with e.g. "No scripts captured yet. Open the debugger from a script's editor to add it here." Acceptance: The empty state describes an in-UI action. Confidence: Verified Effort: S -
P3 — Dependency graph has no zero-scripts empty state (blank canvas) Category: ux Where:
pages/dashboard-depgraph.js:576-620(render()has nonodes.length === 0path); the only empty copy is the sidebar's "Select a node…" Problem: A user with no scripts (or no relationships) opening Utilities → Dependency Graph sees an unexplained empty canvas. Evidence: Verified — readrender();dg-emptyis sidebar-only. Fix: Whennodes.length === 0, overlay "No scripts to graph yet — install a script to see its dependencies and match overlaps." Acceptance: The empty graph shows an explanatory message. Confidence: Verified Effort: S -
P3 — Drifted UNSHIPPED mirror modules are pinned by the test suite as if they were the product Category: maintainability Where:
src/background/wrapper-builder.ts,import-export.ts,update-checker.ts,install-handler.ts,trust-receipt.ts,parser.ts(none ints-source-promotion.json; live copies are inline incore.ts); teststests/gm-websocket.test.js,wrapper-gm-tabs-39-13.test.js,wrapper-dom-security.test.js,pending-update-queue.test.js,pending-install-isolation.test.js,trust-receipt*.test.js,versions.test.jsand others import the mirrors Problem: These files are re-exported only by the unusedsrc/background/index.tsbarrel and are not built. They have measurably drifted from the livecore.ts: mirrorwrapper-builder.tslacksGM_getTab/saveTab/getTabsgrant checks (live core.ts has them), defaultsGM_getResourceURLto a never-revoked blob URL (live uses the data-URI leak fix), is missing standaloneGM_updateNotification/GM_closeNotification+buttons[]/onbuttonclick+ the highlight-path_notifCallbackscleanup, andmeta.grant.lengththrows where core usesmeta.grant || ['none']; mirrorimport-export.tslacks versionHistory snapshot, import receipts, per-script settings preservation, and resetscreatedAton overwrite; mirrorupdate-checker.tslacks the backoff engine, the pending-updates byte cap, and the provenance-failure gate (but has theuserModifiedskip the runtime lost). Tests importing these prove nothing about shipped behavior (the "check wired to the wrong data source" failure mode) and mislead anyone who "fixes" a bug in the mirror.GM_webSocketis concrete: implemented in the mirror wrapper and covered bytests/gm-websocket.test.js, but the shipped injected wrapper exposes nowindow.GM_webSocket/GM.webSocket(grep "window.GM_webSocket" background.js→ 0), so a@grant GM_webSocketscript gets aReferenceError. Evidence: Verified —ts-source-promotion.jsonhas no entries for these files; live-vs-mirror drift confirmed feature-by-feature (consolidates three passes' observations);grep "window.GM_webSocket" background.js→ 0. Fix: For each mirror either (a) promote it — extract the live logic fromcore.tsinto the module, add it tots-source-promotion.json+ the drift gate — or (b) delete it and repoint its tests at the generatedbackground.core.jsextraction. At minimum add a drift assertion between each mirror and its inlinecore.tscopy so divergence fails CI. Separately decide whetherGM_webSocketis a shipped feature; if so expose the client in the live wrapper, else drop the mirror + test. Acceptance: No test imports a non-promotedsrc/background/*.tsmirror without a drift assertion against the shipped copy;GM_webSocketeither works in a live browser or is removed from docs/tests. Confidence: Verified Effort: L -
P3 — Doc rot: CLAUDE.md lists dashboard modules that do not exist Category: docs Where: repo
CLAUDE.md"Dashboard Modules (26 files)" listsdashboard-i18n-v2.jsanddashboard-recommendations.jsProblem: Neither file exists inpages/and nothing referencesI18nV2; future maintainers/agents hunt for phantom files. Evidence: Verified —lsfails for both; grep for references returns nothing. Fix: Remove the two entries and re-count the module list. (CLAUDE.md is AI working notes, not user-facing; safe to edit.) Acceptance: The module inventory lists only files that exist. Confidence: Verified Effort: S 2026-08-06 research pass — same item, wider scope than first recorded. The same file also states version v3.19.2 (actual 3.24.0), Monaco 0.55.1 (actual 0.56.0), vendoredacorn 8.14.1anddiff 7.0.0— both wrong:lib/acorn.min.jsis 8.17.0 andlib/diff.min.jsis 9.0.0, correctly recorded indocs/amo-vendored-libraries.md. Fold these into the same sweep; the vendored ones matter most because a stale note invites a needless "upgrade the vendored library" task that is already done.
-
P3 — Unaudited: live-browser drive of the populated install review, editor interactions, and DevTools panel in a real host Category: testing Where:
pages/install.js(populated states),pages/monaco-adapter.js/pages/editor-sandbox.html,pages/devtools-panel.jsProblem: This pass was static/trace + Node-timing based; the populated install review (valid/malformed/oversized/downgrade), editor find/replace/undo-across-tabs/large-file behavior, and the DevTools panel inside an attached DevTools session were not driven live. (Overlaps the prior 2026-08-02 "Unaudited" items; still open.) Evidence: No live-drive harness was run in this pass. Fix: Drive a real.user.jsnavigation into the review UI and load the DevTools panel through an attached session; drive the editor's documented interactions. Acceptance: Observed coverage of the populated install/editor/devtools interactions. Confidence: Needs-repro Effort: M -
P3 — Unaudited: cloud providers against live endpoints, and Stylus import Category: testing Where:
src/modules/sync-providers.ts(Google Drive/Dropbox/OneDrive/S3 live OAuth+quota+429 bodies),src/modules/sync-easycloud.ts,_convertStylusStyle/importStylusBackupinsrc/modules/userstyles.tsProblem: Provider findings above were traced statically; real OAuth consent, token refresh, quota, and 429/403 bodies were not exercised against live services. Stylus import conversion was not audited. Evidence: No live endpoint or Stylus-import fixture was run in this pass. Fix: Audit each provider against a real/high-fidelity fake endpoint; add Stylus-import conversion fixtures. Acceptance: Each provider and the Stylus import path has an observed pass/fail record. Confidence: Needs-repro Effort: L
Net-new from the 2026-08-06 external research pass (baseline v3.24.0, commit
13e63d6). Deduped against every existing section of this file and against
Roadmap_Blocked.md. The prior pass's P0 (blocking high-severity dependency
audit) shipped in 68d2001 and npm audit --omit=optional --audit-level=high
is now clean — not re-added. The prior pass's freshness, watcher-fallback,
RTL-ratchet and version-derived-reference items remain open above and are not
duplicated. Conclusions and sources: RESEARCH.md (2026-08-06).
-
P2 —
release:check:publiccannot pass, because the project never signs tags Why: The public release gate rejects any unsigned tag outright — the accepted-unsigned allowlist is deliberately honoured only whencheckPublicis false — while ScriptVault's stated policy is to ship unsigned. The gate is therefore unpassable by construction, which makes it as uninformative as one that always passes, and it silently failed for v3.21.0, v3.22.0 and now v3.25.0. Evidence:scripts/check-release-artifacts.mjsverifyReleaseTag()only consultslegacyUnsignedTagsunder!checkPublic;tests/release-supply-chain.test.js:177pins that behaviour as intentional ("allows the existing legacy unsigned tag only outside the public release gate");npm run release:check:publicfails with "git tag v3.25.0 is unsigned: error: no signature found" while every other release gate passes andverify_release_notes.py --check-published v3.25.0succeeds. Touches: scripts/check-release-artifacts.mjs, tests/release-supply-chain.test.js, docs/release-runbook.md, Roadmap_Blocked.md (the Sigstore entry). Acceptance: a decision is recorded and enforced — either the allowlist is honoured in the public gate so an unsigned-by-policy release can pass with a visible warning, or the public gate is explicitly scoped to the checks that do not require signing and the signature assertion is removed from it. Either waynpm run release:check:publicreturns a meaningful pass/fail for a release built under the current no-signing policy, and the test names the chosen policy rather than the current accident. Complexity: S -
P2 — Tree-shake Monaco: the editor ships 79 language tokenizers and 5 workers to open one language Why: The bundle is imported as a full barrel, so 4.24 MB of unreachable workers and ~79 unused language tokenizers ship to every Chrome user — and this exact weight is the recorded reason Monaco is omitted from the Firefox package entirely, so pruning it is the direct unblock for Firefox editor parity rather than separate work. Evidence: src/editor/monaco-esm-entry.ts:1
import * as monaco from 'monaco-editor'; pages/editor-sandbox.html:255 and :681 are the only model-language call sites and both pass 'javascript'; built sizes lib/monaco-esm/editor.js 9.78 MB, workers/ts.worker.js 13.36 MB, css.worker.js 1.96 MB, html.worker.js 1.34 MB, json.worker.js 0.94 MB; a scan of editor.js finds 79 distinct basic-language ids; FIREFOX-PORT.md:45 "build-firefox.sh intentionally omits lib/monaco/ because AMO's linter rejects the bundled TypeScript worker as too large to parse"; monaco-editor 0.56.0 added tree-shakeable ESM entry points (https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md). Touches: src/editor/monaco-esm-entry.ts, esbuild.config.mjs worker entry points, pages/editor-sandbox.html worker label map, scripts/check-monaco-package-contract.mjs, scripts/check-monaco-esm-prototype.mjs, docs/monaco-esm-migration-plan.md, docs/audit/monaco-esm-prototype-*.json, build-firefox.sh. Acceptance: the entry imports monaco-editor/esm/vs/editor/editor.api plus only the language contributions the product opens (javascript/typescript, and css once the UserCSS item lands); html and json workers are no longer built or mapped; the package contract budgets are lowered to the new sizes so a regression fails;npm run smoke:editorand the Monaco E2E specs still pass; the resulting bundle size is recorded against the AMO lint ceiling so FIREFOX-PORT.md can state whether Monaco now fits. Complexity: L -
P2 — Edit UserCSS in a CSS model, not a JavaScript one Why: Persistent UserCSS was the v3.22.0 headline feature, but the editor never creates a css model, so userstyles are authored under JavaScript tokenization and receive JavaScript diagnostics — while css.worker.js is bundled and unreachable. Evidence: pages/editor-sandbox.html:255,681 both hardcode 'javascript'; no 'css' or usercss string appears in pages/editor-sandbox.html or pages/monaco-adapter.js; src/editor/monaco-esm-entry.ts:9-11 maps css/scss/less labels to a worker nothing requests; src/modules/userstyles.ts owns the UserCSS parse and tests/userstyle-compat-fixtures.test.js already exercises parseUserCSS. Touches: pages/editor-sandbox.html, pages/monaco-adapter.js, pages/dashboard.js editor-open path, src/editor/userscript-language-service.ts, tests/ editor and userstyle coverage, tests/e2e/usercss-advanced.spec.js. Acceptance: opening a userstyle selects the css language model and switching between a script and a style tab switches the model language; CSS diagnostics appear for a malformed userstyle and JavaScript diagnostics do not; @preprocessor / @-moz-document blocks do not produce spurious errors; the E2E UserCSS spec asserts the active model language. Complexity: M
-
P2 — Measure service-worker cold start and gate it Why: Nothing in the repo measures the cost users actually pay on every service-worker wake — parsing and executing a 1.81 MB script — and MV3 boot latency is the most-reported performance symptom across the whole ecosystem, so a regression here is currently invisible. Evidence: scripts/smoke-large-library.mjs and tests/large-library-perf.test.js gate MatchSet build/lookup and dashboard virtual-row render only; a grep for cold-start/boot timing across scripts/.mjs and tests/.test.js returns nothing; background.js is 44,365 lines / 1.81 MB; violentmonkey/violentmonkey#2608 ("Microfreezes in MV3 version"), Tampermonkey/tampermonkey#2347 (50-500 ms per navigation, hundreds of restarts per day), Tampermonkey/tampermonkey#2456. Touches: new scripts/smoke-service-worker-boot.mjs (mirroring smoke-large-library.mjs's --check/--json shape), package.json scripts, scripts/release-preflight.mjs, docs/large-library-perf.md, tests/ threshold mirror. Acceptance: a headless-Chromium harness forces the service worker to terminate and wake, measures boot-to-first-message-response p50/p99 over a repeated basket, writes a JSON report, and exits non-zero above a documented threshold; the harness runs against both a fresh profile and a seeded 1k-script library; the measured baseline is recorded so the production-build item can be evaluated against it. Complexity: M
-
P2 — Classify update-host failures instead of reporting them as parse errors Why: Greasy Fork is the dominant update and discovery host, and as of 2026-08-06 its API certificates had expired while update.greasyfork.org sat behind a Cloudflare challenge — an HTML challenge body reaches parseUserscript and surfaces as a generic "Parse failed", so users see a broken script rather than a broken host, and repeated challenges quietly drive the update backoff toward silence. Evidence: greasyfork-org/greasyfork#1553 (Cloudflare challenge on update.greasyfork.org breaking manager update checks), greasyfork-org/greasyfork#1561 (expired certs on api.greasyfork.org / api.sleazyfork.org); src/background/update-checker.ts:375-384 returns
parsed.error ?? 'Parse failed'with no content-type or challenge classification; src/background/core.ts SubscriptionSystem and the install-interception fetch share the pattern; discovery hardcodes https://api.greasyfork.org/en/scripts.json and https://openuserjs.org/api/script/list (pages/dashboard.js); the existing exponential-backoff ring (_updateFailureCount/_updateNextCheck) treats all failures alike. Touches: src/background/update-checker.ts, src/background/core.ts (subscription plus install fetch paths), generated background.core.js/background.js, pages/dashboard.js update banner and Find Scripts source status, src/locales/en.json, tests/ update and subscription coverage. Acceptance: a response that is not a userscript is classified — host-challenge (HTML/Cloudflare interstitial), transport (TLS/DNS/network), http-status, and not-a-userscript — and each is reported with a distinct, actionable message naming the host; host-level failures do not advance the script's failure ring the way a genuine bad body does, and do not mark the script as having a pending update; the Find Scripts panel shows a per-source health state instead of an empty result list; tests cover a Cloudflare challenge body, an HTML error page, a TLS failure and a truncated body. Complexity: M -
P2 — Prove document-start actually wins the race in a real browser Why: Unreliable @run-at document-start is one of the most-reported userscript-manager defects, and ScriptVault's native chrome.userScripts registration is believed to avoid the workarounds competitors need — but no test anywhere asserts a document-start script runs before the page's own inline script, so the claim is untested on both engines. Evidence: src/background/registration.ts:550-581,669 maps @run-at to runAt and is unit-tested for the mapping only; a grep for document-start assertions across tests/e2e/.spec.js and scripts/smoke-.mjs returns no ordering test; Tampermonkey/tampermonkey#211 and Tampermonkey/tampermonkey#2771; Violentmonkey needed an opt-in "Alternative page mode" for this and caps it at ~1 MB per page (https://github.com/violentmonkey/violentmonkey/releases/tag/v2.46.0). Touches: tests/e2e/ (new spec), tests/e2e/helpers/, scripts/smoke-firefox-sideload.mjs, README.md @run-at section, docs/cross-browser-pipeline.md. Acceptance: an E2E fixture page with an inline script that stamps a marker is visited with a registered @run-at document-start userscript; the test asserts the userscript observed the document before the page marker existed, both on a cold service worker and a warm one, and the equivalent assertion runs in the Firefox sideload smoke; the README states the measured behaviour rather than asserting it. Complexity: M
-
P3 — Surface userScripts.execute() syntax diagnostics on the on-demand run path Why: Chrome 149 made userScripts.execute() validate syntax synchronously and return diagnostics on failure, and Firefox 153 added execute() at all — the on-demand run path currently reports a generic failure where the browser can now name the syntax error and its location. Evidence: https://developer.chrome.com/docs/extensions/whats-new (Chrome 149: "validates script syntax synchronously and returns diagnostics on failure"; execute() since Chrome 135); https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/ ("userScripts.execute(), which provides for one-off injection"); src/background/core.ts:7327 and :9376 already branch on chrome.userScripts.execute availability with a hard error when absent. Touches: src/background/core.ts (runScriptNow / on-demand execution), generated background.core.js/background.js, pages/popup.js "Run on This Tab", pages/dashboard.js toast path, src/locales/en.json, tests/ core-flow coverage. Acceptance: when execute() rejects with diagnostics, the message shown to the user includes the reported error and line/column; when the browser supplies no diagnostics the current generic message is retained; Firefox 153+ takes the same path via feature detection and the smoke harness exercises one failing script. Complexity: S
-
P3 — Use runtime.getDocumentId() on Firefox to key per-document injection state Why: The open UserCSS SPA and injection-dedup items key state per tab, which cannot distinguish a reloaded document from a live one; Firefox 153 introduced a stable per-document identifier that makes that distinction directly. Evidence: https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/ ("Firefox 153 introduces documentId, a stable identifier for a document instance, including a new runtime.getDocumentId() method"); relates to the open items "Persistent UserCSS can orphan an injected stylesheet on an SPA route change" and "SPA navigation events are dropped, not coalesced" above — this is an enabling mechanism for those, not a replacement. Touches: src/modules/userstyles.ts, src/background/core.ts (onTabNavigated/onTabUpdated/rehydrateOpenTabs), generated modules/userstyles.js, tests/userstyle-injection.test.js, scripts/smoke-firefox-sideload.mjs. Acceptance: where available, per-document injection state is keyed by documentId rather than tab id, with the tab-id path retained for Chrome; a reload no longer reuses the previous document's registry entry; the Firefox smoke harness proves a reloaded SPA page re-injects exactly once. Complexity: M
-
P3 — Review the Find Scripts surface against the 2026-08-01 Chrome Web Store prohibited-products update Why: CWS added a Malicious and Prohibited Products clause on 2026-08-01 banning circumvention of AI service safety guardrails and usage restrictions; ScriptVault's discovery panel installs arbitrary third-party scripts from Greasy Fork and OpenUserJS, and neither the listing copy nor the in-product risk copy acknowledges that class. Evidence: https://developer.chrome.com/blog/cws-policy-updates-2026 (Limited Use, Disclosure Requirements and the prohibited-products clause all effective 2026-08-01); pages/dashboard.js Find Scripts sources hardcode https://api.greasyfork.org/en/scripts.json and https://openuserjs.org/api/script/list; scripts/check-permission-copy.mjs and docs/store-listing-copy.md cover permissions and privacy but not prohibited-content classes; the @antifeature surface already exists as the disclosure mechanism (https://greasyfork.org/en/help/antifeatures). Touches: docs/store-listing-copy.md, PRIVACY.md, scripts/check-permission-copy.mjs, pages/dashboard.js Find Scripts copy, pages/install.js review copy, docs/cws-remote-code-compliance.md. Acceptance: the listing and in-product copy state that discovery surfaces third-party code the project does not author or endorse and that installation is the user's decision; the compliance doc records the 2026-08-01 clauses and how each is met; the store-copy gate covers the new clauses so a future policy change fails the check instead of passing silently. Complexity: S
-
P3 — Refresh dev dependencies and adopt the test-tooling features already paid for Why: Several dev dependencies are behind current releases and the newer versions ship capabilities that would directly improve this repo's existing harnesses; none of these are security-driven, so they belong behind the P0 audit work. Evidence: current vs latest as of 2026-08-06 — @playwright/test 1.61.1 vs 1.62.1 (2026-07-30), jsdom 29.1.1 vs 30.0.1 (2026-07-29, requires Node >=24.15 which the repo's >=24.16.0 satisfies), puppeteer-core 25.2.1 vs 25.5.0, acorn 8.17.0 vs 8.18.0, chrome-types 0.1.431 vs 0.1.436; Playwright 1.61 added page.localStorage/page.sessionStorage, 1.62 added retryStrategy 'isolated' and WebP screenshots; Vitest 4 ships Playwright trace integration usable from the existing browser-mode config; acorn 8.17's strict option and using/await-using fixes matter for the AST analyzer, whose vendored copy is already 8.17.0. Touches: package.json, package-lock.json, playwright.config.mjs, vitest.visual.config.mjs, tests/e2e/helpers/, scripts/check-cve-floors.mjs, docs/audit/cycle-22-dependency-freshness-*.md, docs/amo-vendored-libraries.md if acorn is re-vendored. Acceptance: dependencies are bumped with the full gate suite green; the extension-state assertions that currently go through page.evaluate() use the WebStorage API where it is clearer; failing browser-mode tests emit a Playwright trace; retryStrategy 'isolated' is applied to the extension-load E2E specs that have historically flaked; if acorn is re-vendored, docs/amo-vendored-libraries.md hashes are regenerated in the same change. Complexity: S