max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
8 files changed,
+199 insertions,
-5 deletions
| @@ -1946,9 +1946,61 @@ | |||
| 1946 | 1946 | done; \ | |
| 1947 | 1947 | echo "alloy-shot: $(echo "$modes" | wc -l) bound modes, all handled"; \ | |
| 1948 | 1948 | else \ | |
| 1949 | - | rm -f /usr/bin/alloy-shot /usr/bin/alloy-menu /usr/bin/alloy-clipmenu; \ | |
| 1949 | + | rm -f /usr/bin/alloy-shot /usr/bin/alloy-menu /usr/bin/alloy-clipmenu \ | |
| 1950 | + | /usr/bin/alloy-clipstore /usr/bin/alloy-secret-copy /usr/bin/alloy-secret-clear; \ | |
| 1950 | 1951 | test ! -e /usr/bin/alloy-shot \ | |
| 1951 | 1952 | || { echo "profile=server still carries alloy-shot" >&2; exit 1; }; \ | |
| 1953 | + | test ! -e /usr/bin/alloy-clipstore \ | |
| 1954 | + | || { echo "profile=server still carries alloy-clipstore; there is no clipboard on a headless box" >&2; exit 1; }; \ | |
| 1955 | + | fi | |
| 1956 | + | ||
| 1957 | + | # ===================================================================== | |
| 1958 | + | # The private clipboard — assert the leak stays closed. | |
| 1959 | + | # | |
| 1960 | + | # Alloy ships a password manager and a clipboard history, and until | |
| 1961 | + | # 2026-08-17 the second recorded the output of the first: `gopass show -c` | |
| 1962 | + | # put a password on the clipboard and the sway config's watchers wrote it | |
| 1963 | + | # into cliphist's database, where $mod+Shift+v read it back. GO alloy | |
| 1964 | + | # problem e7a9e38c. | |
| 1965 | + | # | |
| 1966 | + | # Three parts have to agree or the leak is silently back, and no part | |
| 1967 | + | # reports its own absence: | |
| 1968 | + | # | |
| 1969 | + | # 1. the scripts exist and parse | |
| 1970 | + | # 2. the sway config's watchers call alloy-clipstore, not `cliphist store` | |
| 1971 | + | # 3. gopass is pointed at alloy-secret-copy through environment.d | |
| 1972 | + | # | |
| 1973 | + | # Miss (2) and the watcher stores everything. Miss (3) and gopass finds | |
| 1974 | + | # wl-copy by itself. Both look exactly like a working system: the password | |
| 1975 | + | # is on the clipboard, the paste works, and the only difference is a row in | |
| 1976 | + | # a database nobody opens. That is the same shape as the emoji alias and the | |
| 1977 | + | # swayosd unit path, which is why it is asserted rather than trusted. | |
| 1978 | + | RUN set -eu; \ | |
| 1979 | + | if [ "$PROFILE" = client ]; then \ | |
| 1980 | + | set -x; \ | |
| 1981 | + | for script in alloy-clipstore alloy-secret-copy alloy-secret-clear; do \ | |
| 1982 | + | test -x "/usr/bin/$script" \ | |
| 1983 | + | || { echo "$script is missing or not executable; every copied password would be archived" >&2; exit 1; }; \ | |
| 1984 | + | sh -n "/usr/bin/$script" \ | |
| 1985 | + | || { echo "$script does not parse; the private clipboard would fail open" >&2; exit 1; }; \ | |
| 1986 | + | done; \ | |
| 1987 | + | watchers=$(grep -c '^exec wl-paste .*--watch alloy-clipstore' /etc/skel/.config/sway/config); \ | |
| 1988 | + | [ "$watchers" = 2 ] \ | |
| 1989 | + | || { echo "expected 2 clipboard watchers through alloy-clipstore, found $watchers; a password would reach cliphist" >&2; exit 1; }; \ | |
| 1990 | + | ! grep -q '^exec wl-paste .*--watch cliphist store' /etc/skel/.config/sway/config \ | |
| 1991 | + | || { echo "a watcher still calls cliphist store directly, so it records secrets too" >&2; exit 1; }; \ | |
| 1992 | + | env=/etc/skel/.config/environment.d/alloy.conf; \ | |
| 1993 | + | grep -q '^GOPASS_CLIPBOARD_COPY_CMD=/usr/bin/alloy-secret-copy$' "$env" \ | |
| 1994 | + | || { echo "gopass is not pointed at alloy-secret-copy; it would call wl-copy itself and the leak is back" >&2; exit 1; }; \ | |
| 1995 | + | grep -q '^GOPASS_CLIPBOARD_CLEAR_CMD=/usr/bin/alloy-secret-clear$' "$env" \ | |
| 1996 | + | || { echo "gopass has no clear command, so clipboard history would stay paused until the backstop" >&2; exit 1; }; \ | |
| 1997 | + | echo "private clipboard: 2 watchers gated, gopass routed through it"; \ | |
| 1998 | + | else \ | |
| 1999 | + | for script in alloy-clipstore alloy-secret-copy alloy-secret-clear; do \ | |
| 2000 | + | test ! -e "/usr/bin/$script" \ | |
| 2001 | + | || { echo "profile=server still carries $script; there is no clipboard and no gopass on this profile" >&2; exit 1; }; \ | |
| 2002 | + | done; \ | |
| 2003 | + | echo "private clipboard: nothing to gate on a headless profile"; \ | |
| 1952 | 2004 | fi | |
| 1953 | 2005 | ||
| 1954 | 2006 | # ===================================================================== |
| @@ -414,11 +414,21 @@ | |||
| 414 | 414 | **Sway integration:** two watchers `exec`'d from the sway config, and a picker bound to `Mod+Shift+V`: | |
| 415 | 415 | ||
| 416 | 416 | ``` | |
| 417 | - | exec wl-paste --type text --watch cliphist store | |
| 418 | - | exec wl-paste --type image --watch cliphist store | |
| 417 | + | exec wl-paste --type text --watch alloy-clipstore | |
| 418 | + | exec wl-paste --type image --watch alloy-clipstore | |
| 419 | 419 | bindsym $mod+Shift+v exec $term --app-id=alloy-menu -e /usr/bin/alloy-clipmenu | |
| 420 | 420 | ``` | |
| 421 | 421 | ||
| 422 | + | **The watchers call `alloy-clipstore` rather than `cliphist store`, and that is a fix rather than a wrapper for its own sake.** Alloy ships a password manager and a clipboard history, and until 2026-08-17 the second recorded the output of the first: `gopass show -c` put a password on the clipboard, a watcher wrote it into cliphist's database, and `Mod+Shift+V` read it back later. gopass clearing the clipboard after its timeout does not help, because the copy has already been archived by then. A user following this manual on these defaults wrote their passwords to disk in plaintext and was told nothing. | |
| 423 | + | ||
| 424 | + | **The private clipboard, in three small scripts.** `alloy-secret-copy` writes a sentinel into `$XDG_RUNTIME_DIR` and then copies; `alloy-clipstore` reads the sentinel and drops the selection instead of storing it; `alloy-secret-clear` clears the clipboard and removes the sentinel. gopass is pointed at the first and last through `GOPASS_CLIPBOARD_COPY_CMD` and `GOPASS_CLIPBOARD_CLEAR_CMD` in `etc/skel/.config/environment.d/alloy.conf`, which are variables gopass names in its own diagnostics, so this is a documented seam rather than a discovered one. | |
| 425 | + | ||
| 426 | + | **Why a sentinel and not the MIME convention.** The ecosystem already has one: an extra offer type `x-kde-passwordManagerHint` with value `secret`, which KeePassXC emits and KDE's Klipper honours. Two things stop it working here. The cliphist in the image carries no such string, so nothing would honour it, and `wl-copy` offers one type at a time, so a password offered under the hint alone could not be pasted into anything asking for `text/plain`. Honouring the hint is still the better long-term answer and belongs in a clipboard daemon Alloy owns, which would fix this for KeePassXC and Bitwarden users too. | |
| 427 | + | ||
| 428 | + | **The failure direction is the design.** A sentinel left behind pauses clipboard history, which is a feature quietly not working. A sentinel that never appears writes a password to disk. Those are not the same size of mistake, so the sentinel is created before the clipboard is written and a 90-second backstop removes it even if nothing calls the clear command. The alternative considered and rejected was stopping the watcher for the duration, which fails the other way: a crash between stop and restart kills clipboard history until the next login, silently. | |
| 429 | + | ||
| 430 | + | **What this does not fix:** any application that can read the clipboard can still read the password while it is there. This closes the durable archive on disk, not the live exposure, and those are different categories of problem. | |
| 431 | + | ||
| 422 | 432 | The picker is `usr/bin/alloy-clipmenu`: `cliphist list` into fzf into `cliphist decode` into `wl-copy`, which is the pipeline this section already described. It went unwritten longer than it should have, so the watchers filled a history from the first login that nothing could read back. `cliphist decode` rather than the fzf line itself is what makes images and multi-line text survive the round trip. | |
| 423 | 433 | ||
| 424 | 434 | No config file; cliphist's storage lives at `~/.local/share/cliphist/db` and needs no tuning. |
| @@ -78,6 +78,17 @@ | |||
| 78 | 78 | you want one, and the age identity that decrypts it should never live in the | |
| 79 | 79 | repository it decrypts or travel with it. | |
| 80 | 80 | ||
| 81 | + | `gopass show -c` copies a password to the clipboard, and Alloy keeps that copy | |
| 82 | + | out of the clipboard history that `Mod+Shift+V` reads. That is not automatic | |
| 83 | + | anywhere else and it was not automatic here until 2026-08-17: the history | |
| 84 | + | watchers recorded every password anyone copied, and the entry outlived the | |
| 85 | + | clipboard itself. It works now because gopass is pointed at | |
| 86 | + | `alloy-secret-copy`, which marks the selection private, and the watchers skip | |
| 87 | + | anything so marked. Nothing to configure, and the reason it is worth knowing is | |
| 88 | + | the limit: this keeps passwords out of the history *on disk*, and any program | |
| 89 | + | running as you can still read the clipboard while the password is on it. Paste | |
| 90 | + | it and move on rather than leaving it there. | |
| 91 | + | ||
| 81 | 92 | That identity lives on the machine, under `~/.config/gopass/age/`. Run `gopass | |
| 82 | 93 | config` to see the paths your install is actually using rather than trusting | |
| 83 | 94 | this page. It matters because it is the one part of the store that is not in |
| @@ -9,3 +9,16 @@ | |||
| 9 | 9 | # and this variable is inert without it. It stays as a toolkit hint like | |
| 10 | 10 | # the two above, not as browser configuration. | |
| 11 | 11 | MOZ_ENABLE_WAYLAND=1 | |
| 12 | + | ||
| 13 | + | # gopass copies through Alloy's private clipboard path rather than calling | |
| 14 | + | # wl-copy itself, so a password never reaches the clipboard history that the | |
| 15 | + | # sway config's two watchers fill. Without these two lines gopass finds | |
| 16 | + | # wl-copy on its own and the leak is back (GO alloy problem e7a9e38c). | |
| 17 | + | # | |
| 18 | + | # gopass names both variables in its own "no clipboard available" message, so | |
| 19 | + | # this is the documented seam rather than a discovered one. Both are needed: | |
| 20 | + | # the copy half marks the selection private, the clear half unmarks it, and | |
| 21 | + | # setting only the first would pause clipboard history until the backstop | |
| 22 | + | # fired. | |
| 23 | + | GOPASS_CLIPBOARD_COPY_CMD=/usr/bin/alloy-secret-copy | |
| 24 | + | GOPASS_CLIPBOARD_CLEAR_CMD=/usr/bin/alloy-secret-clear |
| @@ -112,8 +112,14 @@ | |||
| 112 | 112 | # the other half, the presets, is in etc/systemd/user-preset/50-alloy.preset. | |
| 113 | 113 | exec mkdir -p ~/Pictures/Screenshots | |
| 114 | 114 | # Clipboard history: two watchers per docs/STACK.md#clipboard-history. | |
| 115 | - | exec wl-paste --type text --watch cliphist store | |
| 116 | - | exec wl-paste --type image --watch cliphist store | |
| 115 | + | # | |
| 116 | + | # Through alloy-clipstore rather than `cliphist store` directly, since | |
| 117 | + | # 2026-08-17. Both are the same one line apart: store the selection, unless | |
| 118 | + | # alloy-secret-copy marked it as a password on its way to a paste. Before that | |
| 119 | + | # these two lines archived every password gopass copied, and $mod+Shift+v read | |
| 120 | + | # them back (GO alloy problem e7a9e38c). | |
| 121 | + | exec wl-paste --type text --watch alloy-clipstore | |
| 122 | + | exec wl-paste --type image --watch alloy-clipstore | |
| 117 | 123 | # swayosd-server backs the volume/brightness/caps overlays, and this line is | |
| 118 | 124 | # the only thing that starts it. Checked rather than assumed, because the note | |
| 119 | 125 | # above retires the sibling `exec`: SwayOSD-0.3.2 ships three binaries and |
| @@ -1,0 +1,27 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # alloy-clipstore — store a clipboard selection, unless it is a secret. | |
| 3 | + | # | |
| 4 | + | # What the sway config's two `wl-paste --watch` lines call instead of | |
| 5 | + | # `cliphist store` directly. The read half is still alloy-clipmenu and the | |
| 6 | + | # store is still cliphist's; this only decides whether an item reaches it. | |
| 7 | + | # | |
| 8 | + | # The rule is one line: if alloy-secret-copy left its sentinel, this selection | |
| 9 | + | # is a password on its way to a paste and is not history. See that script for | |
| 10 | + | # why a sentinel rather than the `x-kde-passwordManagerHint` MIME convention, | |
| 11 | + | # and for the failure direction it is built around. GO alloy problem e7a9e38c. | |
| 12 | + | # | |
| 13 | + | # Consuming stdin on the skip path is deliberate. `wl-paste --watch` writes the | |
| 14 | + | # selection to this process; exiting without reading it leaves the writer with | |
| 15 | + | # a closed pipe and a SIGPIPE on the next selection, which is a broken watcher | |
| 16 | + | # rather than a skipped item. | |
| 17 | + | ||
| 18 | + | set -eu | |
| 19 | + | ||
| 20 | + | sentinel="${XDG_RUNTIME_DIR:-/tmp}/alloy-secret-clipboard" | |
| 21 | + | ||
| 22 | + | if [ -e "$sentinel" ]; then | |
| 23 | + | cat >/dev/null | |
| 24 | + | exit 0 | |
| 25 | + | fi | |
| 26 | + | ||
| 27 | + | exec cliphist store |
| @@ -1,0 +1,21 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # alloy-secret-clear — clear the clipboard and re-enable history. | |
| 3 | + | # | |
| 4 | + | # The other half of alloy-secret-copy, and what gopass runs through | |
| 5 | + | # $GOPASS_CLIPBOARD_CLEAR_CMD when its timeout expires. Clearing the clipboard | |
| 6 | + | # is wl-copy's job; removing the sentinel is what lets alloy-clipstore start | |
| 7 | + | # recording again. | |
| 8 | + | # | |
| 9 | + | # Order matters and is the reverse of the copy path. The clipboard is cleared | |
| 10 | + | # first and the sentinel removed second, so there is no window in which history | |
| 11 | + | # is live and the secret is still on the clipboard. | |
| 12 | + | # | |
| 13 | + | # Safe to run when nothing is pending: both steps are idempotent, and gopass | |
| 14 | + | # calls this on paths where it is not certain a copy happened. | |
| 15 | + | ||
| 16 | + | set -eu | |
| 17 | + | ||
| 18 | + | sentinel="${XDG_RUNTIME_DIR:-/tmp}/alloy-secret-clipboard" | |
| 19 | + | ||
| 20 | + | wl-copy --clear || true | |
| 21 | + | rm -f "$sentinel" |