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
44enum Clipboard {
55 struct Handoff: Codable, Equatable {
66 var secret: String
7 var previous: String
87 }
98
109 struct Tool: Equatable {
@@ -14,7 +13,7 @@ enum Clipboard {
1413
1514 static let timeoutSeconds = 45
1615
17 static func shouldRestore(secret: String, current: String?) -> Bool {
16 static func shouldClear(secret: String, current: String?) -> Bool {
1817 current == secret
1918 }
2019
@@ -75,7 +74,7 @@ enum Clipboard {
7574
7675 static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws {
7776 _ = try requireTool()
78 let handoff = Handoff(secret: secret, previous: try read())
77 let handoff = Handoff(secret: secret)
7978 try write(secret)
8079 let process = Process()
8180 process.executableURL = Bundle.main.executableURL
@@ -103,8 +102,8 @@ enum Clipboard {
103102 }
104103 Thread.sleep(forTimeInterval: TimeInterval(seconds))
105104 let current = try? read()
106 guard shouldRestore(secret: handoff.secret, current: current) else { return }
107 try write(handoff.previous)
105 guard shouldClear(secret: handoff.secret, current: current) else { return }
106 try write("")
108107 }
109108
110109 private static func requireTool() throws -> Tool {
Tests/KeycaskCLITests/ClipboardTests.swift +5 −5
@@ -4,14 +4,14 @@ import Testing
44@testable import keycask
55
66@Suite struct ClipboardTests {
7 @Test func restoresOnlyWhenClipboardStillHoldsTheSecret() {
8 #expect(Clipboard.shouldRestore(secret: "s", current: "s"))
9 #expect(!Clipboard.shouldRestore(secret: "s", current: "user pasted"))
10 #expect(!Clipboard.shouldRestore(secret: "s", current: nil))
7 @Test func clearsOnlyWhenClipboardStillHoldsTheSecret() {
8 #expect(Clipboard.shouldClear(secret: "s", current: "s"))
9 #expect(!Clipboard.shouldClear(secret: "s", current: "user pasted"))
10 #expect(!Clipboard.shouldClear(secret: "s", current: nil))
1111 }
1212
1313 @Test func handoffRoundTrips() throws {
14 let h = Clipboard.Handoff(secret: "s3cret", previous: "old")
14 let h = Clipboard.Handoff(secret: "s3cret")
1515 let data = try JSONEncoder().encode(h)
1616 #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h)
1717 }
docs/superpowers/specs/2026-09-17-keycask-design.md +7 −6
@@ -224,11 +224,12 @@ and a Windows body where they differ.
224224 Linux; `clip.exe` to write and `powershell -command Get-Clipboard` to
225225 read on Windows. No tool found is exit 1 with a message naming the
226226 tools. `clip` spawns `keycask clipboard-daemon` detached with stdout
227 and stderr to null, writes `{"secret": ..., "previous": ...}` to its
228 stdin, and exits without waiting. `clip` writes the clipboard itself,
229 then spawns the daemon. The daemon sleeps 45 seconds, reads the
230 clipboard, and if it still equals the secret restores `previous` or
231 clears when `previous` is empty.
227 and stderr to null and exits without waiting. `clip` writes the
228 clipboard itself, then spawns the daemon with `{"secret": ...}` on its
229 stdin. The daemon sleeps 45 seconds, reads the clipboard, and clears
230 it if it still equals the secret. It never restores earlier contents,
231 so a second `clip` inside the window cannot bring an earlier secret
232 back.
232233
233234## Errors
234235
@@ -272,7 +273,7 @@ CLI, black box:
272273 masking versus `--reveal`, `--field` raw output, the ambiguous-name
273274 listing, `rm` without `--yes` and without a TTY, `edit` with no flags,
274275 `--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;
276277 the tests do not touch the real clipboard.
277278
278279The CLI suite is the conformance suite. When the app exists, its