fix: mask credentials on screen, not on paste - #39
Merged
Merged
Conversation
Claude Code resumes its task by itself when a usage limit resets, but only while its session is alive, which makes leaving termit open the thing that gets work done overnight. A sleeping Mac freezes every process, so the reset passes and nothing happens -- and none of the assertions already on this machine (coreaudiod, sharingd, powerd) hold PreventSystemSleep, so the default state is not enough. `caffeinate -is termit` covers it, and releases both assertions when termit exits. Verified: PreventSystemSleep goes 0 -> 1 while it runs and back to 0 after. The two limits are stated, because both are easy to trip over: -s applies only on AC power, per its man page, and neither flag stops a laptop sleeping when the lid closes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The paste-time redaction shipped in #34/#35 solved the wrong problem. The requirement is that a credential still works and simply is not visible, not that it never reaches the program. Redacting the paste turned `export AWS_SECRET_ACCESS_KEY=wJalr...` into `export AWS_SECRET_ACCESS_KEY=[redacted]`, so the shell assigned the literal string and the failure surfaced much later, in an AWS call far from its cause. Masking now happens while drawing. The grid keeps the real text and the pty gets the real bytes, so a pasted export works exactly as typed and ⌘C copies the real value; what disappears is the credential being visible in a screenshot, a screen share, or scrollback an hour later. Redactor::spans returns byte ranges, masked_cells maps them to columns, and the draw loop substitutes a bullet per cell so column alignment -- and any full-screen UI drawn on top of it -- does not shift. The byte-to-column mapping is the part that can silently go wrong, so a test masks a line with double-width characters ahead of the secret and asserts the bullets land on the key and nowhere else. Costs 0.041ms median for a full 47x163 screen with a credential every eighth line, against roughly 1ms to build a frame. An ignored test re-measures it. `[paste]` becomes `[screen]`, since the table now describes what is drawn. ⌥⌘V and Action::PasteRaw are gone: with paste left alone there is nothing to escape from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The paste-time redaction in #34/#35 solved the wrong problem. Thank you for catching it — your suspicion was exactly right, and it reproduces:
The shell assigns the literal string, and the failure surfaces much later in an AWS call, far from its cause.
The requirement, corrected
[redacted]— broken⌘CWhat needed protecting was the credential being visible — a screenshot, a screen share, someone walking past, scrollback an hour later — not the credential reaching the program.
How
Redactor::spansreturns byte ranges;masked_cellsbuilds each visible row, maps those ranges to columns, and the draw loop substitutes one bullet per cell. The grid keeps the real text, so⌘C, the program's own repaint, and the agent-state detection are all untouched.One bullet per cell keeps the column count identical, so a full-screen UI drawn on top does not shift.
The part that can silently go wrong
Byte offsets and columns diverge as soon as a double-width character appears. A test masks
鍵は AKIAIOSFODNN7EXAMPLE ですand asserts the bullets land on the key, that鍵はandですsurvive, and that there are exactly 20 of them. It caught a real bug in the test helper on the first run — the logic itself was right.Cost
0.041 ms median (p90 0.043) for a full 47×163 screen with a credential on every eighth line, against roughly 1 ms to build a frame — about 4%. An
#[ignore]d test re-measures it.Also
[paste]→[screen]: the table now describes what is drawn. Never shipped in a release, so nothing to migrate.⌥⌘VandAction::PasteRaware gone. With paste left alone there is nothing to escape from.docs/references/paste.mdrecords the correction and why the boundary moved, alongside the earlier iTerm2 research and Claude Code'sredactForDisplay(which uses only high-confidence rules for exactly this reason — on a screen a human reads, a false positive is the bigger harm).Test
237 pass, 2 ignored. clippy
-D warningsandfmt --checkclean.🤖 Generated with Claude Code