Skip to content

feat: opt-in transcript auto-delete with retention window#77

Closed
arfrank wants to merge 197 commits into
matthartman:mainfrom
arfrank:feature/transcript-expiry
Closed

feat: opt-in transcript auto-delete with retention window#77
arfrank wants to merge 197 commits into
matthartman:mainfrom
arfrank:feature/transcript-expiry

Conversation

@arfrank

@arfrank arfrank commented Apr 16, 2026

Copy link
Copy Markdown

What

Adds a privacy-focused, opt-in transcript retention feature to Ghost Pepper:

  • A global retention window in Settings → Privacy (Never / 7 / 14 / 30 / 60 / 90 / 180 / 365 days).
  • A per-meeting "auto-delete" checkbox on the meeting window (shows only when retention is set). Stored as auto_delete: true in the markdown file's YAML frontmatter.
  • A scope picker: Flagged meetings only (default) or All meetings. In flagged mode only meetings the user has opted in get swept; in global mode every meeting older than the window gets swept.
  • A background sweeper that runs at launch, every 6h thereafter, and immediately when any related setting changes. It strips only the ## Transcript section — notes, summary, and trailing sections (e.g. Granola ## Chapters) are preserved. A parseable marker <!-- ghost-pepper-transcript-expired: YYYY-MM-DD --> is stamped so the operation is idempotent.
  • The meeting window shows a "Transcript auto-deleted" banner in place of the transcript section for expired files.

Why opt-in by default

An always-on global auto-delete is an easy footgun — users with important meetings could lose them silently. The opt-in flag forces an explicit choice per meeting. The global scope toggle is available for users who know they want it that way.

Privacy — honest about the caveat

This is not secure erasure. String.write(atomically:) does an atomic rename-replace, so the previous file's blocks linger in APFS free space until reused, and Time Machine snapshots may retain earlier versions. FileVault encrypts these blocks at rest but does not prevent recovery once the disk is unlocked.

The UI copy, the sweeper doc comment, and PRIVACY_AUDIT.md all say this explicitly. An earlier draft claimed FileVault provided secure erasure; that claim has been removed.

How it was built

  • TDD: failing XCTest cases written first, then implementation.
  • Two rounds of Codex review, findings addressed before merging:
    • Round 1 (6 findings): open-tab/sweep race (a swept file could be resurrected by a stale in-memory tab), markdown-header fragility (trailing whitespace on ## Transcript), false privacy claim, silent directory-scan failures, sticky expired flag in renderer, DateFormatter thread-safety.
    • Round 2 (3 findings after opt-in + scope toggle): saveActiveTab still bypassed the new on-disk guard, per-folder scan failures were still swallowed, scope-picker description copy could leak stale state when retention was set to Never.

Files

New

  • `GhostPepper/Meeting/TranscriptExpirySweeper.swift` — the sweeper (227 lines).
  • `GhostPepperTests/TranscriptExpirySweeperTests.swift` — 21 tests (age boundary, CRLF, BOM, trailing-whitespace headers, flagged-only vs global, silent-scan vs surfaced-scan, nested sections, idempotency, never-setting).
  • `GhostPepperTests/MeetingMarkdownWriterTests.swift` — 12 tests (frontmatter round-trip, marker parse/emit, race-guard on `write`, segment preservation when the expired flag is spuriously set).

Modified

  • `GhostPepper/Meeting/MeetingMarkdownWriter.swift` — frontmatter emit/parse, expiry marker helpers, pre-write on-disk marker guard.
  • `GhostPepper/Meeting/MeetingTranscript.swift` — `transcriptExpiredDate` + `autoDeleteFlagged` properties.
  • `GhostPepper/AppState.swift` — `runTranscriptExpirySweep()` + 6-hour timer + two new `@AppStorage` settings.
  • `GhostPepper/UI/SettingsWindow.swift` — Privacy card with retention picker, scope picker, honest caveat copy.
  • `GhostPepper/UI/MeetingTranscriptWindow.swift` — expired banner, per-meeting auto-delete checkbox, route `saveActiveTab` through the guarded write path.
  • `PRIVACY_AUDIT.md` — row 8 added with accurate caveats.

Tests

33 new unit tests, all green. Full app build clean. Touching `@MainActor` on AppState; sweep I/O runs on a detached Task and hops back to main for logging.

```
Executed 33 tests, with 0 failures in 0.07 seconds
** BUILD SUCCEEDED **
```

Test plan

  • Unit tests pass (see above)
  • Manual: set retention → flag a meeting → wait/toggle → transcript strips, banner appears
  • Manual: unset retention ("Never") → per-meeting checkbox disappears; Settings description reads "No transcripts are auto-deleted"
  • Manual: switch to global mode → per-meeting checkbox disappears; all old meetings strip on next sweep
  • Manual: open a meeting in a tab, change retention so it qualifies, then edit notes — verify the file retains its expiry (no resurrection bug)
  • Signed/notarized release build via `./scripts/build-dmg.sh` (requires Developer ID cert)

🤖 Generated with Claude Code

matthartman and others added 30 commits March 23, 2026 14:53
Models section shows each model with loaded/not loaded status.
Taller settings window (580px) to fit all sections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows when any model isn't loaded. Downloads WhisperKit and/or
cleanup models directly from Settings — no need to re-run onboarding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LLM.swift's bundled llama.cpp doesn't support Qwen3/3.5 architecture.
Reverted to Qwen 2.5 1.5B + 3B which work reliably.
Will upgrade when LLM.swift updates its llama.cpp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Info.plist was still at v1.1 so Sparkle thought the "update"
was the same version. Now properly at v1.3 build 4.
Fixes #2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Improve post-paste learning via Accessibility
Picker in Settings under Input section. Switching models triggers
re-download and reload. Default remains small.en for accuracy.
tiny.en is ~75 MB and much faster for shorter recordings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oggle)

Matches the original Ghost Pepper behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Input Monitoring prompt doesn't reliably show the system dialog
for debug-signed apps. Now attempts to start the hotkey monitor
even without it — Accessibility alone is sufficient for Control key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed codesign verification failure reported in #4.
Now verifies signature after extracting from DMG before release.

Also includes: speech model picker, default Control shortcuts,
non-blocking Input Monitoring check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS kills the app after Screen Recording is granted but doesn't
relaunch it. Now spawns a background process that reopens the app
after 3 seconds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace no-audio modal with status pill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses #5 — users should know about local transcript log
and auto-launch behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
File-based logging was replaced by in-memory DebugLogStore.
Nothing is written to disk. Updated disclosure accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No longer blocks Continue button. Shown as "(optional)" with
a bordered (not prominent) Enable button. Users can skip it
and enable later in Settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches WhisperFlow and other dictation tools.
Toggle is Right Command + Right Option + Space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supports Spanish and 90+ other languages. Same size as small.en.
Users can tweak the cleanup prompt for their language's filler words.
Addresses #6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
matthartman and others added 28 commits April 10, 2026 16:44
…chat threading

Context Bundler (renamed from Pepper Chat):
- Trello integration via OAuth-style authorize flow (user's own API key)
- "Add to Trello" creates cards with voice-parsed title, board, list matching
- TrelloCommandParser extracts structured data from natural speech
- Boards/lists fetched and cached, default list picker in Settings
- Button only shows when Trello is configured

Multi-window context capture:
- Captures screenshot + OCR at recording START (not end) so it gets the
  right window, not the Ghost Pepper bubble
- Continuously monitors for window switches during recording (every 1s)
- Each new window gets its own screenshot + OCR, shown as thumbnails
- All contexts combined into one bundle for Zo/Trello/clipboard

Chat threading with Zo:
- Conversation accumulates automatically — prior Q&As sent as context
- Scrollable chat history in the bubble
- "Clear context" wipes thread, "Save as note" exports to meetings
- "Chatting with Zo..." with larger logo during processing

UI improvements:
- Keyboard navigation: arrow keys, Enter, Escape (via local event monitor)
- Close button on all states
- Full command text (no truncation)
- Sound effects on hotkey press/release

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Native Granola importer: imports from local cache + fetches full notes
  & transcripts via Granola API with proper pagination (cursor/hasMore)
- Reads summary_markdown from API (not just "summary" field)
- Granola API key persisted to UserDefaults for re-sync
- Sidebar search to filter meetings by name
- Granola sync icon in sidebar header (shows when cache detected)
- Fixed markdown parser to read Summary, Chapters, and YAML frontmatter
- Fixed Summary tab showing "No transcript" even when summary exists
- Fallback transcript parser for Granola format (plain text, no timestamps)
- Writer now preserves Summary section on save
- Improved summary prompt: topic-based headings instead of generic format,
  incorporates user notes as emphasis guide
- PRIVACY_AUDIT.md: audit prompt + results for verifying 100% local default
- README: privacy audit table with verify-it-yourself instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…media resume launching Apple Music

- Set input device directly on audio unit via kAudioOutputUnitProperty_CurrentDevice
  instead of changing the system-wide default input device (#60)
- Added manual mono downmix for aggregate devices with >2 channels,
  since AVAudioConverter can't handle non-standard channel counts
- Persist selected device to UserDefaults instead of system default
- Removed auto-resume (kMRPlay) from MediaPlaybackController — was
  unconditionally sending play command on recording stop, which launched
  Apple Music or resumed YouTube even when nothing was playing (#56, #57)

Closes #60, closes #56, closes #57

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Centered hero with 128px logo, tagline, prominent download button
- 2x2 feature grid (speech-to-text, meetings, cleanup, privacy)
- Testimonials section with Ryan Hoover quote + Dave Morin, Nick Saltarelli, Michael Mignano tweets as screenshot images
- Star on GitHub CTA
- Streamlined layout: download first, details later

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…heading

- SVG diagram: Your Voice → Your Mac (on device) → Your Text
- Dynamic shields.io badges: GitHub stars, MIT license, 100% Local, 50+ Languages
- Renamed bottom section to "Why Ghost Pepper?"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README reverted to standard project docs format
- site/index.html is the Product Hunt landing page with hero, testimonials, privacy flow diagram, embedded tweets, and live star count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Logo and favicon now use raw.githubusercontent.com URLs
- Removed Your Voice → Your Mac → Your Text diagram
- All image paths are absolute for GitHub Pages compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…targeted

- Set isTranscribing=true before async Task to prevent bubble auto-dismiss
  during the gap between recording stop and transcription start
- Show context review even when transcription is empty (screenshots still useful)
- Set targetDeviceID on context bundler's AudioRecorder (was missing after
  the aggregate device fix changed away from system default)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolved conflicts:
- AppState.swift: kept both context capture cleanup and hotkey binding update
- MenuBarView.swift: kept pepperChatEnabled guard with our "Context Bundler" rename

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix Zoom meeting transcript lifecycle and window chrome
Per-meeting "auto-delete" flag (stored as `auto_delete: true` in YAML
frontmatter) plus a global retention window in Settings. The sweeper
runs at launch and every 6 hours, stripping the `## Transcript` section
from flagged files whose folder date is older than the window; notes
and summary are preserved. Unflagged meetings are never touched.

The meeting window shows an "auto-delete" checkbox only when the
retention window is set (i.e. not "Never"). The write path re-checks
the on-disk expiry marker before saving so an open tab can't resurrect
a swept transcript.

Privacy copy is explicit that this is not secure erasure: APFS
copy-on-write and Time Machine snapshots may retain previously
written bytes; FileVault encrypts them at rest but does not prevent
recovery once the disk is unlocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings Privacy card now has a second picker ("Applies to: Flagged
meetings only / All meetings") that shows up once retention is set.
The sweeper takes an `onlyFlagged` parameter (default true preserves
opt-in behavior); when set to false, every meeting past the retention
window is swept regardless of flag. The per-meeting auto-delete
checkbox hides in global mode since the flag is ignored there.

Also wires sweep-on-toggle so flagging a meeting triggers an immediate
sweep instead of waiting for the next 6h timer tick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- P1: saveActiveTab was bypassing the on-disk expiry guard by calling
  renderMarkdown + markdown.write directly. Now routes through
  MeetingMarkdownWriter.write(transcript:to:existingFileURL:) so any
  tab interaction (notes edit, rename, flag toggle, switch) honors
  a swept file's marker instead of clobbering it.
- P2: per-folder contentsOfDirectory failures are no longer swallowed
  by try?. They now record an error like the base-directory path does.
- P2: Privacy card description is now coherent with retention state.
  When retention is "Never" the copy says "no transcripts are auto-
  deleted" instead of leaking whatever value the scope toggle held.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…expiry

# Conflicts:
#	GhostPepper.xcodeproj/project.pbxproj
#	GhostPepper/UI/MeetingTranscriptWindow.swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants