Redact PII — Privacy tab & the dialogs its flows imply.
Off by default. On the Mac the detection stack is delivered on demand
(Apple-Hosted Background Assets) the first time you enable it — so enabling shows a
brief one-time download, then it's a plain setting. Failures surface through the
normal project-sidebar status line, not a bespoke dialog. Full spec:
docs/design-redact-pii.md.
1 · Settings › Privacy (new tab)
Same 660-pt window and in-cell help-subtitle idiom as the Appearance / LLM / Transcription tabs. New tab item: Privacy, SF Symbol hand.raised.
🖌️Appearance
🧠AI
〰️Transcription
✋Privacy
Redact personal information
Automatically remove names, emails, phone numbers, and other personal
details from transcripts before they are analysed. Redaction imperfect
– do check your transcripts. Applies to the next analysis, onward.
How redaction works →
◐ shown only when the app language is not English —
Detection is best for English. Redaction in other languages less thorough.
Why one toggle only.pii_llm_pass and pii_custom_names
exist in config.py but are inert (they emit runtime warnings and do nothing),
so they are deliberately not surfaced. pii_score_threshold stays a tuned
default — no threshold UI, per house rule.
1b · Turning it on — the one-time model download
The detection model is 425 MB and is not in the app bundle — it arrives on demand the
first time the toggle is switched on, via the managed Background Assets API —
AssetPackManager.ensureLocalAvailabilityOfAssetPack plus a
ManagedDownloaderExtension of a few lines. That API is macOS 26+, which is why
the feature is gated at macOS 26 (state F) rather than the app's 15.0 floor being raised:
gating buys one code path instead of a 15.0 fallback beside a 26 fast path. Only weights download — the detection code ships
inside the reviewed binary, which is what keeps this on the clean side of App Store §2.5.2.
The size is disclosed before the flick, not after it.
A · Off — the default, and the first thing every user sees
Redact personal information
Automatically remove names, emails, phone numbers, and other personal details from transcripts before they are analysed. Redaction is imperfect — do check your transcripts. Applies to the next analysis, onward. Turning this on downloads a 425 MB language model, once.How redaction works →
B · Downloading — the switch stays on
Redact personal information
Redaction starts with your next analysis.
Downloading language model…Cancel
182 of 425 MB
Why the switch goes on immediately rather than after the download. Per decision D2 redaction
applies to the next analysis, onward — so the switch states an intention about future runs, and the
download has until the next run to finish. A switch that snapped back, or sat disabled behind a spinner,
would be worse and would also be wrong: nothing about the setting is untrue while the bytes are still
arriving. The system owns the transfer — resume, background, storage accounting — so quitting the app
does not lose it.
C · Paused — offline mid-download. Not an error, so not styled as one
Redact personal information
Redaction starts with your next analysis.
Paused — waiting for a network connection.Cancel
182 of 425 MB
D · Failed — the switch returns to off, because leaving it on would be a lie
Redact personal information
Automatically remove names, emails, phone numbers, and other personal details from transcripts before they are analysed. How redaction works →
Couldn’t download the language model. Check your connection and try again.Try Again
E · On and ready — a plain setting, no residue of the download
Redact personal information
Applies to the next analysis. Existing reports are unchanged. The model stays on your Mac. macOS may reclaim the space if the disk runs low, and it will be fetched again next time it is needed.How redaction works →
Turning it back off does not delete anything, and we offer no “remove model” button. Purging is the
system’s call: Background Assets accounts the storage to the app and reclaims it under pressure — exactly
what a Mac user already expects from on-demand dictation and translation languages. Our own delete button
would duplicate a system affordance and invite the user to manage storage we do not own.
What a run does if it starts before the download finishes. Open decision, and the only one this
screen does not settle. Two candidates: wait (the run blocks on the pack, showing the existing
progress subtitle) or fail cleanly (the normal .failed row with
MISSING_DEP, Retry). Failing is nearly free — §2 below already builds that path — but waiting
is kinder and matches “the appliance copes”. Decide before the Swift lands, not after.
F · Below macOS 26 — visible and disabled, and honest about why
Redact personal information
Requires macOS 26 Tahoe or later.
Visible and disabled, not hidden. A researcher who has read that Bristlenose can redact
needs to find out why they cannot, and a control that is simply absent answers nothing —
they conclude the feature was dropped, or that they are looking in the wrong place. The row states
the requirement in the position where the setting will appear once they upgrade.
Why gate the feature rather than raise the floor. The managed Background Assets API is
macOS 26+, and it is the one whose downloader extension is a few lines rather than a
hand-written one. Raising the app's floor to 26 would cost every user on 15–15.x the whole product
to give some of them redaction; gating costs only the feature, only to them, and the gate deletes
itself when the floor moves.
2 · Failure surfaces the normal way — no new UI
Turning the toggle on is just a setting; re-analysing is the user's existing choice, made the existing way. If a run's redaction fails, it uses the standard .failed project-row subtitle — nothing bespoke.
Projects
Coffee Machine Study
12 sessions · 2 Jul
Ikea Kitchen Study
✕ Redact PII — language model missing
Onboarding Diaries
8 sessions · 28 Jun
This is the existing failure family from ProjectRow.swift — red
MessageKind.error glyph · one-line summary · the whole subtitle is a
click target opening the normal diagnostic popover for detail. Retry is
the standard right-click action. The title-bar pill carries the dominant category +
count exactly as for any other stage failure.
Summary text is chosen by the typed Cause: missing bundled model →
MISSING_DEP; CLI download failure → NETWORK. Same
precedence chain, same tooltip, same Retry. There is nothing here to design — only
wiring to add.
⚠ For this to work, PII must be wired into that mechanism — today it isn't.
The stage-7 block in pipeline.py has no try/except and never calls
mark_stage_failed, unlike transcribe / topics / quotes / clusters. A
remove_pii() raise today propagates as an unclassified crash that can't
reach the .failed subtitle at all. The fix is to wrap the stage and route to
CauseCategoryEnum.MISSING_DEP / NETWORK like every other stage —
no new UI, just the missing wiring. See the apparatus review below.
3 · CLI equivalent (pip install)
The one surface where a fetch does happen — first --redact-pii run. Single inline status line (under the 50 MB banner cutoff), then cached.
$ bristlenose run interviews/ --redact-pii✓ Merged transcript [2.1s]
Downloading language model… ✓[6s]✓ Redacted PII (47 entities) [3.4s]✓ Topic segmentation [11s]# on failure — same fail-stop, classified:✗ Redact PII — language model not installed
Run python -m spacy download en_core_web_sm or reinstall.
4 · Apparatus review — what PII has vs. lacks
You were right to suspect gaps: PII is wired for the happy path but is missing the failure & progress machinery every other heavy stage has.
✅ Manifest tracking have
mark_stage_running / _complete(STAGE_PII_REMOVAL) — resume & provenance know it ran.
✅ Config provenance have
Topic stage hashes pii_enabled, so toggling correctly invalidates the downstream cache.
Stage-7 block has no try/except and no mark_stage_failed. A raise becomes an unclassified crash that can't reach the .failed status line.
🔴 Model-name mismatch Phase 0
Code loads en_core_web_sm; Presidio default + the bundle ship en_core_web_lg. In the frozen sidecar the sm probe → FrozenSidecarError. Works in dev only because both are installed. Reconcile first.
🟡 No live-progress event decide
PII is deliberately folded into its neighbours (RunProgressSubtitle.swift) and absent from timing.py stages — fine for the ring, but a stall/failure has no labelled surface.
🟡 No resume/cache guard minor
Unlike topics/quotes, PII isn’t wrapped in _is_stage_verified — it re-redacts on every resume. Cheap, but inconsistent.
Sequence. Phase 0 (reconcile the model name across stage guard · Presidio NlpEngine ·
spec, prove it in a real bundle) → wrap stage 7 in try/except routing to typed
Cause + mark_stage_failed (this is what makes the .failed status line reachable) →
native Privacy tab + piiEnabled UserDefaults key + one env line in
BristlenoseShared.swift → copy & i18n. The toggle is the small part; the
failure apparatus is the real work.