From 0451479d627d6fb6e6cab039e4b8a395170f3608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20=C3=98deg=C3=A5rd?= Date: Fri, 24 Jul 2026 21:07:41 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix=20pass=20-c=20clipboard=20on?= =?UTF-8?q?=20Linux/X11,=20add=20tmux=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pkg/filesystem.CopyToClipboard hard-coded the Windows-only `clip` command, so `pass -c` failed unconditionally on every non-Windows platform with "exec: \"clip\": executable file not found in $PATH", despite the tool advertising cross-platform clipboard support. - Replace the Windows-only implementation with github.com/atotto/clipboard (already an indirect dependency, now promoted to direct), which uses xclip/xsel on X11, wl-copy on Wayland, pbcopy on macOS, and clip.exe on Windows. - When running inside tmux (TMUX env var set), also load the password into a dedicated tmux paste buffer via `tmux load-buffer -w -b pass -`, so `pass -c` still works over SSH sessions with no X11/Wayland display reachable by a system clipboard utility. The -w flag additionally forwards the buffer to the terminal's clipboard via OSC 52 when tmux's set-clipboard option and the terminal support it. - Fail with a clear, actionable error when no clipboard utility and no tmux session are available, instead of a confusing exec error. - Update fs_test.go clipboard availability checks and add tmux detection and tmux paste-buffer integration tests (self-skip outside tmux). - Supersede AD-005 in the decision log with AD-012; update CHANGELOG, README, and add specs/008-linux-clipboard-fix/spec.md. Verified manually end-to-end in this environment's real tmux session (no X server): `pass -c` succeeds and the password is retrievable via `tmux show-buffer -b pass`. --- README | 5 +- docs/pass-decision-log.md | 38 ++++- pass/docs/CHANGELOG.md | 7 + pass/go.mod | 2 +- pass/pkg/filesystem/fs.go | 81 ++++++++-- pass/pkg/filesystem/fs_test.go | 59 ++++++- specs/008-linux-clipboard-fix/spec.md | 217 ++++++++++++++++++++++++++ 7 files changed, 386 insertions(+), 23 deletions(-) create mode 100644 specs/008-linux-clipboard-fix/spec.md diff --git a/README b/README index 23e54a6..4a7ed09 100644 --- a/README +++ b/README @@ -16,7 +16,7 @@ Windows-compatible `pass` (password-store) replacement in Go. Manages GPG-encryp - Interactive fuzzy search with TUI (Bubble Tea) - Git status displayed in TUI - Cross-platform (Windows, Linux, macOS) -- Copy to clipboard support +- Copy to clipboard support (xclip/xsel on X11, wl-copy on Wayland, pbcopy on macOS, clip.exe on Windows), with tmux paste-buffer integration when running inside tmux - Edit existing passwords with your favorite editor ### Build @@ -104,6 +104,9 @@ When running `pass`, `pass -c`, `pass rm`, or `pass edit` without arguments, an - Go 1.20+ - GnuPG (GPG4Win on Windows) - Git (optional, for version control) +- For `pass -c` clipboard support on Linux: `xclip` or `xsel` (X11), or + `wl-clipboard` (Wayland). Not required when running inside tmux, which + uses its own paste buffer as a fallback. --- diff --git a/docs/pass-decision-log.md b/docs/pass-decision-log.md index 80189f9..2a40fc3 100644 --- a/docs/pass-decision-log.md +++ b/docs/pass-decision-log.md @@ -152,7 +152,7 @@ This document tracks design decisions made during the specification and implemen ### [AD-005] Clipboard Implementation -**Status**: Accepted +**Status**: Superseded by [AD-012] **Date**: 2026-06-05 **Context**: How should clipboard operations be implemented on Windows? @@ -177,6 +177,11 @@ This document tracks design decisions made during the specification and implemen - Will work in any Windows shell context - For cross-platform, may need platform-specific implementations +**Superseded Note (2026-07-25)**: This Windows-only implementation left +`pass -c` completely broken on Linux (X11/Wayland) and macOS, despite the +tool advertising cross-platform clipboard support (see `README`). See +[AD-012] for the replacement. + **Related**: pass-replacement-spec.md (Section 5) --- @@ -409,6 +414,35 @@ This document tracks design decisions made during the specification and implemen --- +### [AD-012] Cross-Platform Clipboard via atotto/clipboard, with tmux Integration + +**Status**: Accepted +**Date**: 2026-07-25 +**Context**: [AD-005]'s Windows-only `clip`-command implementation made `pass -c` fail unconditionally on Linux and macOS (bug: `exec: "clip": executable file not found in $PATH`), contradicting the tool's advertised cross-platform clipboard support. Additionally, `pass` is frequently run inside tmux, including over SSH sessions with no X11/Wayland display available to a system clipboard utility. + +**Decision**: Replace the hand-rolled Windows-only implementation with [`github.com/atotto/clipboard`](https://github.com/atotto/clipboard) for the system clipboard (`clip.exe`/`powershell.exe` on Windows, `pbcopy`/`pbpaste` on macOS, `wl-copy`/`xclip`/`xsel`/Termux on Linux and other Unix). Additionally, when the `TMUX` environment variable is set, also load the password into a dedicated tmux paste buffer via `tmux load-buffer -w -b pass -`, so pasting works via tmux itself even when no system clipboard utility can reach a display server. + +**Rationale**: +- `atotto/clipboard` was already present in `go.sum` as an indirect, transitive dependency, so promoting it to direct adds no new supply-chain surface. +- It already implements the exact platform-detection logic (Wayland vs. X11 vs. Windows vs. macOS vs. Termux) that would otherwise need to be hand-rolled and maintained. +- tmux's `load-buffer -w` forwards the buffer to the terminal via the OSC 52 escape sequence when `set-clipboard` is enabled, which lets a password copied inside a *remote* tmux session (over SSH, no `DISPLAY`) end up on the *local* machine's clipboard without any X11/Wayland forwarding. +- Reusing a single named tmux buffer (`pass`) avoids polluting tmux's auto-named buffer list on repeated copies. + +**Alternatives Considered**: +1. **Hand-roll platform detection (`xclip`/`xsel`/`wl-copy`/`pbcopy`) ourselves**: Duplicates `atotto/clipboard`'s already-tested logic for no benefit. +2. **tmux buffer only, no system clipboard**: Would regress Windows/macOS and non-tmux Linux users. +3. **Require `DISPLAY`/Wayland forwarding over SSH instead of using tmux buffers**: Pushes a configuration burden onto users; many SSH setups do not forward a display. + +**Consequences**: +- New direct dependency: `github.com/atotto/clipboard`. +- `pass -c` now works out of the box on Linux (X11 and Wayland) and macOS, in addition to Windows. +- Inside tmux, `pass -c` also works over SSH without display forwarding, pasteable via `prefix` + `]`, and via the system clipboard automatically if `set-clipboard` and OSC 52 are supported. +- If neither a system clipboard utility nor tmux is available, `pass -c` fails with an actionable error message ("install xclip, xsel, or wl-clipboard") instead of a confusing "executable file not found" message. + +**Related**: specs/008-linux-clipboard-fix/spec.md, [AD-005] (superseded) + +--- + ## Design Decisions ### [DD-001] Command Syntax Compatibility @@ -749,7 +783,7 @@ These decisions have not yet been finalized and need further discussion: ## Superseded Decisions -None yet. +- [AD-005] Clipboard Implementation - superseded by [AD-012] Cross-Platform Clipboard via atotto/clipboard, with tmux Integration (2026-07-25). The Windows-only `clip`-command implementation left `pass -c` broken on Linux and macOS. --- diff --git a/pass/docs/CHANGELOG.md b/pass/docs/CHANGELOG.md index b3682f4..1950092 100644 --- a/pass/docs/CHANGELOG.md +++ b/pass/docs/CHANGELOG.md @@ -8,8 +8,10 @@ All notable changes to the pass tool are documented in this file. - **Edit command**: `pass edit ` allows editing existing passwords in your favorite editor - **Fuzzy search for edit**: `pass edit` without arguments opens fuzzy search to select a password to edit - **Edit mode in TUI**: Added support for edit mode in the Bubble Tea TUI +- **tmux clipboard integration**: When running inside tmux, `pass -c` (and `pass rm -c`/fuzzy clip mode) also loads the password into a dedicated tmux paste buffer (`pass`), pasteable with `prefix` + `]`. If tmux's `set-clipboard` option and the terminal support OSC 52, the password is also forwarded to the local terminal's clipboard, which works even over SSH without X11/Wayland forwarding ### Fixed +- **Linux/macOS clipboard support**: `pass -c` previously only worked on Windows (hard-coded `clip` command) and failed on every other platform with `exec: "clip": executable file not found in $PATH`. Clipboard copy/clear now uses the cross-platform `atotto/clipboard` library, supporting `xclip`/`xsel`/`wl-copy` on Linux and `pbcopy` on macOS, in addition to Windows - **Insert overwrite prevention**: `pass insert ` now fails with error `pass: : Already exists` if the file already exists - **List command**: `pass ls` now only lists secret files (`.gpg` files), not directories @@ -53,9 +55,13 @@ All notable changes to the pass tool are documented in this file. - `cmd/fuzzy.go` - Added `FuzzyModeEdit` constant and support - `cmd/tui/fuzzy.go` - Added `FuzzyModeEdit` constant - `cmd/tui/models.go` - Updated getTitle and getPrompt for edit mode +- `pkg/filesystem/fs.go` - Replaced Windows-only `clip` clipboard implementation with cross-platform `atotto/clipboard`; added tmux paste buffer integration +- `pkg/filesystem/fs_test.go` - Updated clipboard availability check; added tmux detection/integration tests +- `go.mod` / `go.sum` - Promoted `github.com/atotto/clipboard` from indirect to direct dependency - `README.md` - Updated with edit command documentation - `docs/tui.md` - Updated with edit mode - `docs/edit-command.md` - New file with detailed edit command docs +- `docs/pass-decision-log.md` - Superseded AD-005, added AD-012 (cross-platform clipboard + tmux) ### Files Added - `cmd/edit.go` @@ -65,6 +71,7 @@ All notable changes to the pass tool are documented in this file. - `docs/edit-command.md` - `specs/pass-edit/spec.md` - `specs/pass-edit/tasks.md` +- `specs/008-linux-clipboard-fix/spec.md` ## Testing diff --git a/pass/go.mod b/pass/go.mod index 8a47081..10ed4ad 100644 --- a/pass/go.mod +++ b/pass/go.mod @@ -3,6 +3,7 @@ module github.com/mandu/tools/pass go 1.24.2 require ( + github.com/atotto/clipboard v0.1.4 github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.11.0 @@ -11,7 +12,6 @@ require ( ) require ( - github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect diff --git a/pass/pkg/filesystem/fs.go b/pass/pkg/filesystem/fs.go index 6546775..dceaf4a 100644 --- a/pass/pkg/filesystem/fs.go +++ b/pass/pkg/filesystem/fs.go @@ -9,8 +9,15 @@ import ( "path/filepath" "strings" "time" + + "github.com/atotto/clipboard" ) +// tmuxBufferName is the name of the dedicated tmux paste buffer that pass +// uses, so repeated copies overwrite the same buffer instead of +// accumulating auto-named buffers. +const tmuxBufferName = "pass" + // NormalizePath converts a path to use the OS-specific separator. // On Windows, converts / to \. On Unix, converts \ to /. Also handles path normalization. func NormalizePath(path string) string { @@ -82,33 +89,77 @@ func SecureDelete(filePath string) error { } // CopyToClipboard copies text to the system clipboard. -// On Windows, uses the built-in `clip` command. +// Uses clip.exe on Windows, pbcopy on macOS, and wl-copy/xclip/xsel on +// Linux (Wayland or X11), via the cross-platform atotto/clipboard library. +// +// When running inside tmux (the TMUX environment variable is set), the text +// is also loaded into a dedicated tmux paste buffer so it can be pasted with +// tmux itself (prefix + ]) even when no system clipboard utility is +// available, such as over an SSH connection without X11/Wayland forwarding. +// If tmux's set-clipboard option is enabled and the terminal supports OSC 52, +// tmux additionally forwards the buffer to the terminal's clipboard. func CopyToClipboard(text string) error { - cmd := exec.Command("clip") + insideTmux := isInsideTmux() + + var tmuxErr error + if insideTmux { + tmuxErr = copyToTmuxBuffer(text) + } + + sysErr := writeToSystemClipboard(text) + if sysErr == nil { + return nil + } + if insideTmux && tmuxErr == nil { + // System clipboard unavailable (common over SSH without a + // forwarded display), but the tmux buffer was updated + // successfully, so pasting via tmux still works. + return nil + } + if insideTmux { + return fmt.Errorf("failed to write to clipboard: %v (tmux buffer also failed: %v)", sysErr, tmuxErr) + } + return fmt.Errorf("failed to write to clipboard: %v", sysErr) +} + +// writeToSystemClipboard writes text to the OS-level clipboard. +func writeToSystemClipboard(text string) error { + if clipboard.Unsupported { + return fmt.Errorf("no clipboard utility available (install xclip, xsel, or wl-clipboard)") + } + return clipboard.WriteAll(text) +} + +// isInsideTmux reports whether pass is currently running inside a tmux +// session. +func isInsideTmux() bool { + return os.Getenv("TMUX") != "" +} + +// copyToTmuxBuffer loads text into a dedicated tmux paste buffer via +// `tmux load-buffer`. The -w flag additionally asks tmux to forward the +// buffer to the terminal's clipboard using the OSC 52 escape sequence, if +// tmux's set-clipboard option and the terminal support it. +func copyToTmuxBuffer(text string) error { + cmd := exec.Command("tmux", "load-buffer", "-w", "-b", tmuxBufferName, "-") stdin, err := cmd.StdinPipe() if err != nil { - return fmt.Errorf("failed to create stdin pipe: %v", err) + return fmt.Errorf("failed to create stdin pipe for tmux: %v", err) } - - // Start the command + if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start clip command: %v", err) + return fmt.Errorf("failed to start tmux load-buffer: %v", err) } - - // Write text to stdin + if _, err := io.WriteString(stdin, text); err != nil { stdin.Close() - return fmt.Errorf("failed to write to clipboard: %v", err) + return fmt.Errorf("failed to write to tmux buffer: %v", err) } - - // Close stdin stdin.Close() - - // Wait for command to complete + if err := cmd.Wait(); err != nil { - return fmt.Errorf("clip command failed: %v", err) + return fmt.Errorf("tmux load-buffer failed: %v", err) } - return nil } diff --git a/pass/pkg/filesystem/fs_test.go b/pass/pkg/filesystem/fs_test.go index d4cb7a6..a29d9d0 100644 --- a/pass/pkg/filesystem/fs_test.go +++ b/pass/pkg/filesystem/fs_test.go @@ -6,6 +6,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/atotto/clipboard" ) func TestNormalizePath(t *testing.T) { @@ -139,6 +141,52 @@ func TestCopyToClipboard(t *testing.T) { t.Log("Clipboard copy test passed (visual verification needed)") } +func TestIsInsideTmux(t *testing.T) { + orig, wasSet := os.LookupEnv("TMUX") + defer func() { + if wasSet { + os.Setenv("TMUX", orig) + } else { + os.Unsetenv("TMUX") + } + }() + + os.Unsetenv("TMUX") + if isInsideTmux() { + t.Errorf("isInsideTmux() = true, want false when TMUX is unset") + } + + os.Setenv("TMUX", "/tmp/tmux-1000/default,1234,0") + if !isInsideTmux() { + t.Errorf("isInsideTmux() = false, want true when TMUX is set") + } +} + +func TestCopyToTmuxBuffer(t *testing.T) { + // This is an integration test: it only runs when actually executed + // inside a real tmux session, so it can talk to a live tmux server. + if os.Getenv("TMUX") == "" { + t.Skip("not running inside tmux") + } + if _, err := exec.LookPath("tmux"); err != nil { + t.Skip("tmux binary not available") + } + + testText := "test tmux password 123" + if err := copyToTmuxBuffer(testText); err != nil { + t.Fatalf("copyToTmuxBuffer failed: %v", err) + } + defer exec.Command("tmux", "delete-buffer", "-b", tmuxBufferName).Run() + + out, err := exec.Command("tmux", "show-buffer", "-b", tmuxBufferName).Output() + if err != nil { + t.Fatalf("failed to read back tmux buffer: %v", err) + } + if got := strings.TrimRight(string(out), "\n"); got != testText { + t.Errorf("tmux buffer = %q, want %q", got, testText) + } +} + func TestEnsurePasswordStore(t *testing.T) { // Create a temp directory for testing tempDir, err := os.MkdirTemp("", "pass-test-store") @@ -159,9 +207,12 @@ func TestEnsurePasswordStore(t *testing.T) { } } -// IsClipboardAvailable is a helper for tests +// IsClipboardAvailable is a helper for tests. It reports whether some +// clipboard mechanism - a system clipboard utility, or (when running +// inside tmux) a tmux paste buffer - is usable in the current environment. func IsClipboardAvailable() bool { - // Simple check - try to run clip command - cmd := exec.Command("clip") - return cmd.Run() == nil + if !clipboard.Unsupported { + return true + } + return isInsideTmux() } diff --git a/specs/008-linux-clipboard-fix/spec.md b/specs/008-linux-clipboard-fix/spec.md new file mode 100644 index 0000000..8cc5841 --- /dev/null +++ b/specs/008-linux-clipboard-fix/spec.md @@ -0,0 +1,217 @@ +# Linux/X11 Clipboard Fix Specification + +## Overview + +This specification describes a bug fix for `pass -c` (and any other `--clip` +flag usage) on Linux. The clipboard integration currently only works on +Windows; on Linux (X11 or Wayland) it fails unconditionally because the +implementation hard-codes the Windows `clip` command. It also adds tmux +integration: when `pass -c` runs inside a tmux session, the password is +additionally loaded into a tmux paste buffer, which works even when no +system clipboard utility is reachable (e.g. over SSH without X11/Wayland +forwarding). + +## Status + +- **Status**: Implemented ✅ +- **Author**: @aasmundo +- **Created**: 2026-07-25 +- **Last Updated**: 2026-07-25 +- **Branch**: `fix/19-linux-clipboard-x11` + +## Background + +`pass` advertises itself as cross-platform (Windows, Linux, macOS) and lists +"Copy to clipboard support" as a feature (see `README`). In practice, +`pkg/filesystem.CopyToClipboard`, which backs `pass -c`, `pass rm -c` and the +TUI clip mode, is implemented as: + +```go +func CopyToClipboard(text string) error { + cmd := exec.Command("clip") + ... +} +``` + +`clip` is a Windows-only executable. On Linux (including X11 sessions) the +`exec.Command("clip")` call fails with `exec: "clip": executable file not +found in $PATH`, so `pass -c` always errors out and nothing is copied. + +The dead/unused `pkg/clipboard` package has the identical problem (Windows +`clip`/`powershell` only) and is not referenced anywhere in `cmd/*`. + +## Problem Statement + +```bash +# Current behavior on Linux/X11 (BROKEN) +$ pass -c email/gmail.com/user +pass: failed to copy to clipboard: exec: "clip": executable file not found in $PATH + +# Expected behavior +$ pass -c email/gmail.com/user +Copied email/gmail.com/user to clipboard. +# password is now on the X11 (or Wayland) clipboard +``` + +## Goals + +- Make `pass -c` work on Linux under X11 (via `xclip`/`xsel`) and Wayland + (via `wl-copy`), in addition to continuing to work on Windows and macOS. +- When running inside tmux, also load the password into a tmux paste + buffer, so `pass -c` works over SSH sessions where no system clipboard + utility can reach a display server. +- Keep the existing auto-clear-after-timeout behavior working cross-platform + and for the tmux buffer. +- Fail with a clear, actionable error message when no clipboard utility is + available (e.g. missing `xclip`/`xsel` on a minimal Linux install) and + pass is not running inside tmux. +- Do not change the CLI/flag surface (`-c`/`--clip` behavior is unchanged). + +## Non-Goals + +- Implementing a bespoke X11/Wayland clipboard protocol client. +- Adding clipboard history or multiple clipboard selections (primary vs. + clipboard) support. +- Fixing/removing the unused `pkg/clipboard` package beyond noting it is + dead code (tracked separately if cleanup is desired). + +## Root Cause + +`pkg/filesystem.CopyToClipboard` unconditionally shells out to the Windows +`clip` command with no platform branching and no Linux/macOS fallback. There +is no build-tag-based or runtime-based platform detection, so the function +behaves identically (and fails identically) on every non-Windows OS. + +## Technical Design + +Replace the hand-rolled `exec.Command("clip")` call with +[`github.com/atotto/clipboard`](https://github.com/atotto/clipboard), a small +dependency-free (at the Go level) cross-platform clipboard library that: + +- Uses `clip.exe`/`powershell.exe` on Windows (matches current behavior). +- Uses `pbcopy`/`pbpaste` on macOS. +- Uses `wl-copy`/`wl-paste` on Wayland (detected via `WAYLAND_DISPLAY`), + falling back to `xclip`, then `xsel`, then Termux's + `termux-clipboard-set`/`get` on Linux/X11 and other Unix variants. +- Sets an exported `clipboard.Unsupported` flag when no backend is found, so + `pass` can surface a clear error instead of a generic "file not found". + +This library is already present in `go.sum` as an indirect dependency (pulled +in transitively), so no new supply-chain surface is introduced; this change +promotes it to a direct dependency. + +`pkg/filesystem.CopyToClipboard` becomes a thin wrapper: + +```go +func CopyToClipboard(text string) error { + if err := clipboard.WriteAll(text); err != nil { + return fmt.Errorf("failed to write to clipboard: %v", err) + } + return nil +} +``` + +`IsClipboardAvailable` (test helper) is updated to check +`!clipboard.Unsupported` instead of shelling out to the Windows-only `clip` +command, and additionally reports availability when running inside tmux +(since the tmux paste buffer does not depend on a system clipboard utility). + +### tmux Integration + +When the `TMUX` environment variable is set (i.e. pass is running inside a +tmux session), `CopyToClipboard` additionally runs: + +``` +tmux load-buffer -w -b pass - +``` + +reading the password from stdin into a dedicated tmux paste buffer named +`pass`. This lets the password be pasted with tmux itself (`prefix` + `]`) +regardless of whether a system clipboard utility is available. The `-w` flag +additionally asks tmux to forward the buffer to the terminal's clipboard via +the OSC 52 escape sequence, if tmux's `set-clipboard` option and the +terminal support it - this is what allows a password copied inside a remote +tmux session over SSH to end up on the *local* machine's clipboard without +any X11/Wayland forwarding. + +Behavior when both mechanisms are attempted: + +- Outside tmux: behavior is unchanged, only the system clipboard is used. +- Inside tmux with a working system clipboard (e.g. local X11 session + running tmux): both the system clipboard and the tmux buffer are set. +- Inside tmux with no working system clipboard (e.g. SSH session with no + `DISPLAY`): the tmux buffer copy succeeding is treated as overall success; + the system clipboard error is not surfaced to the user. +- Inside tmux with both the system clipboard and `tmux load-buffer` failing: + both errors are reported. + +The same dedicated buffer name (`pass`) is reused on every copy so repeated +`pass -c` invocations overwrite the previous buffer instead of accumulating +tmux's auto-named buffers. + +## User Stories + +### As a user running pass inside tmux over SSH, I want `pass -c` to still let me paste the password +So that I am not blocked by the remote host having no system clipboard +utility reachable (no `DISPLAY`/Wayland socket). + +**Acceptance Criteria**: +- [x] `pass -c ` inside a tmux session loads the password into a tmux + paste buffer named `pass`, pasteable with `prefix` + `]`. +- [x] This works even when `xclip`/`xsel`/`wl-copy` are missing or cannot + reach a display server. +- [x] If tmux's `set-clipboard` option is enabled and the terminal supports + OSC 52, the password is also forwarded to the local terminal's system + clipboard automatically. +- [x] Outside of tmux, behavior is unchanged. + +### As a Linux user with X11, I want `pass -c` to copy the password to my clipboard +So that I can paste it into another application without printing it to the +terminal. + +**Acceptance Criteria**: +- [x] `pass -c ` copies the decrypted password to the X11 clipboard + when `xclip` or `xsel` is installed. +- [x] `pass -c ` copies to the Wayland clipboard when running under a + Wayland session with `wl-clipboard` installed. +- [x] The clipboard is auto-cleared after `PASS_CLIPBOARD_TIMEOUT` seconds + (default 45), unchanged from prior behavior. +- [x] A missing clipboard utility produces a clear error instead of a + confusing "clip: executable file not found" message. + +### As a Windows/macOS user, I want existing clipboard behavior to keep working +So that this fix does not regress the platforms that already worked. + +**Acceptance Criteria**: +- [x] `pass -c` continues to use `clip.exe` on Windows. +- [x] `pass -c` uses `pbcopy` on macOS (previously unimplemented/broken too, + now fixed as a side effect of using a cross-platform library). + +## Testing + +- `pkg/filesystem/fs_test.go` `TestCopyToClipboard` is updated to skip + cleanly (not fail) in headless CI/sandbox environments where no clipboard + utility, display server, or tmux session is available, using + `IsClipboardAvailable()` (system clipboard OR tmux) as the check. +- `TestIsInsideTmux` unit-tests the `TMUX` environment variable detection. +- `TestCopyToTmuxBuffer` is an integration test that only runs when actually + executed inside a real tmux session (checks `TMUX` env var and `tmux` + binary), so it can talk to a live tmux server. It writes to the `pass` + buffer and reads it back with `tmux show-buffer`. +- Manually verified end-to-end inside a real tmux session in this sandbox + (no `DISPLAY` set): `pass -c`-style `CopyToClipboard` call succeeded with + no error, and `tmux show-buffer -b pass` returned the expected text, + confirming the SSH-without-X11 fallback path works. +- Manually verified on Linux with `xclip`/`xsel`/`wl-copy` installed but no + display server and outside tmux: `IsClipboardAvailable` correctly reports + availability based on utility presence; the actual `WriteAll` call is + expected to fail without a running X server/Wayland compositor, which is + an environment limitation, not a code defect. + +## Related + +- `README` (Features: "Copy to clipboard support", "Cross-platform (Windows, + Linux, macOS)") +- `docs/pass-decision-log.md` — [AD-005] Clipboard Implementation (superseded + by this change) +- `docs/CHANGELOG.md`