feat: v0.3.0 — multiple entries per field, README setup guide

Identity fields now support multiple entries per type:
- Add unlimited names (first, last, middle, nickname), emails,
  usernames, hostnames, and phone numbers per profile
- "+ Add" button on each section, "x" to remove rows
- Names have a type selector (1st/Last/Mid/Nick)

README now includes:
- First-time setup walkthrough (step by step)
- Icon color legend (gray/black/blue/red)
- Keyboard shortcuts table
- Note that extension does nothing until configured

Also bumps version to 0.3.0 for Firefox re-signing.

https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
Claude
2026-03-26 03:08:46 +00:00
parent c4356c3c6a
commit 1995208941
8 changed files with 253 additions and 107 deletions
+36 -1
View File
@@ -60,12 +60,47 @@ A browser extension (Chrome + Firefox) that intercepts personal information and
| `123-45-6789` | `[REDACTED-SSN]` |
| `4111 1111 1111 1111` | `[REDACTED-CARD]` |
## First-time setup
After installing the extension, **it does nothing until you configure it**. The icon will be gray to remind you.
1. Click the Silent Send icon in your toolbar
2. You'll see the **Identity** tab with a red "Setup required" banner
3. Fill in your real info and fake substitutes:
- **Names** — add as many as needed (first, last, nicknames). Click "+ Add" for more rows.
- **Emails** — your real email → fake email. Set a catch-all for unknown addresses.
- **Usernames** — your system username → fake username
- **Hostnames** — your computer name → fake computer name
- **Phones** — your phone number → fake number
4. Click **Save Identity**
5. The icon turns **black** — you're now protected
You can create multiple profiles (Personal, Work, Spouse) using the dropdown at the top. Each can be toggled on/off independently.
### Icon colors
| Icon color | Meaning |
|-----------|---------|
| **Gray** | Not configured — does nothing until you set up your identity |
| **Black** | Active and protecting |
| **Blue** | Reveal mode on — showing your real data in AI responses |
| **Red** | Manually disabled |
### Keyboard shortcuts
| Shortcut | Action |
|----------|--------|
| `Alt+Shift+R` | Toggle reveal mode |
| `Alt+Shift+S` | Toggle Silent Send on/off |
Remap in Chrome: `chrome://extensions/shortcuts` | Firefox: `about:addons` → gear → Manage Extension Shortcuts
## 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 live
- **Reveal mode** (eye icon) toggles showing real vs substitute data in responses
- **Reveal mode** (eye icon or `Alt+Shift+R`) shows your real data in AI responses for easy copy/paste
- **Browser DevTools** → Console shows `[Silent Send] Substituted N value(s)` messages
## Installation
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Silent Send",
"version": "0.2.0",
"version": "0.3.0",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"browser_specific_settings": {
"gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Silent Send",
"version": "0.2.0",
"version": "0.3.0",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"permissions": [
"storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "silent-send",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"description": "Browser extension that substitutes personal data before sending to AI services",
"scripts": {
+1 -1
View File
@@ -136,7 +136,7 @@
</section>
<footer>
<p>Silent Send v0.2.0</p>
<p>Silent Send v0.3.0</p>
</footer>
</div>
+49 -1
View File
@@ -310,13 +310,61 @@ body {
margin-bottom: 4px;
}
.id-label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.id-label {
font-size: 11px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.btn-add {
background: none;
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 2px 8px;
font-size: 10px;
font-weight: 500;
color: #6b7280;
cursor: pointer;
}
.btn-add:hover { background: #f3f4f6; color: #111; }
.id-entry-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.id-entry-row .input-sm { flex: 1; }
.id-entry-row .btn-remove {
background: none;
border: none;
color: #d1d5db;
cursor: pointer;
font-size: 16px;
padding: 0 2px;
line-height: 1;
}
.id-entry-row .btn-remove:hover { color: #dc2626; }
.id-type-label {
font-size: 9px;
color: #9ca3af;
text-transform: uppercase;
min-width: 28px;
text-align: center;
}
.id-row {
+16 -36
View File
@@ -60,55 +60,35 @@
<p class="help-text">Each profile is a person/identity. All active profiles are protected simultaneously.</p>
<div class="id-section">
<div class="id-label">Names</div>
<div class="id-row">
<input type="text" id="idFirstReal" placeholder="First name" class="input input-sm">
<span class="arrow">&rarr;</span>
<input type="text" id="idFirstSub" placeholder="Fake first" class="input input-sm">
</div>
<div class="id-row">
<input type="text" id="idLastReal" placeholder="Last name" class="input input-sm">
<span class="arrow">&rarr;</span>
<input type="text" id="idLastSub" placeholder="Fake last" class="input input-sm">
</div>
<div class="id-label-row"><span class="id-label">Names</span><button class="btn-add" data-field="names">+ Add</button></div>
<div id="idNames"></div>
<div class="id-hint">Catches: first last, last first, possessives, case variants</div>
</div>
<div class="id-section">
<div class="id-label">Email</div>
<div class="id-label-row"><span class="id-label">Emails</span><button class="btn-add" data-field="emails">+ Add</button></div>
<div id="idEmails"></div>
<div class="id-row">
<input type="email" id="idEmailReal" placeholder="you@gmail.com" class="input input-sm">
<span class="arrow">&rarr;</span>
<input type="email" id="idEmailSub" placeholder="fake@example.com" class="input input-sm">
</div>
<div class="id-row">
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for unknown addresses)" class="input" style="flex:1">
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for any unrecognized address)" class="input" style="flex:1">
</div>
<div class="id-hint">Catches: any *@gmail.com, *@yahoo.com, *@outlook.com, etc.</div>
</div>
<div class="id-section">
<div class="id-label">Username / Computer</div>
<div class="id-row">
<input type="text" id="idUserReal" placeholder="jsmith" class="input input-sm">
<span class="arrow">&rarr;</span>
<input type="text" id="idUserSub" placeholder="ademo" class="input input-sm">
</div>
<div class="id-row">
<input type="text" id="idHostReal" placeholder="macbook-pro" class="input input-sm">
<span class="arrow">&rarr;</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 class="id-label-row"><span class="id-label">Usernames</span><button class="btn-add" data-field="usernames">+ Add</button></div>
<div id="idUsernames"></div>
<div class="id-hint">Catches: user@host, /home/user, ~user, C:\Users\user</div>
</div>
<div class="id-section">
<div class="id-label">Phone</div>
<div class="id-row">
<input type="text" id="idPhoneReal" placeholder="(555) 123-4567" class="input input-sm">
<span class="arrow">&rarr;</span>
<input type="text" id="idPhoneSub" placeholder="(555) 000-0000" class="input input-sm">
</div>
<div class="id-label-row"><span class="id-label">Hostnames</span><button class="btn-add" data-field="hostnames">+ Add</button></div>
<div id="idHostnames"></div>
<div class="id-hint">Catches: hostname in user@hostname and standalone</div>
</div>
<div class="id-section">
<div class="id-label-row"><span class="id-label">Phones</span><button class="btn-add" data-field="phones">+ Add</button></div>
<div id="idPhones"></div>
<div class="id-hint">Catches: all formats — (555) 123-4567, 555-123-4567, 555.123.4567</div>
</div>
+148 -65
View File
@@ -191,81 +191,164 @@ function renderProfileSelector() {
$('#profileActive').checked = profile?.active ?? true;
}
// --- Identity ---
// --- Identity (dynamic multi-entry) ---
// Field configs for rendering
const FIELD_CONFIGS = {
names: {
container: 'idNames',
placeholderReal: 'Real name',
placeholderSub: 'Fake name',
typeOptions: [{ value: 'first', label: '1st' }, { value: 'last', label: 'Last' }, { value: 'middle', label: 'Mid' }, { value: 'nick', label: 'Nick' }],
defaultType: 'first',
},
emails: {
container: 'idEmails',
placeholderReal: 'you@gmail.com',
placeholderSub: 'fake@example.com',
},
usernames: {
container: 'idUsernames',
placeholderReal: 'jsmith',
placeholderSub: 'ademo',
},
hostnames: {
container: 'idHostnames',
placeholderReal: 'macbook-pro',
placeholderSub: 'mycomputer',
},
phones: {
container: 'idPhones',
placeholderReal: '(555) 123-4567',
placeholderSub: '(555) 000-0000',
},
};
function renderFieldList(fieldName, items) {
const config = FIELD_CONFIGS[fieldName];
const container = $(`#${config.container}`);
if (!container) return;
if (!items || items.length === 0) {
// Show one empty row
items = [{ real: '', substitute: '', type: config.defaultType || '' }];
}
container.innerHTML = items.map((item, i) => {
let typeHtml = '';
if (config.typeOptions) {
typeHtml = `<select class="id-type-select" data-index="${i}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
config.typeOptions.map(o =>
`<option value="${o.value}" ${item.type === o.value ? 'selected' : ''}>${o.label}</option>`
).join('') +
`</select>`;
}
return `<div class="id-entry-row" data-index="${i}">
${typeHtml}
<input type="text" class="input input-sm id-real" value="${escapeAttr(item.real || '')}" placeholder="${config.placeholderReal}">
<span class="arrow" style="font-size:12px">&rarr;</span>
<input type="text" class="input input-sm id-sub" value="${escapeAttr(item.substitute || '')}" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button>
</div>`;
}).join('');
// Bind remove buttons
container.querySelectorAll('.btn-remove').forEach(btn => {
btn.addEventListener('click', () => {
const row = btn.closest('.id-entry-row');
if (container.querySelectorAll('.id-entry-row').length > 1) {
row.remove();
} else {
// Clear instead of remove if it's the last row
row.querySelectorAll('input').forEach(inp => inp.value = '');
}
});
});
}
function loadIdentityForm() {
const profile = profiles.find(p => p.id === currentProfileId);
if (!profile) return;
const first = (profile.names || []).find(n => n.type === 'first');
const last = (profile.names || []).find(n => n.type === 'last');
const email = (profile.emails || [])[0];
const user = (profile.usernames || [])[0];
const host = (profile.hostnames || [])[0];
const phone = (profile.phones || [])[0];
$('#idFirstReal').value = first?.real || '';
$('#idFirstSub').value = first?.substitute || '';
$('#idLastReal').value = last?.real || '';
$('#idLastSub').value = last?.substitute || '';
$('#idEmailReal').value = email?.real || '';
$('#idEmailSub').value = email?.substitute || '';
renderFieldList('names', profile.names || []);
renderFieldList('emails', profile.emails || []);
renderFieldList('usernames', profile.usernames || []);
renderFieldList('hostnames', profile.hostnames || []);
renderFieldList('phones', profile.phones || []);
$('#idCatchAllEmail').value = profile.catchAllEmail || '';
$('#idUserReal').value = user?.real || '';
$('#idUserSub').value = user?.substitute || '';
$('#idHostReal').value = host?.real || '';
$('#idHostSub').value = host?.substitute || '';
$('#idPhoneReal').value = phone?.real || '';
$('#idPhoneSub').value = phone?.substitute || '';
$('#profileActive').checked = profile.active ?? true;
// Bind add buttons
$$('.btn-add').forEach(btn => {
// Remove old listeners by cloning
const newBtn = btn.cloneNode(true);
btn.replaceWith(newBtn);
newBtn.addEventListener('click', () => {
const field = newBtn.dataset.field;
const config = FIELD_CONFIGS[field];
const container = $(`#${config.container}`);
const count = container.querySelectorAll('.id-entry-row').length;
const tempDiv = document.createElement('div');
let typeHtml = '';
if (config.typeOptions) {
// Alternate: if first row is 'first', next should be 'last', etc.
const defaultType = count % 2 === 0 ? 'first' : 'last';
typeHtml = `<select class="id-type-select" data-index="${count}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
config.typeOptions.map(o =>
`<option value="${o.value}" ${o.value === defaultType ? 'selected' : ''}>${o.label}</option>`
).join('') +
`</select>`;
}
tempDiv.innerHTML = `<div class="id-entry-row" data-index="${count}">
${typeHtml}
<input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}">
<span class="arrow" style="font-size:12px">&rarr;</span>
<input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button>
</div>`;
const row = tempDiv.firstElementChild;
container.appendChild(row);
row.querySelector('.btn-remove').addEventListener('click', () => {
if (container.querySelectorAll('.id-entry-row').length > 1) row.remove();
else row.querySelectorAll('input').forEach(inp => inp.value = '');
});
row.querySelector('.id-real').focus();
});
});
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
}
// Read entries from a dynamic field list
function readFieldEntries(fieldName) {
const config = FIELD_CONFIGS[fieldName];
const container = $(`#${config.container}`);
if (!container) return [];
const entries = [];
container.querySelectorAll('.id-entry-row').forEach(row => {
const real = row.querySelector('.id-real')?.value.trim() || '';
const sub = row.querySelector('.id-sub')?.value.trim() || '';
if (real && sub) {
const entry = { real, substitute: sub };
const typeSelect = row.querySelector('.id-type-select');
if (typeSelect) entry.type = typeSelect.value;
entries.push(entry);
}
});
return entries;
}
async function saveIdentity() {
const names = [];
const firstReal = $('#idFirstReal').value.trim();
const firstSub = $('#idFirstSub').value.trim();
if (firstReal && firstSub) {
names.push({ real: firstReal, substitute: firstSub, type: 'first' });
}
const lastReal = $('#idLastReal').value.trim();
const lastSub = $('#idLastSub').value.trim();
if (lastReal && lastSub) {
names.push({ real: lastReal, substitute: lastSub, type: 'last' });
}
const emails = [];
const emailReal = $('#idEmailReal').value.trim();
const emailSub = $('#idEmailSub').value.trim();
if (emailReal && emailSub) {
emails.push({ real: emailReal, substitute: emailSub });
}
const usernames = [];
const userReal = $('#idUserReal').value.trim();
const userSub = $('#idUserSub').value.trim();
if (userReal && userSub) {
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();
if (phoneReal && phoneSub) {
phones.push({ real: phoneReal, substitute: phoneSub });
}
const profileData = {
names,
emails,
usernames,
hostnames,
phones,
names: readFieldEntries('names'),
emails: readFieldEntries('emails'),
usernames: readFieldEntries('usernames'),
hostnames: readFieldEntries('hostnames'),
phones: readFieldEntries('phones'),
catchAllEmail: $('#idCatchAllEmail').value.trim(),
emailDomains: [],
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },