Skip to main content

max / alloy

2.7 KB · 55 lines History Blame Raw
1 #!/bin/sh
2 # alloy-secret-copy — put something on the clipboard WITHOUT recording it.
3 #
4 # The write half of the private clipboard. Alloy ships a password manager
5 # (gopass) and a clipboard history (cliphist, two `wl-paste --watch` watchers
6 # in the sway config), and until 2026-08-17 the second recorded the output of
7 # the first: `gopass show -c` put a password on the clipboard, the watcher
8 # stored it in cliphist's database on disk, and $mod+Shift+v read it back
9 # later. gopass clearing the clipboard after its timeout does not help, because
10 # the copy has already been archived by then. GO alloy problem e7a9e38c.
11 #
12 # WHY A SENTINEL RATHER THAN A MIME TYPE. The ecosystem convention is an extra
13 # offer type, `x-kde-passwordManagerHint: secret`, which KeePassXC emits and
14 # KDE's Klipper honours. Two things stop it working here: the cliphist in this
15 # image carries no such string, so there is nothing to honour it, and wl-copy
16 # offers one type at a time, so a password offered under the hint alone could
17 # not be pasted into anything that asks for text/plain. Honouring the hint is
18 # still the right long-term fix and belongs in a clipboard daemon Alloy owns;
19 # this is the version that works with the tools already installed.
20 #
21 # WHY NOT STOP THE WATCHER. Killing `wl-paste --watch` for the duration and
22 # restarting it afterwards was the other candidate. It fails open: if the
23 # process that killed it dies before it restarts, clipboard history is dead
24 # until the next login and nothing says so. The sentinel fails the other way.
25 #
26 # THE FAILURE DIRECTION IS THE POINT. A stale sentinel pauses clipboard
27 # history, which is a feature quietly not working. A missing sentinel writes a
28 # password to disk. Those are not the same size of mistake, so the mechanism is
29 # built to fail toward the first: the sentinel is created BEFORE the clipboard
30 # is written, and a backstop removes it even if nothing ever calls
31 # alloy-secret-clear.
32 #
33 # Reads the secret on stdin so it never appears in argv, which is world
34 # readable through /proc.
35
36 set -eu
37
38 sentinel="${XDG_RUNTIME_DIR:-/tmp}/alloy-secret-clipboard"
39
40 # The backstop, in seconds. Longer than gopass's own clipboard timeout (45s by
41 # default) so a normal copy-then-clear cycle is covered by the clear command,
42 # and short enough that a crash costs a minute of history rather than a
43 # session. A second copy while one is live re-arms it rather than stacking.
44 BACKSTOP=90
45
46 # Before the write, not after: the watcher fires on the clipboard changing, so
47 # a sentinel created afterwards would race the thing it exists to prevent.
48 : > "$sentinel"
49
50 # Detached, and it survives this script exiting, which is the whole job: gopass
51 # calls this and returns immediately.
52 ( sleep "$BACKSTOP"; rm -f "$sentinel" ) >/dev/null 2>&1 &
53
54 exec wl-copy
55