From 3d8642db52fa1c37ea33611b01081cb3d3c7e0c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 13:19:14 +0000 Subject: [PATCH 1/3] Fix whitespace-only mappings highlighting every space on the page A mapping imported with real: " " (single space) passed the !m.real guard (space is truthy), producing new RegExp(" ", "gi") which matched every space character in the DOM and highlighted them all. - Added .trim() to all real/substitute guard conditions in content.js (inline substitute, reveal functions) and substitution-engine.js so whitespace-only values are treated as empty and skipped - Added the same guard in highlightMatches so the CSS Highlight API never creates ranges for blank search terms - Added trim + blank-check to the bulk import path in options.js so whitespace-only real values are dropped at import time rather than saved to storage https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo --- src/content/content.js | 5 +++-- src/lib/substitution-engine.js | 8 ++++---- src/options/options.js | 7 ++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/content/content.js b/src/content/content.js index 552fd65..c71ec63 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -50,7 +50,7 @@ const sorted = [...maps].sort((a, b) => b.real.length - a.real.length); for (const m of sorted) { - if (!m.enabled || !m.real || !m.substitute) continue; + if (!m.enabled || !m.real?.trim() || !m.substitute?.trim()) continue; const escaped = esc(m.real); const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi'); let match; @@ -70,7 +70,7 @@ let result = text; const sorted = [...maps].sort((a, b) => b.substitute.length - a.substitute.length); for (const m of sorted) { - if (!m.enabled || !m.real || !m.substitute) continue; + if (!m.enabled || !m.real?.trim() || !m.substitute?.trim()) continue; const escaped = esc(m.substitute); const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi'); result = result.replace(regex, m.real); @@ -1130,6 +1130,7 @@ for (const p of pairs) { const searchTerm = settings.revealMode ? p.to : p.from; + if (!searchTerm?.trim()) continue; const escaped = esc(searchTerm); // Add word boundaries when the term starts/ends with word chars to // prevent partial-word matches (e.g. "aud" inside "Claude") diff --git a/src/lib/substitution-engine.js b/src/lib/substitution-engine.js index f7e5fd9..d0e64ec 100644 --- a/src/lib/substitution-engine.js +++ b/src/lib/substitution-engine.js @@ -21,7 +21,7 @@ const SubstitutionEngine = { ); for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -54,7 +54,7 @@ const SubstitutionEngine = { ); for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.substitute); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -71,7 +71,7 @@ const SubstitutionEngine = { const found = []; for (const mapping of mappings) { - if (!mapping.enabled || !mapping.real) continue; + if (!mapping.enabled || !mapping.real?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -103,7 +103,7 @@ const SubstitutionEngine = { // Collect all match positions in the original text const matches = []; for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); diff --git a/src/options/options.js b/src/options/options.js index 313aedf..f78374a 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -1814,11 +1814,12 @@ async function applyBulkImport() { await Storage.updateProfile(profile.id, profile); } - // Add mappings + // Add mappings — skip any with blank/whitespace-only real values for (const m of result.mappings) { + if (!m.real?.trim()) continue; await Storage.addMapping({ - real: m.real, - substitute: m.substitute || '', + real: m.real.trim(), + substitute: m.substitute?.trim() || '', category: m.category || 'general', caseSensitive: false, }); From 617a2d10eb21b24b7644f9d37afe8565ee036d0d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 13:23:33 +0000 Subject: [PATCH 2/3] Wrap fetch interceptor in top-level try/catch to prevent breaking page requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If anything in Silent Send's substitution logic throws (bad mapping, regex error, unexpected body format, etc.), the exception was propagating up to the caller instead of falling through to the original fetch. This broke Claude Code's web interface (claude.ai/code) when a bad mapping caused a substitution error mid-request. The inner try/catch on JSON.parse only covered the JSON path — the outer logic (hasSubstitutions, processBody, notifySubstitutions, etc.) had no protection. Added a top-level try/catch that catches any unhandled error and falls through to originalFetch, logging a warning so the error is still visible in the console. https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo --- src/content/content.js | 125 ++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/src/content/content.js b/src/content/content.js index c71ec63..0702685 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -825,67 +825,74 @@ } window.__ssInterceptFetch = async function (url, options) { - if (!settings.enabled || !hasSubstitutions()) { + // Top-level guard: any unhandled error must never break the original request. + try { + if (!settings.enabled || !hasSubstitutions()) { + return originalFetch.call(this, url, options); + } + + const urlStr = typeof url === 'string' ? url : url?.url || ''; + const method = (options?.method || 'GET').toUpperCase(); + + // Only intercept POST/PUT/PATCH + if ( + (method === 'POST' || method === 'PUT' || method === 'PATCH') && + options?.body && !shouldSkipUrl(urlStr) + ) { + // FormData body — scan file uploads via DocumentScanner + if (options.body instanceof FormData && typeof globalThis.DocumentScanner !== 'undefined') { + try { + const scannedForm = await scanFormData(options.body); + if (scannedForm) { + options = { ...options, body: scannedForm.formData }; + if (scannedForm.replacements.length > 0) { + notifySubstitutions(scannedForm.replacements); + console.log( + `[Silent Send] Substituted ${scannedForm.replacements.length} value(s) in file upload to ${urlStr}` + ); + } + } + } catch (e) { + console.warn('[Silent Send] FormData scan failed:', e); + } + } + + // String body — JSON or raw text + if (typeof options.body === 'string') { + try { + // Try JSON + const body = JSON.parse(options.body); + const { modified, replacements } = processBody(body); + + if (modified) { + options = { ...options, body: JSON.stringify(body) }; + notifySubstitutions(replacements); + console.log( + `[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}` + ); + } + } catch (e) { + // Not JSON — try raw string substitution (form data, etc.) + if (options.body.length > MIN_STRING_LENGTH) { + const result = substituteAll(options.body); + if (result.modified) { + options = { ...options, body: result.text }; + notifySubstitutions(result.replacements); + console.log( + `[Silent Send] Substituted ${result.replacements.length} value(s) in form body` + ); + } + } + } + } + } + + return originalFetch.call(this, url, options); + } catch (e) { + // Something went wrong in Silent Send — never block the original request + console.warn('[Silent Send] Fetch interceptor error, passing through:', e); return originalFetch.call(this, url, options); } - - const urlStr = typeof url === 'string' ? url : url?.url || ''; - const method = (options?.method || 'GET').toUpperCase(); - - // Only intercept POST/PUT/PATCH - if ( - (method === 'POST' || method === 'PUT' || method === 'PATCH') && - options?.body && !shouldSkipUrl(urlStr) - ) { - // FormData body — scan file uploads via DocumentScanner - if (options.body instanceof FormData && typeof globalThis.DocumentScanner !== 'undefined') { - try { - const scannedForm = await scanFormData(options.body); - if (scannedForm) { - options = { ...options, body: scannedForm.formData }; - if (scannedForm.replacements.length > 0) { - notifySubstitutions(scannedForm.replacements); - console.log( - `[Silent Send] Substituted ${scannedForm.replacements.length} value(s) in file upload to ${urlStr}` - ); - } - } - } catch (e) { - console.warn('[Silent Send] FormData scan failed:', e); - } - } - - // String body — JSON or raw text - if (typeof options.body === 'string') { - try { - // Try JSON - const body = JSON.parse(options.body); - const { modified, replacements } = processBody(body); - - if (modified) { - options = { ...options, body: JSON.stringify(body) }; - notifySubstitutions(replacements); - console.log( - `[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}` - ); - } - } catch (e) { - // Not JSON — try raw string substitution (form data, etc.) - if (options.body.length > MIN_STRING_LENGTH) { - const result = substituteAll(options.body); - if (result.modified) { - options = { ...options, body: result.text }; - notifySubstitutions(result.replacements); - console.log( - `[Silent Send] Substituted ${result.replacements.length} value(s) in form body` - ); - } - } - } - } - } - - return originalFetch.call(this, url, options); }; // Activate the fetch hook. If early-hook.js ran first (world: MAIN, document_start), From 450688a78761228596f712d1fa1cc32367e0ae45 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 13:27:01 +0000 Subject: [PATCH 3/3] Bump version to 0.9.43 https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo --- manifest.firefox.json | 2 +- manifest.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest.firefox.json b/manifest.firefox.json index dc9888f..ae8355f 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.42", + "version": "0.9.43", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index 2dec8c9..425b3ec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.42", + "version": "0.9.43", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index 3213a9e..0baad02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.9.42", + "version": "0.9.43", "private": true, "license": "MIT", "description": "Browser extension that substitutes personal data before sending to AI services",