Theme:

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.

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.
✅ Doctor check & outputs have
check_pii() reports health; transcripts-cooked/ + pii_summary.txt (re-identification key, kept hidden) are written.
🔴 No failure classification build
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.