Writing: keep secrets out of Personal History and the next-word predictor - #1908
Merged
Merged
Conversation
…ctor Personal History used to get every raw keyboard batch as it arrived, so a password typed at a sudo prompt in Terminal, an OTP or a card number went into the encrypted log, and a password with letters in it could be learned and served as a ghost suggestion. Now it's reached only through Save my writing's entry composer. An entry's events go to the controller once the entry closes, and only if WritingSecretScrubber redacts nothing from it, as saved or as the keyboard sent it (a secret typed and then Backspaced out is still in the events). The recorder hands cleared events to PersonalHistoryRelay, which delivers them in order in batches the controller takes. WritingHistoryIngest no longer calls the controller directly. The controller and predictor are unchanged; the Tilde deviation is in the port ledger.
21 behavior tests from the promises: a sudo password in Terminal is never stored or predicted (with a control proving the old path served it), ordinary writing is still learned, Backspaced secrets stay out, nothing is released while an entry is open, Save my writing off and delete all drop the open entry, a retried batch lands once, and long entries arrive whole and in order in batches the controller takes.
… a Backspace split costs Review follow-ups. Storage health has no Writing tab line, so a refused batch is only a local diagnostic; that's now a listed follow-up. A Backspace can leave a word alone on a line in the as-sent check and withhold an ordinary entry from learning, never the reverse.
…ry-secret-scrub # Conflicts: # docs/writing-port-ledger.md
r3dbars
marked this pull request as ready for review
September 29, 2026 04:26
r3dbars
added a commit
that referenced
this pull request
Sep 29, 2026
Resolve WritingEntryComposer against #1908: keep the Personal History gate (holdsSecret before the length guard) and this branch's rule that a secret-only entry is saved as its token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 of 8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Personal History (the encrypted log and the next-word predictor) now takes the keyboard's events only after Save my writing has cleared them of secrets. Before this,
WritingHistoryIngestsent every raw batch straight toPersonalHistoryController, guarded only by secure input and the password-manager list. So a password typed at asudoprompt in Terminal, an OTP or a card number got stored, and a password with letters in it could be learned and served as a ghost suggestion (Tr0ub4dor&3teaches the predictor "dor" after "ub").How
The day-file composer (
WritingEntryComposer) already groups events into entries and scrubs each one with per-app context lines, which is what catches a sudo password (it's the segment after the command) or a card typed over four boxes. So Personal History now rides on that:WritingSecretScrubberchanges nothing, checked twice: on the entry as saved, and on the text as the keyboard sent it. The second one matters when Backspace took a secret back out; the log would still store the deleted text.WritingDayFileRecorderhands cleared events (re-checked against the gate, like a write) to a newPersonalHistoryRelay, which delivers them to the controller in order, in valid batches.WritingHistoryIngestno longer calls the controller directly.PersonalHistoryControllerand the predictor are unchanged.Options weighed
Tr,ub,dor), and the scrubber can't judge a fragment. The log would still keep the raw text.What changes from Tilde (in
docs/writing-port-ledger.mdDeviations)Not done
Measured
bash scripts/dev/measure-writing-scrubber.sh: on the repo sweep (about 64k entry-sized chunks of docs, Swift, shell scripts and commit messages with no secrets) the scrubber changed 3 entries, so dropping a whole entry from Personal History when anything is redacted costs almost no ordinary learning. Labelled corpus recall is unchanged (92.4%, the known misses).Tests
21 behavior tests, written by a separate agent from the promises (
PersonalHistorySecretTests.swift, plus a section inWritingEntryComposerTests.swift). End to end throughWritingHistoryIngest→ recorder → relay → a realPersonalHistoryControllerover an in-memory store:sudoincom.apple.Terminalis never stored and never predicted. It starts with a control: the same keystrokes fed straight to the controller do serve "dor" after "Tr ub", so the nil check means something. Thesudo apt updateline still reaches the log.Each test was proven red by a deliberate break (12 mutations, all caught).
Review
Independent review (
writing-revieweragent;codex reviewcouldn't run, since its configured model isn't allowed on this account). Verdict: no blocking code defect. It confirmed the controller is reachable only through relay ← recorder ← composer, deletions never leak,clean == typedis exact, and ordering is serial end to end. Follow-ups taken: ledger wording on where a refused batch shows up, and what a Backspace split costs. Not taken: waiting at quit for the last entry's Personal History write. That's documented as best-effort.Things worth knowing, all in the safe direction:
Python3in a browser), which withholds that entry from learning.Checks
8e239935: app-build, build-and-test, spm-tests, checks and repo-hygiene all pass (hardware smokes skipped; no audio change).main, which retuned the scrubber and fixed the MeetingPromptDetector flake:swift teston the 10 Writing suites passes, 180 tests.bash build.sh --no-openbuilds clean, with no warnings in the changed files.check-source-pins --changed-only,check-test-shapeandcheck-known-trapspass.bash check.shon the merged head: Deterministic proof FAIL on two items outside this diff; everything else passed.build.shpassed.run-tests.sh: 19,961 of 19,961 passed.Sources/UIat 54 warnings against a baseline of 51. That comes from other merged commits (SpeakerPeopleSettingsSection, TodayViewModel and others).Sources/TranscriptedWritingstays at 4/4, and no changed file has a warning.TranscriptionTaskManagerRecoveryTests.testFutureDatedAudioCannotPinRecoveryOwnerOrBlockLaterJournalScan(TranscriptedCore) fails locally at load average 50–100. It races file mtimes against a 20 ms liveness window. CI's spm-tests passes it on this head.🤖 Generated with Claude Code