Replaces the blanket skip on claude.ai/code with a targeted walker so
the user's prompt and pasted attachments still get substituted, but
tool_use / tool_result / system / IDs / metadata pass through verbatim.
Fields touched:
- top-level prompt and input (classic claude.ai shape)
- attachments[].extracted_content and attachments[].file_name
- messages[].content (or text parts) when role === 'user'
FormData uploads continue to flow through DocumentScanner since they
are user input. Raw non-JSON bodies are skipped on this path because
they are typically tool traffic, not user text.
https://claude.ai/code/session_01Y2YprLMx348eWD5C9Z4zpV
Two user-reported conflicts when using the extension alongside Claude
Code on the web and with short mappings:
1. Claude Code (claude.ai/code) sends real file paths, usernames, and
shell commands to its tool runtime. Substituting any of these
corrupts execution, forcing users to disable the extension for that
app. early-hook.js and injector.js now short-circuit on that path so
fetch/XHR are never wrapped and no page-world script is injected.
2. Explicit mappings used raw literal regexes, so "not" -> "bad" would
rewrite "nothing" into "badhing". substitute/reveal/scan/diff (and
their page-world mirrors) now add \b on word-character edges only,
leaving non-word edges like "@foo" unchanged so they keep matching.
https://claude.ai/code/session_01Y2YprLMx348eWD5C9Z4zpV
Two issues fixed:
1. autoDetectPII only skipped identity values (names, emails, etc.) but not
mapping values. After clicking "+" to map e.g. an IP address, the fake
substitute IP was immediately re-detected as PII, causing the notification
to reappear. Now autoDetectPII accepts opts.mappings and adds both real
and substitute values to the skip set.
2. The local mappings array was only updated after the async background
round-trip completed. The 150ms re-scan could fire before that, missing
the new mapping. Now an optimistic temp mapping is added to the local
array immediately so the re-scan already knows to skip both values.
https://claude.ai/code/session_01RfzvB5sHah326acr8Xa7Jn
The plus button handler in the pre-send PII warning had two bugs:
1. It read/wrote mappings directly via the storage bridge (api.storage.local),
bypassing the Storage module's encryption layer. When at-rest encryption was
enabled, getStorageData returned an encrypted blob instead of an array,
causing .push() to throw a TypeError that silently aborted the handler —
the mapping was never saved and replaceInInput never ran.
2. Unlike the ignore button which immediately removes the DOM item, the plus
button relied on a re-scan at 150ms to dismiss the notification. If
replaceInInput didn't stick (e.g. React-controlled inputs), the re-scan
found PII again and the notification persisted.
Fix: Route mapping creation through the background service worker via a new
'add:mapping' message handler (which uses Storage.addMapping with proper
encryption support), and immediately dismiss the notification item from the
DOM like the ignore button does.
https://claude.ai/code/session_01RfzvB5sHah326acr8Xa7Jn
Removed Organization section JS (initOrgUI, showOrgJoined, showOrgNotJoined,
setOrgStatus) and OrgPolicy import from options.js. These referenced DOM
elements deleted in a prior commit, causing DOMContentLoaded init to fail
before the encryption button listener could be registered.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
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
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
- Removed the Organization section from options page (unused feature)
- Fixed mapping table columns spreading Real Value and Substitute to
opposite ends of the row. Added table-layout: fixed with explicit
column widths (35/35/12/8/6/4%) so columns are evenly distributed.
Added text-overflow: ellipsis on td so long values truncate cleanly.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
After a browser restart with no extension data, enabling encryption calls
saveSettings() which writes ss_lastModified = Date.now(). All pull paths
then compare that fresh timestamp against the remote's older timestamp,
see local >= remote, and skip the pull with "already on latest version" —
even though local is completely empty.
Fix: add _hasRealLocalData() which returns true only if local storage
contains actual identity profiles or mappings with real values. The
timestamp skip condition now requires BOTH a newer local timestamp AND
real local data. An empty/fresh install always falls through to pull.
Affects pullFromGist, pullFromUrl, pullFromSyncStorage, and importSyncCode.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
The × close button only hid the warning for the current send — the same
detection reappeared on every subsequent send with no way to silence it.
Added an Ignore button to each item in both the on-send warning
(showAutoDetectWarning) and the pre-send input scanner warning
(showPreSendWarning). Clicking Ignore:
- Adds the specific value to an in-memory Set (ignoredDetections)
- Persists it to ss_ignored_detections in storage
- Removes that item from the current warning; closes the popup if empty
On page load, ss_ignored_detections is read from storage so ignores
survive page reloads. Ignored values are also filtered before
auto-redaction in the fetch interceptor, so they are not redacted either.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
Two fixes:
1. grok.com missing from manifest
Grok moved from grok.x.ai to grok.com. The extension wasn't activating
on grok.com at all (no content script injected). Added to host_permissions
and content_scripts in both manifests.
2. Fetch hook bypassed on React/Next.js sites
content.js was injected async (after awaiting the document-scanner),
so by the time it replaced window.fetch, React/Next.js had already stored
a reference to the original. All fetch calls from framework code bypassed
the hook entirely.
Fix: load early-hook.js as a "world": "MAIN" content script at
document_start. This runs synchronously before any page JavaScript,
captures the real fetch in window.__ssOriginalFetch, and replaces
window.fetch with a lightweight wrapper. When content.js eventually
loads, it sets window.__ssInterceptFetch (the real substitution logic)
and window.__ssReady = true, activating the wrapper. React's stored
fetch reference now routes through the interceptor.
Falls back to direct window.fetch replacement if early-hook.js somehow
didn't run (e.g. older browser without world: MAIN support).
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
Two related races caused substitution to silently fail with at-rest encryption:
1. vault:request-unlock was sent at the top of init(), but the runtime.onMessage
listener that handles the vault:unlocked response was registered only after
await-ing the document-scanner script. A warm service worker (e.g. freshly
woken by a sync operation) could respond before the listener existed, dropping
the message permanently.
2. Even if the listener was registered in time, it posted to window immediately,
but content.js hadn't been injected yet, so its window.addEventListener('message')
handler wasn't live and the message went nowhere.
Fix: register api.runtime.onMessage before injecting content.js, and move the
vault:request-unlock send into script.onload — by that point content.js has fully
executed and its message listener is live.
Also stop passing the encrypted settings blob as initial config. When isLocked,
ss_settings is { _ssLocalEncrypted: true, data: '...' }; passing it to content.js
as the initial settings object clutters the settings with encrypted garbage.
Now falls back to { enabled: true } like mappings/identity already did.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
Root cause: when the popup opens for the first time it calls
addProfile('Personal') + updateProfile(...) to create a default empty
profile. Both call saveProfiles, which was unconditionally setting
ss_lastModified: Date.now(). This made the new browser's local
timestamp look like right now — newer than any Gist data pushed by
the source browser — so every pull returned "Already up to date"
without ever prompting for a password or importing anything.
Fixes:
1. storage.js saveProfiles: only advance ss_lastModified when at
least one profile contains real PII (non-empty real value in
names, emails, usernames, phones, or catchAllEmail). Creating the
default empty profile structure on first install leaves
ss_lastModified at 0 so pulls correctly see remote data as newer.
2. sync.js pushToGist: persist ss_last_push_time and
ss_last_push_source alongside ss_gist_id so the source browser
(which only pushes) can also show its last activity time.
3. options.js: display both "Pushed: <time>" and "Pulled: <time>"
in the Gist status area on page load, giving both browsers
meaningful feedback.
4. service-worker.js: after a successful auto-sync pull, broadcast
vault:unlocked to all open content-script tabs so substitution
works immediately without a page reload.
https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
On a fresh browser install with auto-sync enabled, performAutoSync
would push because config.lastPush was null (!config.lastPush = true).
With _getAllData now returning lastModified: 0 for browsers with no
saved data, this pushed a payload with lastModified: 0 to the Gist,
overwriting the real data from the source browser. Subsequent pulls
then saw remoteMod (0) <= local (0) and returned "Already up to date"
without ever prompting for a password or importing anything.
Three fixes:
1. performAutoSync: add local.lastModified > 0 guard to the push
condition so a browser with no saved data never pushes in the
background (both Gist and URL paths).
2. pushToGist / pushToUrl: return an explicit error if lastModified
is 0, preventing a manual Push click on a fresh browser from
clobbering the Gist too.
3. pullFromGist: if the Gist's lastModified is 0 (already clobbered),
return a clear error message telling the user to push from the
source browser first, rather than silently returning "up to date".
https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
When at-rest encryption is enabled, injector.js detects encrypted blobs
in storage, passes empty config to the content script, and waits for a
vault:unlocked broadcast from the background. That broadcast was only
ever triggered when the user explicitly entered their password in the
popup. After a Gist/URL sync import (which writes newly-imported data
in encrypted form), no page ever received the decrypted config, so
substitution silently stopped working.
Two fixes:
1. injector.js: when isLocked is true (encrypted blobs detected), send
vault:request-unlock to the background. If the key is already cached
(e.g. the user authenticated during the sync pull), the background
responds immediately with vault:unlocked containing the decrypted
data. This fixes every new page load after a sync import.
2. service-worker.js: add vault:request-unlock handler that checks
Storage.isLocked() and, if the key is available, reads decrypted
mappings/identity/settings and sends vault:unlocked back to the
requesting tab.
3. options.js: after a successful Gist or URL pull, send vault:unlocked
to the background so it broadcasts decrypted data to all currently-
open tabs immediately, without requiring a page reload.
https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
Three issues in the Gist pull flow on a browser with no prior data:
1. _getAllData() returned Date.now() as lastModified when ss_lastModified
was unset (fresh install). The timestamp check in pullFromGist then
saw the Gist data as older than local, silently returned imported:false
("Already up to date") without importing anything or prompting for
auth. Fixed by using || 0 so a browser with no data always accepts
remote data as newer.
2. _applyData now persists ss_last_pull_time and ss_last_pull_source so
the last successful pull survives page reloads.
3. The Gist section on the options page now shows "Last pulled: <time>"
alongside the Gist ID on load, so both browsers display their sync
status rather than showing nothing on first open.
https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
Two bugs prevented sync from working between different browsers
sharing the same GitHub token and encryption password:
1. pullFromGist failed immediately with "No Gist ID stored" on any
browser that had not previously pushed, because ss_gist_id lives
in browser.storage.local (isolated per browser). Fix: when no
local Gist ID is found, search the authenticated user's Gists for
one containing silent-send-sync.json and cache the result.
2. When the pulled data was encrypted with a different salt (each
browser generates its own random salt on setup), pullFromGist
returned needsAuth:true but the UI never set
window.__ssPendingSyncImport, so the auth prompt fell through to
reverifyWithPassword instead of authenticateForSync, and the pull
was never retried after the user entered their password. Fix: set
window.__ssPendingSyncImport before showing the auth prompt for
both Gist pull and custom-URL pull, matching how sync-code import
already handled this flow.
https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
The fetch/XHR interception correctly checked settings.enabled, but
several other features ignored it — highlights, reveal mode, and
PII auto-detect all continued running after disabling. This adds
settings.enabled checks to the MutationObserver, highlightMatches,
auto-detect input/paste listeners, reveal mode, and cleans up
visual artifacts (highlights, warnings) when the extension is
toggled off.
https://claude.ai/code/session_015TEttQgcq5FALLKLb3uEW8