Put AI in the meeting. Keep the names out of the model.
Backchannel is an open-source meeting assistant you run yourself. With its PII Shield on, a client's name, company, email address or phone number becomes a per-session token the moment it is written, and every model, on this machine or in a cloud, reads the token. This page is the long version of that claim: the mechanism, the parts that are enforced rather than advised, the limits, and the file to open when you want to check it yourself.
Off by default · detection runs on this machine · MIT licensed · free desktop downloads, no sign-in needed
What a model receives, and what you do
Paste a sentence into the Privacy tab and it shows the sentence a model would receive, with a legend for every token and the layer that found it. Nothing typed there is stored. The same substitution runs on every transcript line, directive, document excerpt, session name and speaker name in a real session.
Nine categories, eight on by default
People, organizations, places, email addresses, phone numbers, national identifiers, payment-card numbers, IP addresses and street addresses. Card numbers must pass a Luhn check, so a long invoice reference is not mistaken for a card. Places are off by default, because “we are expanding into Texas” is analysis, not identity; turn the category on if a city is sensitive in your work.
Numbered per session, unlinked across sessions
Tokens read [CATEGORY_n]. Owen is
[PERSON_1] on every line of one call, so
the analyst can follow who promised what to whom. In the next session
he is nobody in particular. The vault stores a keyed hash of each
value rather than the value, so nothing in the database links a
person from one session to another.
The shield in ordinary use
A protected session says so in its header: a count of shielded values beside the export button. The outcome line, the commitments and the open loops on this screen were all produced by models that never saw the names they are shown with. The names were put back on the way to the screen, and the badge is the only sign that anything happened.
Tokenized at the door, not filtered at the exit
The usual design for this feature is an outbound filter. Text is stored as spoken, and something scrubs it just before the model call. A reviewer will recognise the failure mode: every new prompt builder has to remember to call the filter, and the one that forgets is the leak.
Backchannel encodes at ingress instead. Every path that writes human
text passes through protect_text (or
protect_name, for speakers) in
backend/app/services/pii/shield.py before the row
exists. The analyst, the objection handler, the synthesizer,
strategic signals, the briefing, chat and Ask all read stored text,
and stored text is already tokens. No prompt builder was changed to
add the shield, and none can bypass it, because there is nothing to
bypass. The plain value never reached the database.
Every write path, and the file where the encode call sits. Paths are under backend/app/.
| Text arrives as | Tokenized in |
|---|---|
| Live transcript segments, as each diarized segment is transcribed | ws/audio_handler.py |
| Transcript imports (.txt, .md, .docx), audio imports and re-transcription | routers/imports.py |
| Manual transcript entries | routers/transcripts.py |
| Directives, typed before the call over REST or mid-call over the WebSocket | routers/directives.py, ws/audio_messages.py |
| Session name, notes and meeting context | routers/sessions.py |
| Speaker names, which are a person's name by definition | routers/speakers.py via protect_name |
| The document excerpt, extracted on this machine | routers/documents.py |
| The question you type into Ask during a call | routers/ask.py |
| Your turns in Chat | routers/chat.py |
Found on this machine, in four layers
No layer makes a network call. Each catches what the one before it cannot, and the roster is what makes a name caught once stay caught.
Patterns, for the structured categories
Email, phone, national identifier, card, IP and street address are
shapes, and shapes are what regular expressions are for
(services/pii/recognizers.py). A card candidate must
pass Luhn, so a thirteen-to-nineteen-digit reference number stays a
reference number.
The roster, matched as whole words
The session's speakers, the protected-terms list you maintain for the
workspace, and every person, organization and place already in the
session's vault. A name caught once by any layer is caught on every
later line, with or without the model. Each capitalized part of a
multi-word name maps to the same token, so a later
“Delacroix” on its own still becomes
[PERSON_1]. The protected-terms list is
where you put what a model would miss: client companies, project code
names, a person who is never introduced.
Introductions
“My name is” and “this is” mark the name that follows as a person, which is how most people enter a call.
An on-device name model, optional
Xenova/bert-base-NER, the CoNLL-2003 BERT model as a
quantized ONNX file of about 110 MB. It downloads once into
DATA_DIR/pii-models/ and runs on the onnxruntime the app
already ships for speaker diarization, on the CPU. If it cannot be
fetched, the shield keeps working on the other three layers and the
Privacy tab says so rather than showing a green light.
When layers disagree, the roster wins, then patterns, then introductions, then the model. The roster knows that Brown is Bill Brown and that Cyberdyne is a company; the model only guesses a category.
Encrypted at rest, decoded only at the edge
The vault (services/pii/vault.py, table
pii_vault_entries) holds each value Fernet-encrypted
under a key derived with HKDF from the credentials master key, the
same root of trust that protects your provider API keys, through
secrets.derive_subkey. Lookup uses a keyed HMAC of the
normalized value. So the table reveals neither the values nor whether
two sessions share one; the same person in two sessions is two
unrelated rows.
Decoding is reachable from exactly three places, all of them on the way to the person at this machine:
PiiRevealMiddlewaresubstitutes tokens in every session-scoped JSON response and in the session list.RevealingWebSocketdoes the same for every live message during a call.- The exports and
/api/chat, whose session is not in the URL path, reveal explicitly throughshield.reveal_text.
Nothing under services/agents and nothing in
services/llm.py imports the reveal path. That is an
invariant you can enforce with a grep, and the verification section
below gives you the command.
Your screen is the edge
During this call the transcript shows Owen and Maya, the answered question quotes Owen's threshold, and the ask bar sends your questions to a Gemini model. Every one of those model calls carried [PERSON_1]. The names on screen were put back by the WebSocket wrapper on the last hop, and each substitution was counted.
Exports and the audit trail
Exports carry tokens unless you ask otherwise. The Export menu has a
box, Include personal data, which passes ?reveal=1;
leave it unticked and the transcript, the insights workbook and the
summary all leave with tokens in place. Every reveal, on screen or
into a file, appends a row to pii_reveal_events with the
session, the route and the number of tokens replaced, and the Privacy
tab shows the last 24 hours.
Audio is enforced, not advised
Text can be tokenized before a model reads it. Audio cannot, so the rule for audio is a lock rather than a setting.
Locked to local models while the shield is on
While the shield is on, transcription_runtime.audio_lock_reason locks audio to local models the way Privacy First does, but for audio alone. A cloud batch transcriber is coerced to a local model; a cloud live-caption gateway is paused, and the live call says “Live captions off: PII Shield” rather than going quiet. Cloud text models stay available throughout, because text is tokens by the time they see it. Uploaded documents are read on this machine and never sent as files. The Privacy tab reports each row as it stands, including a cloud gateway that is configured but paused.
Closing the quality gap: the Transcript Refiner
Local transcription is rougher than cloud transcription: thin punctuation, flat casing, a product name split in two. The optional Transcript Refiner agent, off by default, sends the tokenized transcript to any text model, local or cloud, to fix punctuation, casing, sentence boundaries and obvious mishearings. It runs every 45 seconds during the call, once more at call end, and before post-import analysis. A rewrite is kept only if it carries exactly the original tokens; a model that drops, invents or renumbers one loses that entry, and the transcriber's text stands.
Check it, instead of taking our word
Three checks, in increasing order of effort. None of them needs a network.
Read the prompts as they left
Turn on Record outbound prompts in the Privacy tab and every prompt is
appended to DATA_DIR/prompt-log/outbound.jsonl exactly
as it was sent, with the agent, the model and the session. The tab
lists the newest entries with a badge, “tokens only” or
“blocked”. The file is written raw on purpose, bypassing
the log scrubber, because a scrubbed record could not show a leak. It
never leaves the machine, and one button deletes it.
The tripwire
Independently of the log, while the shield is on every text prompt
passes an egress check (services/pii/egress.py, called
from generate_text and generate_json). A
prompt that still carries a value the vault has seen in plaintext is
refused before it is sent, and the refusal is written to the audit
trail. A gap upstream costs one model call, not one disclosure.
The grep
The decode path must not be reachable from any prompt builder. From a checkout of the repository:
# Expected output: nothing.
grep -rl "reveal_" backend/app/services/agents backend/app/services/llm.py
# The three places that do decode, all on the way to the screen:
# backend/app/services/pii/reveal_middleware.py session-scoped JSON and the session list
# backend/app/services/pii/ws.py every live WebSocket message
# backend/app/routers/artifacts.py, chat.py exports and /api/chat
What the shield does not do
A privacy page that only makes promises is not worth sending to a reviewer. These are the edges, in the product's own words.
It is off by default
Turn it on in Admin, Privacy. A fresh install protects nothing until you do.
Detection is very good, not perfect
An unusual name that is never introduced and is on no roster can be missed. That is exactly what the protected-terms list is for: add the names a model would not know.
Places are off by default, on purpose
Place names are usually analysis rather than identity. Turn the category on if a city or a site is sensitive in your work.
Earlier sessions keep what they hold
Sessions recorded before the shield was on stay as they were until you protect them. POST /api/sessions/{id}/pii/protect runs the encode path over a stored session, and the product offers it.
Tokenization is reversible by design
This is not redaction. The vault exists so your own screen can show the real names. Anyone with the data directory and the master key can decode, so protecting the master key still matters.
It governs what models read
It is not a claim about your disk, your backups, or who can log into the machine. Those are yours to secure, as with any software you run.
Privacy First decides where. The shield decides what.
Different switches, and the difference matters
Privacy First keeps everything on this machine and turns off anything that needs an outside call. The PII Shield decides what any model, local or cloud, gets to read. They compose. A configuration many teams will land on is shield on with cloud text models: frontier-model analysis quality, and no name ever leaves. A fully local team turns both on and loses nothing the shield offers.
| Question | Privacy First | PII Shield |
|---|---|---|
| What it decides | Where processing happens | What any model is allowed to read |
| Cloud text models | Turned off | Allowed; they receive tokens only |
| Transcription audio | Local models only | Local models only, enforced while the shield is on |
| Live captions | On-device captioner only | Cloud gateway paused; on-device captioner allowed |
| Uploaded documents | Read on this machine | Read on this machine, never sent as files |
| Default | Off | Off |
Questions a reviewer asks
Can I use cloud models such as Gemini or GPT with the shield on?
Yes, for text. Every agent, the briefing, chat and Ask send tokenized text, so a cloud text model receives [PERSON_1] and never the name. Audio is different: while the shield is on, transcription is held to a local model and a cloud live-caption gateway is paused, because audio cannot be tokenized.
Does any detection step call out to a service?
No. Pattern matching, roster matching, introduction patterns and the optional on-device name model all run on this machine. The model is an ONNX file of about 110 MB downloaded once into DATA_DIR/pii-models/; if it cannot be fetched, the other layers keep working and the Privacy tab says so.
Is this redaction? Can the names be recovered?
It is tokenization, which is reversible by design. The real values sit in a vault encrypted under a key derived from the credentials master key, and they are put back only on the way to your screen, or into an export when you tick Include personal data. Anyone with the data directory and the master key can decode, so protect the master key.
What happens to sessions recorded before I turned it on?
They keep their plain text until you protect them. POST /api/sessions/{id}/pii/protect runs the encode path over a stored session's transcript, insights, directives, document excerpts, session fields and speakers, and reports what changed. The product offers it for those sessions.
How do I verify that a prompt really left with tokens only?
Turn on Record outbound prompts. Every prompt is written to DATA_DIR/prompt-log/outbound.jsonl exactly as it was sent and badged tokens only or blocked in the Privacy tab. Separately, while the shield is on, a prompt still carrying a vault value is refused before it is sent.
What is the difference between Privacy First and the PII Shield?
Privacy First decides where processing happens: it keeps everything on this machine and turns off anything that needs an outside call. The PII Shield decides what any model, local or cloud, gets to read. They compose. Shield on with cloud text models gives frontier-model analysis with no name leaving the machine.
Try it on a sentence before you try it on a client
Install, open Admin, Privacy, and paste the worst sentence you can think of into the try-a-sentence box. Nothing typed there is stored, and the legend tells you which layer caught what.