Fix double-paste: kitty router socket + daemon dedup window/normalization#1
Merged
Merged
Conversation
A single Ctrl+V inside kitty+tmux fired two handlers (kitty `map ctrl+v` + tmux `bind -n C-v`), both calling flashpaste-trigger ~hundreds of ms apart, which the daemon's dedup window had to race. The new router suppresses kitty's redundant handler when it can confidently detect tmux in the focused window (kitty remote control + foreground process is tmux), so tmux's bind is the only fire. On any uncertainty it falls through to the original path and the daemon dedup still backstops — a detection miss degrades to today's behaviour, never to a broken paste. - bin/kitty-paste-router.sh: the launch target for kitty's `map ctrl+v` - bin/kitty-focused-is-tmux.py: parses `kitten @ ls --match state:focused`, exits 0 only when the focused window's foreground process is tmux - kitty.conf `map ctrl+v` now points at the router (deployed to ~/.local/bin) Parser verified on 5 cases (tmux, non-tmux, empty, garbage, tmux-in-other- window). Latency: the kitten round-trip only delays kitty's REDUNDANT handler; tmux's fast bind already did the user-visible paste. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon logged "PASTED text/image" = "I dispatched", which can't tell a clean paste from a leak — this session's binary-wall and blob bugs were invisible in the journal until the user screenshotted them. - dispatch_text_paste now refuses to paste a payload that fails looks_like_text (image magic / NUL / non-UTF8) and logs outcome= rejected-nontext. The daemon must never dump image bytes into a pane as text; this is the single chokepoint all text pastes flow through, so it's the right last gate even though upstream staging should already reject. - "PASTED text" now carries sent_bytes, html_sanitized, outcome=clean so a sanitized or suppressed paste is visible in the log. - looks_like_text made pub(crate) so paste.rs shares the ipc.rs check. 30/30 tests pass (+2 covering the guard decision). Daemon rebuilt + restarted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every bug this session shipped because only Rust unit tests existed and nothing exercised the actual paste-decision logic end to end. Add a headless, mocked-clipboard regression suite that IS that net: - tests/wl-paste-guard.test.sh: the image-coexistence guard (7 checks) — image present ⇒ text reads return empty; image/png + list-types still work. - tests/kitty-focused-is-tmux.test.sh: the router's tmux detector (5 checks) — exit 0 only when the focused window's foreground process is tmux. - bin/flashpaste-selftest.sh: one command runs the suite (+ `--rust` for cargo test). Mocks the clipboard, so it needs no display and never touches the user's real selection. - flashpaste-doctor now runs the suite and a failure counts as a doctor fail. - CI: new `behavior-tests` job in lint.yml runs the suite on every push/PR. Suite green: 12/12 checks. Doctor integration verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The double-paste bug was config drift: kitty.conf and tmux.conf each bound Ctrl+V independently and got out of sync. Rather than rewrite the user's hand-tuned, richly-commented dotfiles (high risk, low marginal value now that #2 fixed the actual handler), declare ONE canonical source and enforce it read-only: - config/keybindings.canonical: the source of truth — kitty ctrl+v must route through kitty-paste-router.sh, tmux C-v through flashpaste-trigger. - bin/flashpaste-keybindings-check.sh: read-only checker (env-overridable for tests) that flags drift. Escapes ERE metachars so the literal '+' in ctrl+v matches correctly (caught by its own test). - flashpaste-doctor warns (not fails) on drift — it degrades to daemon dedup. - tests/keybindings-check.test.sh: 4 fixture cases (consistent, kitty drift, missing tmux, the ctrl+v literal-plus regression). In CI via the suite. Live check passes: kitty + tmux consistent. Suite green: 16/16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…py lints) handle_paste was a ~290-line god-function where the probe/override races hide. Extract its three phases verbatim into named helpers so the hot path reads as a sequence and each phase is reviewable in isolation: - eager_screenshot_pickup() — inotify-lag screenshot pickup - eager_live_image_pickup() — browser "Copy Image" bridge - resolve_paste_intent() — the staged-slot + live-clipboard override cascade, returns the StagedSelection to dispatch handle_paste now reads: pickup → pickup → resolve → dispatch-match. Pure code move, no behaviour change — proven by the unchanged 30/30 test suite, clippy (-D warnings) and fmt both clean. Also fixes two pre-existing lints that failed release-clippy: is_text_target is test-only (now #[cfg(test)]); extract_blob_domain uses an array char pattern. These predate this work but blocked the clippy gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rather than force-implement two items that shouldn't be done blind, record the decision and the concrete path for each: - ADR 0006 (#3): the big bash surface (tmux-paste-dispatch.sh, 657 lines) is the Tier-1 zero-dependency fallback BY DESIGN (ADR 0001), so folding it into the daemon is partly a non-goal — it would delete the no-daemon guarantee. Reframes #3 as bounded, test-first dedup of shim/daemon overlap, not a multi-day rewrite. First slice named. - ADR 0007 (#7): owning the Wayland clipboard via data-control on wlroots/KDE would fix the blob leak at source (no shim needed), hooking the existing WAYLAND_WEDGED latch. Deferred + guarded: untestable on the maintainer's Mutter box, and shipping an unexercisable correctness path is a liability. Test matrix specified before it can be Accepted. Both indexed in docs/adr/README.md as Proposed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two LOW findings from independent review, both on the "never drop a paste" guarantee: - kitty-focused-is-tmux.py: require is_focused IS True (was: not False). On old kitty that ignores --match and omits is_focused, no window qualifies, so we report "not tmux" and the router lets the paste happen — instead of risking a false-positive tmux match in a non-focused window that would suppress a paste kitty should have handled. Added a regression test. - kitty-paste-router.sh: only call flashpaste-trigger when the pane id is non-empty; an empty pane (no tmux) goes straight to the image fallback rather than handing the daemon a blank pane. Self-test: 17/17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… dedup
Two independent causes made a single paste land twice:
1. kitty-paste-router.sh could never reach kitty's remote-control socket.
It looked for `unix:$XDG_RUNTIME_DIR/kitty-main`, but kitty appends the
pid (`kitty-main-<pid>`), and KITTY_LISTEN_ON is absent from the env that
`launch --copy-env` copies. So `kitten @ ls` failed every time, the
focused-window-is-tmux probe always returned false, and the router never
suppressed kitty's redundant ctrl+v fire. Now resolves the socket via
KITTY_LISTEN_ON, else globs the real `kitty-main-<pid>` socket.
2. The daemon dedup missed the second fire. kitty launches the router as a
BACKGROUND process, so its paste can land well after tmux's bind already
pasted — measured 1.84s apart, past the 1000ms window. And the two paths
differ by a trailing newline ("+N" vs "+N+1 lines"), so they hashed to
different signatures and dodged content-dedup entirely. Widen the window
to 2500ms and trim trailing \n/\r in paste_signature so the variants
collide and the second is absorbed.
Verified: the daemon journal showed one keypress producing two requests
~1.8s apart with identical payload; real-wire capture confirmed flashpaste
itself delivers exactly one bracketed paste. All 15 flashpasted unit tests
pass (dedup window + trailing-newline cases added).
…om PR #1) Avoids word-splitting on socket paths with spaces and drops the ls dependency. Behaviour unchanged for the single-kitty-instance case.
5 tasks
NagyVikt
added a commit
that referenced
this pull request
Jun 10, 2026
shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array expansion and errors. Introduced with the checker in PR #1; this was the shellcheck regression that turned main's Lint job red. Braces quiet it with no behavior change (tests 6/6, live check consistent).
NagyVikt
added a commit
that referenced
this pull request
Jun 10, 2026
…etection (#2) * fix: retire kitty ctrl+v interception — XWayland breaks router tmux detection Under XWayland kitty, `kitten @ ls` reports is_focused=false for every OS window and empty foreground_processes, so kitty-paste-router.sh's focused_window_is_tmux() can never succeed. Every Ctrl+V took the slow fallthrough (background launch, seconds late under load) and the late second fire landed outside the daemon's 2.5s dedup window — journal shows identical 162-byte text pastes 4s apart (pane %23, 15:14:50 + 15:14:54). Fix: kitty must not bind ctrl+v at all. tmux's own `bind -n C-v` is the single handler (~5ms, pane id from the pressing client — also kills the wrong-pane hazard of the router's target-less `tmux display-message`). Non-tmux kitty windows: raw 0x16 reaches Claude Code, which reads the clipboard itself via the wl-paste shim; plain shells use ctrl+shift+v. - keybindings.canonical: kitty ctrl+v -> UNBOUND (new keyword) - flashpaste-keybindings-check.sh: support UNBOUND (binding must not exist) - kitty-paste-router.sh: retired-status header + clip-pipeline-log tracing (it was the only paste path with zero logging) - tests/keybindings-check.test.sh: cover UNBOUND pass/violation + keep positive-rule and literal-+ coverage via fixture canonicals * fix: SC1087 in keybindings-check — brace ${key_re} before [[:space:]] shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array expansion and errors. Introduced with the checker in PR #1; this was the shellcheck regression that turned main's Lint job red. Braces quiet it with no behavior change (tests 6/6, live check consistent).
NagyVikt
added a commit
that referenced
this pull request
Jun 12, 2026
* fix: retire kitty ctrl+v interception — XWayland breaks router tmux detection
Under XWayland kitty, `kitten @ ls` reports is_focused=false for every OS
window and empty foreground_processes, so kitty-paste-router.sh's
focused_window_is_tmux() can never succeed. Every Ctrl+V took the slow
fallthrough (background launch, seconds late under load) and the late
second fire landed outside the daemon's 2.5s dedup window — journal shows
identical 162-byte text pastes 4s apart (pane %23, 15:14:50 + 15:14:54).
Fix: kitty must not bind ctrl+v at all. tmux's own `bind -n C-v` is the
single handler (~5ms, pane id from the pressing client — also kills the
wrong-pane hazard of the router's target-less `tmux display-message`).
Non-tmux kitty windows: raw 0x16 reaches Claude Code, which reads the
clipboard itself via the wl-paste shim; plain shells use ctrl+shift+v.
- keybindings.canonical: kitty ctrl+v -> UNBOUND (new keyword)
- flashpaste-keybindings-check.sh: support UNBOUND (binding must not exist)
- kitty-paste-router.sh: retired-status header + clip-pipeline-log tracing
(it was the only paste path with zero logging)
- tests/keybindings-check.test.sh: cover UNBOUND pass/violation + keep
positive-rule and literal-+ coverage via fixture canonicals
* fix: SC1087 in keybindings-check — brace ${key_re} before [[:space:]]
shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array
expansion and errors. Introduced with the checker in PR #1; this was the
shellcheck regression that turned main's Lint job red. Braces quiet it
with no behavior change (tests 6/6, live check consistent).
* add
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Single paste gesture landed twice. Causes: (1) kitty-paste-router.sh probed the wrong kitty socket (kitty-main vs real kitty-main-; KITTY_LISTEN_ON absent under launch --copy-env) so it never suppressed kitty's redundant fire; (2) daemon dedup window (1000ms) < measured 1.84s gap between tmux's immediate paste and the kitty router's background paste, and the two differ by a trailing newline so signatures didn't match. Fix: router globs real kitty-main- socket; dedup 1000->2500ms + paste_signature trims trailing newlines. Verified user-confirmed fixed; 15/15 unit tests pass.