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
- options.html/js: Footer version now reads dynamically from manifest via
api.runtime.getManifest().version instead of hardcoded v0.3.0
- document-scanner.js: Increase FlateDecode lookback from 200 to 500 bytes
(real PDF stream dictionaries are often larger than 200 bytes), and support
array form /Filter [/FlateDecode] alongside the scalar form
https://claude.ai/code/session_01ReZUeR1nrYzJeX7fTqwXMw
- document-scanner.js: Add missing await on _inflateSync() call — FlateDecode
streams were not being decompressed because the async function was called
without await, causing decompressed to hold a Promise instead of data
- content.js, storage.js, options.html, options.js: Gate proper noun / capital
letter detection behind a new detectProperNouns setting (default off); was
previously always-on causing noisy false-positive warnings on AI thinking
output and common capitalized words
- Bump version 0.9.23 → 0.9.25
https://claude.ai/code/session_01ReZUeR1nrYzJeX7fTqwXMw
The old unrevealInElement relied on a WeakMap (originalTexts) to restore
text nodes to their pre-reveal state. This broke when SPA frameworks
(React) re-rendered the DOM while reveal was active — new text nodes
containing real values had no WeakMap entry to restore from, so real
data stayed visible after turning reveal off.
Fix: unrevealInElement now actively reverse-replaces real values back
to their substitute counterparts using the reveal pairs, matching the
same approach revealText uses in the forward direction. This works
regardless of DOM re-renders or streaming content changes.
https://claude.ai/code/session_01NNBEPuXMFGWezJb1f958nL
- Inject document-scanner.js into page world via injector.js (as module,
sets globalThis.DocumentScanner)
- Add FormData interception to fetch hook: scans File/Blob entries through
DocumentScanner.processUpload(), also substitutes string fields
- Add document-scanner.js to web_accessible_resources in both manifests
- Bump version to 0.9.23 in package.json, manifest.json, manifest.firefox.json
https://claude.ai/code/session_01NNBEPuXMFGWezJb1f958nL
Going back to a known-good baseline. This version had:
- Working reveal mode with CSS Highlight API
- Working substitution (fetch + XHR hooks)
- Smart patterns (names, emails, phones, usernames)
- Encryption/sync (password, TOTP, WebAuthn)
- Multiple identity profiles
- Activity log
- Secret scanner
- Auto-detect PII warnings
- Pre-send PII detection
Kept current manifests (UUID, data_collection_permissions, version).
No renames applied — uses original naming (secretScanning, PPI, etc).
Will re-apply renames and new features from this working base.
https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
The data-ss-config attribute on the script tag was being removed
(via script.onload) before content.js could read it. Changed approach:
inject config as a separate <script type="application/json" id="ss-config-data">
element that persists in the DOM until content.js reads and removes it.
This eliminates the race condition between script execution and onload
removal. Bump 0.9.20.
https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT