diff --git a/README.md b/README.md index 755adee..aeb431a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,37 @@ # Silent Send -A Chrome extension that intercepts personal information and substitutes it with user-defined replacements before sending to Claude.ai. +A browser extension (Chrome + Firefox) that intercepts personal information and substitutes it with user-defined replacements before sending to Claude.ai. ## How it works -1. **You define mappings** — e.g. "John Smith" → "Alex Demo", "john@gmail.com" → "alex@example.com" -2. **You type normally** — you see your real text while composing -3. **On send, it swaps** — the extension intercepts the API request and replaces real values with substitutes -4. **Badge shows count** — the extension icon shows how many substitutions were made -5. **Reveal mode** — optionally translates Claude's responses back to your real data +1. **You fill in your Identity** — name, email, username, computer name, phone +2. **Smart patterns auto-catch variations** — `jsmith@macbook-pro`, `John's`, `/home/jsmith`, `555.123.4567` +3. **You type normally** — you see your real text while composing +4. **On send, it swaps** — the extension intercepts the API request and replaces real values with substitutes +5. **Badge shows count** — the extension icon shows how many substitutions were made +6. **Reveal mode** — optionally translates Claude's responses back to your real data + +### Smart pattern examples + +| You type | What gets sent | +|----------|---------------| +| `jsmith@macbook-pro` | `ademo@mycomputer` | +| `anyone@gmail.com` | `anon@example.com` (catch-all) | +| `John Smith` | `Alex Demo` | +| `Smith, John` | `Demo, Alex` | +| `John's code` | `Alex's code` | +| `/home/jsmith/project` | `/home/ademo/project` | +| `~jsmith` | `~ademo` | +| `C:\Users\jsmith` | `C:\Users\ademo` | +| `(555) 123-4567` | `(555) 000-0000` | +| `555.123.4567` | `(555) 000-0000` | +| `macbook-pro` | `mycomputer` | ## How to verify it's working - **Badge count** on the extension icon shows substitutions per page - **Activity tab** in the popup shows a timestamped log of every substitution -- **Test tab** in the popup lets you type text and see the before/after diff +- **Test tab** in the popup lets you type text and see the before/after diff live - **Reveal mode** (eye icon) toggles showing real vs substitute data in responses - **Browser DevTools** → Console shows `[Silent Send] Substituted N value(s)` messages @@ -29,28 +46,61 @@ A Chrome extension that intercepts personal information and substitutes it with 6. Navigate to claude.ai — the extension is active ### Firefox (signed, persistent) -1. Clone this repo -2. `npm install` -3. Get your free Mozilla API credentials at https://addons.mozilla.org/developers/addon/api/key/ -4. `cp .env.example .env` and fill in your key + secret -5. `source .env && npm run sign:firefox` -6. Install the `.xpi` file from `dist/firefox-signed/` — drag it into Firefox or use File → Open -7. Navigate to claude.ai — the extension is active -The signed `.xpi` is permanent — survives restarts, no store listing needed. +Firefox requires extensions to be signed. Mozilla provides free self-hosted signing — no store listing needed. + +#### Step 1: Get your Mozilla API credentials + +1. Go to https://addons.mozilla.org/developers/addon/api/key/ +2. Sign in with a Firefox account (create one free if you don't have one) +3. On that page you'll see two values: + - **JWT issuer** — looks like `user:12345678:901` + - **JWT secret** — a long alphanumeric string +4. These are your API key and secret + +#### Step 2: Configure and sign + +```bash +# Install dependencies +npm install + +# Copy the env template and fill in your credentials +cp .env.example .env +``` + +Edit `.env` with the values from step 1: +``` +WEB_EXT_API_KEY="user:12345678:901" +WEB_EXT_API_SECRET="your-jwt-secret-here" +``` + +Then build and sign: +```bash +source .env && npm run sign:firefox +``` + +This produces a signed `.xpi` file in `dist/firefox-signed/`. + +#### Step 3: Install the signed extension + +- Drag the `.xpi` file into any Firefox window, **or** +- Firefox menu → File → Open File → select the `.xpi` +- Click "Add" when prompted + +The signed `.xpi` is **permanent** — it survives browser restarts, updates, everything. No store listing, no review process, no fees. ### Firefox (temporary, for development) -``` +```bash npm install && npm run run:firefox ``` -Opens Firefox with the extension pre-loaded. Reloads on file changes. - -No store required for either browser. +Opens Firefox with the extension pre-loaded. Auto-reloads on file changes. Resets when Firefox closes. ## Architecture ``` -manifest.json — Extension manifest (Manifest V3) +manifest.json — Chrome extension manifest (Manifest V3) +manifest.firefox.json — Firefox variant (adds gecko ID for signing) +build.sh — Copies the right manifest to dist/{chrome,firefox}/ src/ background/ service-worker.js — Badge management, logging coordination @@ -59,11 +109,12 @@ src/ content.js — Page script (main world) — hooks fetch(), does substitution content.css — Visual indicators (highlights, reveals) popup/ - popup.html/css/js — Quick access: add mappings, view activity, test mode + popup.html/css/js — Quick access: identity, mappings, activity, test mode options/ options.html/css/js — Full mapping management, import/export, settings lib/ - substitution-engine.js — Core find/replace logic + substitution-engine.js — Core explicit find/replace logic + smart-patterns.js — Auto-detection of emails, names, usernames, hostnames, phones, paths storage.js — Browser storage wrapper browser-polyfill.js — Chrome/Firefox API compatibility ``` @@ -73,3 +124,4 @@ src/ - All data stays local in browser storage - No external servers, no telemetry, no analytics - The extension only activates on claude.ai +- Your real identity data never leaves your machine diff --git a/src/content/content.js b/src/content/content.js index 4d6988a..1de8d68 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -186,15 +186,22 @@ } } - // Usernames + paths + // Usernames + hostnames + paths if (id.enabled.usernames !== false) { + const hostMap = new Map(); + for (const h of (id.hostnames || [])) { + if (h.real && h.substitute) hostMap.set(h.real.toLowerCase(), h); + } + for (const u of (id.usernames || [])) { if (!u.real || !u.substitute) continue; - // user@hostname + // user@hostname (substitute both username and hostname if configured) result = result.replace(new RegExp(esc(u.real) + '@[a-zA-Z0-9._\\-]+', 'g'), (matched) => { const host = matched.slice(u.real.length + 1); - const sub = u.substitute + '@' + host; + const hostEntry = hostMap.get(host.toLowerCase()); + const subHost = hostEntry ? hostEntry.substitute : host; + const sub = u.substitute + '@' + subHost; replacements.push({ original: matched, replaced: sub, category: 'username', pattern: 'smart' }); return sub; }); @@ -228,6 +235,16 @@ }); } } + + // Standalone hostname substitution + for (const [, h] of hostMap) { + if (h.real.length >= 3) { + result = result.replace(new RegExp('\\b' + esc(h.real) + '\\b', 'gi'), (matched) => { + replacements.push({ original: matched, replaced: h.substitute, category: 'hostname', pattern: 'smart' }); + return h.substitute; + }); + } + } } return { text: result, replacements }; diff --git a/src/lib/smart-patterns.js b/src/lib/smart-patterns.js index 6af22c4..9264c9e 100644 --- a/src/lib/smart-patterns.js +++ b/src/lib/smart-patterns.js @@ -222,13 +222,17 @@ const SmartPatterns = { return { text: result, replacements }; }, - // ----- Usernames ----- + // ----- Usernames + Hostnames ----- // Catches: user@hostname, ~username, /home/username, mentions of username - // in shell/code contexts + // in shell/code contexts. Also substitutes hostnames independently. _substituteUsernames(text, identity) { const replacements = []; let result = text; const usernames = identity.usernames || []; + const hostMap = new Map(); + for (const h of (identity.hostnames || [])) { + if (h.real && h.substitute) hostMap.set(h.real.toLowerCase(), h); + } for (const u of usernames) { if (!u.real || !u.substitute) continue; @@ -240,7 +244,10 @@ const SmartPatterns = { ); result = result.replace(userHostRegex, (matched) => { const host = matched.slice(u.real.length + 1); - const sub = u.substitute + '@' + host; + // Also substitute hostname if configured + const hostEntry = hostMap.get(host.toLowerCase()); + const subHost = hostEntry ? hostEntry.substitute : host; + const sub = u.substitute + '@' + subHost; replacements.push({ original: matched, replaced: sub, @@ -279,6 +286,22 @@ const SmartPatterns = { } } + // Standalone hostname substitution (catches hostnames appearing on their own) + for (const [, h] of hostMap) { + if (h.real.length >= 3) { + const hostRegex = new RegExp('\\b' + esc(h.real) + '\\b', 'gi'); + result = result.replace(hostRegex, (matched) => { + replacements.push({ + original: matched, + replaced: h.substitute, + category: 'hostname', + pattern: 'smart-hostname', + }); + return h.substitute; + }); + } + } + return { text: result, replacements }; }, diff --git a/src/popup/popup.html b/src/popup/popup.html index 7680ef9..3c577af 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -74,7 +74,12 @@ -
Catches: jsmith@hostname, /home/jsmith, ~jsmith, C:\Users\jsmith
+
+ + + +
+
Catches: jsmith@macbook-pro, /home/jsmith, ~jsmith, C:\Users\jsmith
diff --git a/src/popup/popup.js b/src/popup/popup.js index 6d94a53..89f5dad 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -111,6 +111,11 @@ function loadIdentityForm() { $('#idUserReal').value = user.real || ''; $('#idUserSub').value = user.substitute || ''; } + const host = (identity.hostnames || [])[0]; + if (host) { + $('#idHostReal').value = host.real || ''; + $('#idHostSub').value = host.substitute || ''; + } if (phone) { $('#idPhoneReal').value = phone.real || ''; $('#idPhoneSub').value = phone.substitute || ''; @@ -144,6 +149,13 @@ async function saveIdentity() { usernames.push({ real: userReal, substitute: userSub }); } + const hostnames = []; + const hostReal = $('#idHostReal').value.trim(); + const hostSub = $('#idHostSub').value.trim(); + if (hostReal && hostSub) { + hostnames.push({ real: hostReal, substitute: hostSub }); + } + const phones = []; const phoneReal = $('#idPhoneReal').value.trim(); const phoneSub = $('#idPhoneSub').value.trim(); @@ -155,6 +167,7 @@ async function saveIdentity() { names, emails, usernames, + hostnames, phones, catchAllEmail: $('#idCatchAllEmail').value.trim(), emailDomains: identity.emailDomains || [],