feat: add hostname substitution in user@host patterns
jsmith@macbook-pro now becomes ademo@mycomputer when both username and hostname are configured in the Identity tab. Hostnames are also caught standalone (e.g. just "macbook-pro" in text). Also rewrites README with detailed step-by-step Mozilla API key setup instructions for Firefox signing. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -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
|
||||
|
||||
+20
-3
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
|
||||
@@ -74,7 +74,12 @@
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idUserSub" placeholder="ademo" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: jsmith@hostname, /home/jsmith, ~jsmith, C:\Users\jsmith</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idHostReal" placeholder="macbook-pro" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idHostSub" placeholder="mycomputer" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: jsmith@macbook-pro, /home/jsmith, ~jsmith, C:\Users\jsmith</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
Reference in New Issue
Block a user