krz/keycask

Password manager: Swift core library, CLI for macOS/Linux/Windows, iOS/macOS app. cli password-manager swift

Commit ba62721654

ba6272165474d294fe74fc0966638c329e972346

parent: 84a94244b1

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-18 01:22 UTC

Clear the clipboard instead of restoring it

Layout: unified · split

Sources/keycask/Clipboard.swift +4 −5
@@ -4,7 +4,6 @@ import KeycaskCore
4enum Clipboard { 4enum Clipboard {
5 struct Handoff: Codable, Equatable { 5 struct Handoff: Codable, Equatable {
6 var secret: String 6 var secret: String
7 var previous: String
8 } 7 }
9 8
10 struct Tool: Equatable { 9 struct Tool: Equatable {
@@ -14,7 +13,7 @@ enum Clipboard {
14 13
15 static let timeoutSeconds = 45 14 static let timeoutSeconds = 45
16 15
17 static func shouldRestore(secret: String, current: String?) -> Bool { 16 static func shouldClear(secret: String, current: String?) -> Bool {
18 current == secret 17 current == secret
19 } 18 }
20 19
@@ -75,7 +74,7 @@ enum Clipboard {
75 74
76 static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws { 75 static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws {
77 _ = try requireTool() 76 _ = try requireTool()
78 let handoff = Handoff(secret: secret, previous: try read()) 77 let handoff = Handoff(secret: secret)
79 try write(secret) 78 try write(secret)
80 let process = Process() 79 let process = Process()
81 process.executableURL = Bundle.main.executableURL 80 process.executableURL = Bundle.main.executableURL
@@ -103,8 +102,8 @@ enum Clipboard {
103 } 102 }
104 Thread.sleep(forTimeInterval: TimeInterval(seconds)) 103 Thread.sleep(forTimeInterval: TimeInterval(seconds))
105 let current = try? read() 104 let current = try? read()
106 guard shouldRestore(secret: handoff.secret, current: current) else { return } 105 guard shouldClear(secret: handoff.secret, current: current) else { return }
107 try write(handoff.previous) 106 try write("")
108 } 107 }
109 108
110 private static func requireTool() throws -> Tool { 109 private static func requireTool() throws -> Tool {
Tests/KeycaskCLITests/ClipboardTests.swift +5 −5
@@ -4,14 +4,14 @@ import Testing
4@testable import keycask 4@testable import keycask
5 5
6@Suite struct ClipboardTests { 6@Suite struct ClipboardTests {
7 @Test func restoresOnlyWhenClipboardStillHoldsTheSecret() { 7 @Test func clearsOnlyWhenClipboardStillHoldsTheSecret() {
8 #expect(Clipboard.shouldRestore(secret: "s", current: "s")) 8 #expect(Clipboard.shouldClear(secret: "s", current: "s"))
9 #expect(!Clipboard.shouldRestore(secret: "s", current: "user pasted")) 9 #expect(!Clipboard.shouldClear(secret: "s", current: "user pasted"))
10 #expect(!Clipboard.shouldRestore(secret: "s", current: nil)) 10 #expect(!Clipboard.shouldClear(secret: "s", current: nil))
11 } 11 }
12 12
13 @Test func handoffRoundTrips() throws { 13 @Test func handoffRoundTrips() throws {
14 let h = Clipboard.Handoff(secret: "s3cret", previous: "old") 14 let h = Clipboard.Handoff(secret: "s3cret")
15 let data = try JSONEncoder().encode(h) 15 let data = try JSONEncoder().encode(h)
16 #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h) 16 #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h)
17 } 17 }
docs/superpowers/specs/2026-09-17-keycask-design.md +7 −6
@@ -224,11 +224,12 @@ and a Windows body where they differ.
224 Linux; `clip.exe` to write and `powershell -command Get-Clipboard` to 224 Linux; `clip.exe` to write and `powershell -command Get-Clipboard` to
225 read on Windows. No tool found is exit 1 with a message naming the 225 read on Windows. No tool found is exit 1 with a message naming the
226 tools. `clip` spawns `keycask clipboard-daemon` detached with stdout 226 tools. `clip` spawns `keycask clipboard-daemon` detached with stdout
227 and stderr to null, writes `{"secret": ..., "previous": ...}` to its 227 and stderr to null and exits without waiting. `clip` writes the
228 stdin, and exits without waiting. `clip` writes the clipboard itself, 228 clipboard itself, then spawns the daemon with `{"secret": ...}` on its
229 then spawns the daemon. The daemon sleeps 45 seconds, reads the 229 stdin. The daemon sleeps 45 seconds, reads the clipboard, and clears
230 clipboard, and if it still equals the secret restores `previous` or 230 it if it still equals the secret. It never restores earlier contents,
231 clears when `previous` is empty. 231 so a second `clip` inside the window cannot bring an earlier secret
232 back.
232 233
233## Errors 234## Errors
234 235
@@ -272,7 +273,7 @@ CLI, black box:
272 masking versus `--reveal`, `--field` raw output, the ambiguous-name 273 masking versus `--reveal`, `--field` raw output, the ambiguous-name
273 listing, `rm` without `--yes` and without a TTY, `edit` with no flags, 274 listing, `rm` without `--yes` and without a TTY, `edit` with no flags,
274 `--generate` with `--words`, and `init` on an existing vault. 275 `--generate` with `--words`, and `init` on an existing vault.
275- The clipboard daemon's restore decision is unit-tested in process; 276- The clipboard daemon's clear decision is unit-tested in process;
276 the tests do not touch the real clipboard. 277 the tests do not touch the real clipboard.
277 278
278The CLI suite is the conformance suite. When the app exists, its 279The CLI suite is the conformance suite. When the app exists, its