Writing: scrub passwords, codes, cards and tokens out of Save my writing day files - #1905
Merged
Merged
Conversation
…ing day files Secure input and the password-manager list miss a lot of password typing: sudo/ssh prompts in terminals, read -s, show-password web fields, OTP and PIN boxes, card forms. Every entry now goes through WritingSecretScrubber before it reaches a day file, with each app's previous entry tail as context (a terminal password usually starts the entry after its sudo line). Box fragments no longer count toward the scrap word limit, so a card or code typed over several boxes stays one entry. An entry that was only a secret isn't saved. Day files already on disk are rescrubbed once per rules version when Writing starts, one file at a time on the recorder's queue, and only if the file hasn't changed since it was read. SecretRules gains two opt-out flags (generic tokens, loose card scan), default on; the day-file scrubber runs stricter versions because the loose ones took paths, SCREAMING_SNAKE names and UUIDs. Measured with scripts/dev/measure-writing-scrubber.sh: 73/79 labelled secrets caught (the 6 misses are listed), 3 changed entries in ~62k entries of repo docs, code, scripts and commit messages, against 17.7% recall and 2,296 changed entries for SecretRules alone.
…1afa # Conflicts: # docs/writing-port-ledger.md
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.
Why
Save my writing day files (
<capture library>/writing/Writing_YYYY-MM-dd.md) were saving passwords verbatim wherever macOS secure input is off: sudo and ssh prompts in Terminal, iTerm2, Warp, Ghostty and VS Code,read -s, web fields with "show password" on, OTP and PIN boxes, card forms. The only guards were the secure-input flag and a bundle-ID list of password managers. Nothing ranSecretRuleson day files, and the MCP/CLI tools hand those files to agents.What changes
WritingSecretScrubber(new,Sources/TranscriptedWriting/Core/Text/). Pure, Foundation-only, local, deterministic. It works in three layers over one entry (one line per keyboard segment):sudo,su,ssh,passwd,mysql -p,psql,read -s,ssh-keygen,gpg,docker login,git push,ansible --ask-become-pass, …) makes the next 1–3 single-token lines passwords. It stops at the first line that looks like a command. A passphrase prompt's first answer may contain spaces.482 913, digits split over boxes (one per line). A 4–5 digit line counts only after a line that mentions a code or PIN. Recovery-code lists under "backup codes" also count.Summer2024) count as passwords in browsers (where show-password fields are) and terminals, but not versioned tools likepython3. In chat, mail and notes they're usually names (Python3,macOS26), so there they count only after apassword:line or with a common password stem likehunterorqwerty.SecretRulesfor the structured shapes (IBAN, SSN, AWS/OpenAI/GitHub keys, JWT, PEM). Emails and phone numbers are kept, because this is your own writing.password: …, "the wifi password is …",PIN 4821,verification code 482913,cvc 123,exp 04/28SOME_TOKEN=…and config keys likegithub_token: …--password/--tokenflags,mysql -pX,sshpass -p,redis-cli -a,curl -u user:pass,Bearer …user:pass@)Each redaction becomes
⟨redacted:<kind>⟩, the same token shapeSecretRulesuses.Where it runs:
WritingEntryComposer.closeOpen()scrubs every entry before the recorder sees it. The composer keeps each app's last 6 raw lines in memory only as context. Return breaks the segment in a terminal, sosudo apt updatecloses as its own entry and the password starts the next one. Context is per app and history (a Slack reply in between doesn't lose it) and lasts 5 minutes, as long as sudo waits for a password.Words:,Characters:and the heading title come from the scrubbed text, since the title is the first 7 words and could otherwise carry the password.WritingDayFileRescrubberruns once perWritingSecretScrubber.rulesVersionwhen Writing starts (WritingDayFileWriter.start(), which never runs in automated launches).Writing_*.mddirectly inside a folder namedwriting, and it writes atomically with owner-only permissions through the same path appends use.SecretRulesgainsscrubGenericTokensandscrubCardNumbersinScrubConfig. Both default to on, so prompt context and Screen Memory behave exactly as before. The day-file scrubber turns them off and runs stricter versions. The loose generic rule takes long paths (Sources/TranscriptedCore/AudioTests/…) and SCREAMING_SNAKE names, and the loose card scan takes UUIDs and entry IDs whose digit groups happen to pass Luhn. The measurement below shows how much that matters for day files. The deviation is recorded indocs/writing-port-ledger.md.Why not a model
A small local classifier would need labelled typed-password data we don't have, and it would be much harder to measure or reason about than rules. Most of the signal is context a character model can't see anyway: which app this is, the line before, whether a
sudojust ran. What rules can't catch is letters-only passwords with no context, and a classifier would struggle to tellsunshinefrom a word too. If we want one later, it fits as one more line rule behind the same API.Measured false positives and false negatives
bash scripts/dev/measure-writing-scrubber.sh: a labelled corpus (Tests/Fixtures/writing-secret-corpus.json, 70 secret cases and 28 ordinary texts), plus a sweep over text that contains no secrets: this repo's docs, code, scripts and 3,000 commit messages, cut into entry-sized chunks. The last column is the option this replaces, runningSecretRulesas the prompt path does.Missed:
Ordinary texts: 34, changed: 2 (5.9%)
Repo sweep (text that has no secrets: every redaction is a false positive unless noted)
Known misses (6 in the corpus), on purpose:
sunshinein a web field)sudothatbrewasked for with nosudotypedAlso missed:
Tigers2024alone in chat or notes with no label before it, a card whose boxes are more than a minute apart (the composer splits the entry), and alphanumeric codes over boxes (B7Xthen9QK). Catching these would mean redacting ordinary one-word replies, years and names.Known false positives, on purpose: a standalone
x86_64,Room4B-style tokens with letters and digits woven together, and a 4+ letter alias typed right aftergit push. The sweep's 3 hits are the Sparkle public key (base64, twice; indistinguishable from a secret) andFEE005+MEE007+MEE008. The Visa test card inSecretRules' doc comment is excluded from the sweep along with the two rule files, which are catalogues of example secrets.Your real day files (counts only, no text read): 2 files, 107 sections, 0 redactions, 0 removals. So nothing in your current writing would change, and nothing secret seems to have been typed yet (no terminal entries).
Tests
WritingSecretScrubberTests: promises 1–7, table-driven. It was written by a separate agent from a one-page spec, without reading the implementation.WritingEntryComposerTests(13 added): cross-entry sudo context, context expiry, context surviving another app, only-redaction entries dropped, counts. Also, through the real composer: a card typed over four boxes with expiry and CVC, a 6-box OTP, and a sudo password arriving one character per segment.WritingDayFileRescrubberTests: untouched clean files, byte-exact neighbours, section removal, per-app context, idempotence, and onlyWriting_*.mdrewritten with owner-only permissions.WritingSecretCorpusTests: every labelled secret not marked a known miss is gone, and every ordinary text not marked a known false positive comes back byte for byte.WritingSecretScrubberRegressionTests: the independent review's inputs, meaning ordinary sentences the first version redacted and labelled secrets it let through.Proved they can fail: eight deliberate breaks, each of which went red:
Two more mutations stayed green because they hit dead code, which I deleted.
Checks
bash check.shran on the branch before merging currentmain. Every step passed exceptrun-tests.sh: 19,777 of 19,790 passed, and the only failures are inMeetingPromptDetectorTests, which also fails on a cleanorigin/mainon this Mac, with 2–13 failures depending on the run. The steps:build.shbuild-deps --forcerun-integration-smokeswift testAfter merging current
main(the only conflict was a Markdown one in the port ledger, keeping both bullets), I re-ran:build-deps --forceandbuild.sh --no-open: passswift test: 880 tests passrun-tests.sh: 19,924 of 19,932 pass, and the 8 failures are the same flakyMeetingPromptDetectorTestsToken-shaped test strings are
\u-escaped in the fixture or concatenated in Swift, so the nightly scanner and GitHub secret scanning stay quiet.Not covered here
sudo -k; sudo -vwith Save my writing on, then checking the day file.Review
An independent review ran against the full diff and came back SHIP WITH FIXES. Its main catch was real: split boxes still reached disk through the composer, because my first tests fed the scrubber pre-joined strings the composer never produces. It also found false positives the one-time rescrub would have made permanent, a single-slot terminal context, the rescrub holding the recorder queue for a whole folder with a race after teardown, and a set of labelled-value leaks. All of those are fixed here with tests. Not fixed:
passwdanswers were dropped can take one more line. It errs toward redacting and only matters at a rules bump.4\n8\n2then9\n1\n3) aren't rejoined by the rescrub. That only matters for files written before this change.A second review round checked the fixes. It confirmed the composer fix through the real composer with more box layouts (Amex 4-6-5, split sudo, text around the boxes). It also caught one regression I'd introduced:
PIN 4821followed by a new line or more words leaked again, because the "must end the clause" lookahead only matched at the end of the whole text. That's fixed (6–8 digits always count; 4–5 digits are skipped only before a count noun like "lines"). Also fixed in that round: word+number passwords in terminals and after apassword:line, multi-digit OTP boxes, month and year boxes after a card, theFirst pass: v2/basic Real-Time/$db_passfalse positives, and the ledger notes. Final verdict after round two: SHIP WITH FIXES, with those fixes applied here.🤖 Generated with Claude Code