Repository navigation
The clipboard both ways: Share Clipboard on the Mac and the device, Paste on the Mac (#64) - #78
Merged
Merged
Conversation
The wire (kind 32, WindowList.clipboard), the rules both ends share (ClipboardPolicy), the host's side (HostClipboard, Share clipboard with devices, --no-clipboard), the device's (Share Clipboard, the Paste chip built on the system Paste control), the open questions with the default taken for each, and the test plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ardPolicy
Kind 32, both ways, JSON Clipboard {text, image, stop} (Clipboard.swift): pull request #66's number
and shape, image reserved for a later generation, and stop, a device's switch going off. The
window list's clipboard (an Int, 1 for text): a device sends kind 32 only to a host that says it;
an older peer skips the kind. Text at most 256 KB of UTF-8, the JSON at most 1 MiB either way.
ClipboardPolicy (pure, both ends): what a device's message asks of the host; which change of the
Mac's pasteboard the host sends (never its own write, never an item marked concealed or transient
by a password manager or another app, whose text is never read); who a device's text goes to; the
rate; the device's subscription; which text from the Mac it writes; when the chip offers the
device's clipboard; every word.
Tests/checks/clipboard: 189 checks with two random models, 47 of 47 mutants, in CI's mutants
matrix. protocol, compatibility and pointer-control read kind 32 (192, 102 and 157 checks);
compatibility's mutants follow WindowList's new parameter (22 of 22).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evices, --no-clipboard A device subscribes with any kind 32 and stops with stop, one line each, never one per copy and never what was copied. While one subscribes the host looks at its pasteboard's change count four times a second (a Task.sleep loop: the CLI's dispatchMain runs no RunLoop timer) and sends a change ClipboardPolicy takes to every subscriber; what was on the clipboard when the first subscribed is never sent. A device's text is written for this Mac only (.currentHostOnly), marked with this launch's ID, and forwarded to the other subscribers; the look never sends it again. At most four kind 32 a second per connection; the rest, text over 256 KB and what does not decode ignored with one line a minute. Stats clip.sent and clip.recv. HostConfig.shareClipboard, on in standard: Sill.app's Settings > General "Share clipboard with devices" (saved as shareClipboard), the CLI's --no-clipboard; the Mac's alone, applied at once by setTarget (every subscription dropped, the lists sent again). A synthetic host shares a pasteboard of its own (SILL_TEST_PASTEBOARD, TEST ONLY), never this Mac's clipboard; SILL_TEST_CLIPBOARD_LOG=1 prints each decision with sizes, never text. away-quality: shareClipboard in standard and changes(to:), never moved by a device (71 checks, 19 of 19 mutants). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lipboard The clipboard's test client flags: the subscription, a device's text (escapes decoded, or N bytes), a literal kind 32, each kind 32 printed with its size and text, the window list's clipboard=, and the last kind 32's text checked at exit (clipboard=none: none came). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Mac through the system's Paste control StreamClient+Clipboard: an empty kind 32 at the session's first window list that says the Mac shares its clipboard (and on a move's new connection) while this device's switch is on; stop when it goes off. The Mac's text is written to UIPasteboard with .localOnly and a 10-minute expiration, its change count kept as handled. Sill never reads the clipboard by itself: when it becomes active (or a session starts within 15 s of that) and the clipboard's change count is not the handled one while it has text (changeCount and hasStrings, which never ask), the chip offers it. ClipboardChip: "Paste on <Mac>", then press Command-V there, with the system's Paste button (PasteButton, UIPasteControl: the person's tap reads the clipboard without iOS asking) and Dismiss; "Sent to <Mac>'s clipboard" or "Too long to send" for 3 s after. Under the bar sideways (its bottom corner while the link's line shows), above the window bar or row 1 upright; hidden while something is open over the stream. VoiceOver: one label for its words, Paste with a hint, Dismiss. The Settings panel's This iPad group: Share Clipboard (Sill.shareClipboard, on by default) while the Mac shares it, Paste on <Mac> under it, a footnote. The harness: the mock Mac shares its clipboard in every case but legacy; -SillClipboardChip 1 and -SillClipboardNote sent|long; the mock's chip never follows the simulator's clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne look per activation Found while building: an item another of the person's devices put on the Mac through Universal Clipboard (com.apple.is-remote-clipboard) is fetched from that device when read, and macOS shows it pasting from there, for every copy made on the iPhone. The host now skips it from its types alone, as it does a password manager's; those devices have it already. The check's model and one more mutant cover it. The device: the app's and its scene's activation come together, so they make one look. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tore's next version, CLAUDE.md) site/privacy.html: what travels each way (what you copy on the Mac while Share Clipboard is on, never a password manager's concealed copies; the device's text only when Paste is tapped, never read by Sill on its own), what stays on the device (the switch, the change count, the Mac's text on its clipboard for 10 minutes, not passed on by Universal Clipboard), the log never holding what was copied; October 14, 2026. site/support.html: copy and paste both ways. README: How it works and Good to know. DEVELOPMENT.md: the setting, --no-clipboard, the test hooks and the test client's flags. docs/app-store-metadata.md: a description bullet waiting for the next submission, and the App Privacy notes. CLAUDE.md: the Current step entry, the compatibility floor (kind 32 and the list's clipboard), Layout, Build and run. The plan's Results, Untested for Noah and Progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for its contents Found live: a look between an app's clear of the pasteboard (which moves the change count) and its write (which does not) took the count as dealt with, and the copy was never sent: a probe writing 50 to 550 ms after its clear lost 9 of 12 copies against the build before, none now (ClipboardPolicy.hostWaits: a pasteboard with no items is looked at again for up to a second). The pasteboard is now read and written on a queue of its own (HostClipboard.Board), one call at a time and in order, so a slow read (an app asked for text it only promised) never holds up the main actor, where devices' input is handled. A device's write is done before any later look, which finds it as this host's own. Tests/checks/clipboard: the settle rule's cases, an app that clears, is looked at and then writes in the host model, and two more mutants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on, the header up to date Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… CI) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity
added a commit
that referenced
this pull request
Oct 14, 2026
…uests The plan's Progress and the Current step entry: CI's Build and check on 807c8ef (run 38489635350), the private simulators deleted, and the merge order: the pull request merges into main without a conflict; with #74, #75, #77, #78 and #79 it meets in the docs, and with #79 also in StreamScreen.swift's DEBUG hooks (keep both). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that HostClipboard applied the 4-a-second rate to every
kind 32 before reading what it asked, so a {"stop":true} that was the fifth
message in a second (the switch flipped off, on, off, on, off) was refused
as "rate": the connection stayed subscribed and every later Mac copy still
went to a device whose Share Clipboard was off.
ClipboardPolicy.hostReceives now decides what the message asks first, then
applies the rate to subscriptions and texts alone (countsAgainstRate): a stop
is always taken, and neither a stop nor a refusal counts against the rate.
Refusal gains `rate`, so the ignored line's counts are the refusals' names.
Tests/checks/clipboard: the review's sequence and the rate's edges (209
checks), and model 1b, 5,000 quick sessions of switch flips, Pastes and the
Mac's setting through hostReceives: never subscribed at the host while the
device's switch is off. Five new mutants (the review's ordering among them),
each caught.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that Sill.app read NSPasteboard.general at every copy while a device shared the clipboard, and never checked macOS's paste privacy (NSPasteboard.accessBehavior, macOS 15.4 and later). NSPasteboard.h says the general pasteboard asks on every programmatic read by default once that is on: each Mac copy would bring macOS's alert, and the read would wait for the answer; denied, every copy was skipped as "no text" with nothing said. ClipboardPolicy.MacAccess (allowed, notAsked, asks, denied, from the raw value; a value this build does not know counts as asking): the host reads only while macOS allows every read (hostLooks). Otherwise HostClipboard's look reads nothing, not even the change count, and when macOS allows reads again the count is taken as seen, so nothing copied meanwhile is sent (accessStep). One line when it changes (accessLine), never at every look. A device's text is still written: writing needs no leave. Settings › General: ClipboardSection, with an orange line under the switch while macOS asks or denies, what to do, and "Allow in System Settings…" (Privacy & Security › Paste from Other Apps, Privacy_Pasteboard). While macOS has not asked about Sill yet it is not listed there (NSPasteboard.h), so the button first makes one read on the person's click (MacClipboard.askOnce, off the main thread, the text dropped), then opens the pane. PermissionsModel reads the state with the permissions. Previews: pane-clipboard-* in each state. TEST ONLY SILL_TEST_CLIPBOARD_ACCESS (a synthetic host): a state, or a file holding one, read at each look. Tests/checks/clipboard: the mapping, the steps, the lines and the words (245 checks), and model 2 with macOS's answer changing (copies made while it asks or denies never sent). Ten new mutants, each caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e does The review found that Settings › General's footnote said "A password manager’s copies stay on this Mac", where only copies marked as concealed (or with ClipboardPolicy's other markers) do, which the plan itself cannot promise for Apple's Passwords app; and "and theirs here" read as if a device's copies came by themselves, where they come only with Paste. The support page ended a paragraph about both directions with "Sill never reads your clipboard by itself", which Sill for Mac does at every copy while a device shares it. The footnote (now ClipboardPolicy.Copy.macFootnote, checked): "Text you copy here can be pasted on a connected iPhone or iPad, and text sent from one with Paste becomes this Mac’s. Copies a password manager marks as concealed stay on this Mac." support.html and privacy.html: "Sill never reads your iPhone or iPad’s clipboard by itself" / "your device’s clipboard". A mutant puts the old promise back, caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…takes one
The review found that the floor, which holds for good, froze the
subscription as {} and called `image` reserved for a later generation without
saying that a host sends an image only to a device that asked for images. Pull
request #66's host sends {text, image} to every subscriber, so once it
rebases onto this shape every image copied on the Mac would travel to every
iPhone and iPad of this build, which drop it (about 3.5 MB of base64 at #66's
cap, seconds of an away link).
CLAUDE.md's floor, Clipboard.swift and the plan now say it: a subscription
says what the device takes, {} being text alone, and a host sends `image` only
to a connection whose subscription carries a later field saying it takes
images ("accepts":2, say). No code changes: this generation's host never
sends an image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that a Paste said "Sent to ‹Mac›’s clipboard" and counted the copy as dealt with the moment kind 32 was handed to SessionLink. The chip's main moment is Sill coming back from another app, when the connection may be dead without its end read yet (the Mac evicts a suspended device; a network change or a sleeping Mac sends no FIN, and liveness counts from the foreground for up to 6 s): the text was lost, the person told it went, and the chip never offered that copy again. Now a Paste sends a ping after its text through the same door (SessionLink, so on the same connection and after it, wherever a move takes what waits), with a 16-byte random payload (ClipboardPolicy.receiptLength: never a ping's 8 bytes of time, nor a fence's 8). The Mac echoes a ping only once it has read all before it, so its pong is the receipt: then "Sent…", the success haptic, and the copy dealt with (clipboardReceiptCame). No receipt in max(4 s, 4 × the worst round trip) (receiptWait, as a settings pick waits): "Didn’t reach ‹Mac›. Try Paste again.", the copy not dealt with, and the chip offered again; a receipt that comes later still counts. No wire change: hosts echo any ping, and only a host that shares the clipboard is sent one. DEBUG: -SillSettingsScript gains `paste TEXT` (sendClipboard as the Paste control hands TEXT over, under the script's guard: a loopback test host only); -SillClipboardNote notsent. Tests/checks/clipboard: the receipt's length and wait, the note (250 checks); three new mutants, each caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the chip came without an announcement and its 20 s timer ran whatever covered it: under the Settings panel, the Apps list, the tour (on a fresh install the first session offers what is on the clipboard and the tour covers it a second later), a thumbnail's lights or the Aa ruler, it ran out unseen and counted the copy as dealt with for good; and someone finding it with VoiceOver had 20 s. ClipboardPolicy.chipCounts: the time runs only while the chip itself is on screen (StreamScreen.clipboardChipOnScreen: nothing open over the stream, no note in its place), Sill is active, and VoiceOver is off; hidden it waits and then shows for what is left, and with VoiceOver on it stays until Paste or Dismiss. StreamClient keeps the time spent (updateClipboardChipClock), from the screen's word (clipboardChipShown), willResignActive, didBecomeActive and VoiceOver's status. The first time the chip shows for a copy, VoiceOver is told: "Paste on ‹Mac› is available, near the bar." DEBUG: -SillSettingsCloseAt <s> closes the panel -SillSettings 1 opened. Tests/checks/clipboard: the rule's eight cases and the words (259 checks); three new mutants, each caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the clipboard's support section, its privacy lines and the privacy page's date land on main ahead of the release, and a republish for another fix before it (#69's lines on a sleeping or restarted Mac are queued, and hold for the builds already out) would publish them for apps that do not have the feature. The release checklist already republishes the site only after the release whose builds a change describes; the words stay with this pull request as the other features' do, now between `<!-- Clipboard: …` and `<!-- /Clipboard -->` (support.html's section, privacy.html's three items), as the Remote Access blocks are. Part 1 §3 says how a republish before that release cuts them (the perl line beside the rsync, privacy.html's date put back), and that the release deletes the markers. Checked on a copy: after the cut the pages differ from main's only in the date and in words true of the builds out too ("what you copy" in the privacy lede, the log lines' "never what you copy" and "which devices share the clipboard"), and every page's tags and marker pairs balance. support.html also gains the line for macOS's paste privacy: if what you copy on the Mac doesn't arrive, Settings › General says why and opens Paste from Other Apps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the chip's time counted only while it shows (the review's fix), a session that ended with the chip on screen left clipboardChipOnScreen true until the next stream screen said otherwise, so a chip offered at the next session's first window list could start its time for a moment before that screen's first word. resetClipboard now sets it false with the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan's design and open questions now carry the review's changes (the rate a stop never meets, macOS's paste privacy, a Paste's receipt, the chip's time and VoiceOver's announcement, the site's words marked for the release), and open question 1 says what on-by-default does to the device's clipboard, for Noah. Results has "The review", each of the eight findings with what was reproduced, what changed and how it was checked, and the final numbers: `Tests/checks/clipboard` 259 checks and 72 of 72 mutants, run-all all 41, the host and the iOS app three ways from git archive of a371cb6 with only the known warnings, H2 against main again, the live runs on loopback and on a private simulator, and the photos (byte for byte the reviewed build's but the new note). Untested gains macOS's paste alert through the developer preview, a Paste right after an app switch, and on-by-default's effect. CLAUDE.md's entry, Layout and harness lines; DEVELOPMENT.md's clipboard section (the receipt, the chip's time, macOS's paste privacy and how to try it, SILL_TEST_CLIPBOARD_ACCESS, `paste` and -SillSettingsCloseAt); the checks' README row (259 checks, 72 mutants). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lovelymammoth
pushed a commit
to lovelymammoth/sill-plus
that referenced
this pull request
Oct 14, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcmEpeimxxkvfR2ACj1uAo
Code-Tomato
added a commit
to Code-Tomato/sill
that referenced
this pull request
Oct 15, 2026
PR Saffsanity#66 (Sill Viewer, with Saffsanity#72 and Saffsanity#73) numbers 29 to 42, its own audio at 33, and Saffsanity#78 takes 32. 43 is the next number no open pull request holds. Every comment, script, kind check and mutant that named 29 for the sound now names 43; the payloads are unchanged. docs/audio-plan.md, "Kind 43 (2026-10-14)", has the survey and the results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md keeps every Current step entry, this branch's first; ci.yml's matrix and the checks' README take every check; README, DEVELOPMENT.md, privacy.html and support.html keep both sides' sections; HostSettingsPanel's This iPad group has Larger Picture, then Share Clipboard, then the gestures; ContentView's harness contract both sides' arguments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main's paragraph still said no device had run Esc or the trackpad; the iPad did on 2026-10-15 (docs/hardware-keyboard-plan.md, Results). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 15, 2026
This was referenced Oct 15, 2026
ashleigh-hopkins
added a commit
to ashleigh-hopkins/sill
that referenced
this pull request
Oct 15, 2026
ashleigh-hopkins
added a commit
to ashleigh-hopkins/sill
that referenced
this pull request
Oct 15, 2026
… the clipboard's kind 32 negotiation and the display teardown work Conflicts kept both sides: the pill's comment has the bolt and the refresh that restarts captures; the protocol mutants list both sides' cases; CLAUDE.md's wire list has the Dock bounces and full-speed flags beside the clipboard's subscription; the plan keeps this branch's notifications line and mac-client's merge notes.
ashleigh-hopkins
added a commit
to ashleigh-hopkins/sill
that referenced
this pull request
Oct 15, 2026
…anity#78, the clipboard's kind 32 negotiation and the display teardown work Side streams keep this branch's capture plan (StreamRate, the crop's pill after the encoder) with mac-client's rule while display streams deliver nothing: no full-screen display capture and no crop with its app, the window alone. CLAUDE.md's wire list has the paused and background requests beside the clipboard's subscription; the checks list and README take both sides (protocol 249 + 8, 41 mutants).
Code-Tomato
added a commit
to Code-Tomato/sill
that referenced
this pull request
Oct 18, 2026
… side Main brought PRs Saffsanity#74, Saffsanity#75, Saffsanity#77, Saffsanity#78, Saffsanity#79, Saffsanity#80, Saffsanity#85 and Saffsanity#98. 19 files conflicted; each kept both sides whole. - Wire: kind 32 (the clipboard) beside 43 (the sound); 29-31 and 33-42 stay free for Saffsanity#66. - Settings: shareClipboard beside sendAudio in HostConfig, the app's defaults, the CLI (--no-clipboard, --audio) and sillclient.py. - Larger Picture (Saffsanity#79): row 1 holds Sound before Settings from 384 pt (PhonePortraitLayout.row1Beside); narrower, Sound is the panel's switch. - Saffsanity#75: a regular-mode window that streams on off the catalog's list keeps its sound (StreamCoordinator.streamedPID); before, it stopped. - Docs and check labels that still called 29 the audio plan's, or said 32 was free, now say what the merged tree does. Verified: swift build -c release; iOS Debug and Release for the generic simulator (only the known StreamClient warning); the CLI's CI paths; Tests/checks/run-all.sh, all 47; the mutants of the 25 checks that compile a file both sides changed, 936 of 936 caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity
added a commit
to kyleparisi/sill
that referenced
this pull request
Oct 19, 2026
Main moved on since this branch began at 985a733, to d1fa97e (Saffsanity#85, Saffsanity#74, Saffsanity#77, Saffsanity#75, Saffsanity#79, Saffsanity#80, Saffsanity#78, Saffsanity#98, Saffsanity#153 and Saffsanity#142). None of them touched Sources/SillHost/VirtualDisplay.swift, so the merge is clean and this pull request's change applies to main as it is now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity
added a commit
to ipavlidakis/sill
that referenced
this pull request
Oct 19, 2026
Main moved on since this branch began at 985a733, to d1fa97e (Saffsanity#85, Saffsanity#74, Saffsanity#77, Saffsanity#75, Saffsanity#79, Saffsanity#80, Saffsanity#78, Saffsanity#98, Saffsanity#153 and Saffsanity#142). None of them touched Scripts/make-app.sh, so the merge is clean and this pull request's change applies to main as it is now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HunterAhlquist
pushed a commit
to HunterAhlquist/sill
that referenced
this pull request
Oct 19, 2026
privacy.html is on main ahead of every release, and a republish before the Pencil ships (another fix's words) would tell readers the app sends their Pencil's pressure and tilt and keeps a Pencil Pressure switch, which the builds out do not. As the clipboard's words are (PR Saffsanity#78), each Pencil item now stands alone between `<!-- Pencil: …` and `<!-- /Pencil -->`: "Your Apple Pencil's strokes, with how hard you press and how you tilt it." among what the device sends, and "On an iPad, whether Pencil Pressure is on." among what it keeps. docs/release-checklist.md part 1 §3 gains the perl line that cuts them before that release, and says which Pencil lines stay after the cut (index.html's intro, terms.html's trademarks). Checked on a copy: after the Pencil's cut site/ is main's byte for byte, and with the clipboard's too `grep -i pencil` prints only those two lines. The page's date is left for the release to set, as the plan says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity
added a commit
to Code-Tomato/sill
that referenced
this pull request
Oct 20, 2026
…re drafts The PR left the public copy for the release that carries sound, but once it merges main is that release, and README, the App Store description and the privacy policy said Sill plays no sound. As the clipboard (Saffsanity#78) did: - README: How it works gains "Sound from your Mac", a beta off until Send Audio (Beta) is turned on; Good to know says Sill plays sound from your Mac only as a beta, off by default (the trademark note's wording, not "your Mac's sound"). - site/privacy.html (dated October 20, 2026): what the Mac sends while Send Audio is on, the device's report gaining how far the sound trails and the late packets, the mute kept on the device, and what Screen Recording is also for; site/support.html: "Sound from your Mac (beta)", one paragraph (turning it on, the Sound button and the panel's switch, the silent switch, headphones out, AirPods' delay). Every line inside "<!-- Sound: for the release that ships it (PR Saffsanity#94); …" markers. - docs/release-checklist.md part 1 §3: the Sound markers and their perl line. Tried on a copy: every page but privacy.html's date (and a line break in one list item, and a blank line in support.html) is main's. - docs/app-store-metadata.md: for the next version, the description's bullet (and "Sill doesn't play sound" out of Good to know: 3,174 characters), the What's New paragraph, and one review-notes line in WHAT TO TRY with a redundant sentence of PURPOSE AND AUDIENCE out (3,995 bytes of 4,000); the App Privacy answers and Before you submit name the sound. "Beta" is always on the feature, never on the app (guideline 2.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Fixes #64
Why
Text could go from the iPad to the Mac only by typing (a paste into the stream's keyboard is typed a character at a time and never reaches the Mac's clipboard), and from the Mac to the device not at all, unless both were on the same Apple Account with Universal Clipboard, which does not reach a device on another account or away from home. The triage of 2026-10-13 put #64 next, text first, and noted two problems in #66's clipboard: it sends what a password manager copies, and gives the Mac's owner no switch.
What
The plan, its open questions with the default taken for each, every result and the review's fixes are in
docs/clipboard-plan.md.clipboard, both ways, JSONClipboard {text, image, stop}: Sill Viewer: a Mac controlling another Mac #66's number and shape (imagereserved for a later generation), andstop, a device's switch going off.WindowList.clipboard: Int?, 1 for text: a device sends kind 32 only to a host that says it, an older peer skips the kind.{}is text alone: a host sendsimageonly to a subscription that says it takes images (the compatibility floor says so; Sill Viewer: a Mac controlling another Mac #66's host sends one to every subscriber, and follows this as it rebases). Text at most 256 KB of UTF-8, the JSON at most 1 MiB either way. Sill Viewer: a Mac controlling another Mac #66's list key is aBool, which does not decode as thisInt, so it changes as Sill Viewer: a Mac controlling another Mac #66 rebases (never released). Kinds 29 to 31 stay free.ClipboardPolicyin StreamProtocol, checked by the newTests/checks/clipboard(with mutants, in CI's matrix). Never sent: an item markedorg.nspasteboard.ConcealedTypeororg.nspasteboard.TransientType(password managers, one-time codes; and three older markers), one that came from another device through Universal Clipboard (reading it fetches it from there), and the host's own write. Types are judged before any data is read, so a password manager's text is never read. A device's subscriptions and texts are rated 4 a second; its stop never is.HostClipboardlooks at the pasteboard's change count four times a second while a device subscribes, and only while macOS allows every read (NSPasteboard'saccessBehavior, macOS 15.4 and later): while macOS would ask (its alert at every copy) or denies, it reads nothing, and nothing copied meanwhile is sent. New text goes to every subscriber; what was on the clipboard when the first device subscribed is never sent. A device's text is written for this Mac only (.currentHostOnly) with a marker naming this launch, and passed to the other subscribers. One log line when a device subscribes or stops, or when macOS's answer changes; none per copy, never contents;clip.sent/clip.recvin the stats line. Settings › General: "Share clipboard with devices" (on, saved asshareClipboard, applied at once), and while macOS asks or denies, why, with "Allow in System Settings…" (Privacy & Security › Paste from Other Apps). The CLI:--no-clipboard. A synthetic host shares a pasteboard of its own, never this Mac's clipboard..localOnlywith a 10-minute expiration. Sill never reads the device's clipboard on its own: when it becomes active with new text there (changeCountandhasStringsonly, which never prompt), a chip near the bar offers "Paste on ‹Mac›" with the system's Paste control (UIPasteControl viaPasteButton, which reads without a prompt) and Dismiss; its 20 s run only while it shows with Sill active, never under VoiceOver, which is told when it first shows. A Paste says "Sent to ‹Mac›’s clipboard" only once a ping sent after the text comes back (the Mac echoes a ping only after reading all before it); otherwise "Didn’t reach ‹Mac›. Try Paste again." and the chip again. Then the person presses ⌘V on the Mac.<!-- Clipboardblocks that a republish before the release cuts (docs/release-checklist.md part 1 §3); README (How it works, Good to know), DEVELOPMENT.md, the App Store description bullet waiting for the next submission, CLAUDE.md.The review, and what changed
An adversarial review of 7e6468a reran every gate (all held) and found eight things, all taken, one commit per theme:
{"stop":true}that was the fifth in a second (the switch flipped off, on, off, on, off) was refused and the Mac's copies kept going to a device whose switch was off. Reproduced against 7e6468a on loopback (the copy at 5 s reached the client); with the fix the last stop is taken and nothing goes. NowClipboardPolicy.hostReceivesdecides first and rates only subscriptions and texts.accessBehavior; NSPasteboard.h says it asks at each programmatic read by default once that is on. Now the host reads only while macOS allows every read, says so once when that changes, and Settings › General explains and opens Paste from Other Apps (first letting macOS ask once while it has not, since Sill is listed there only after that). Today's macOS reads "always allow", so nothing changes until it asks.<!-- Clipboardblocks; the release checklist says how a republish before the release cuts them.Verified
After the review's fixes (code at a371cb6):
Tests/checks/clipboard259 checks (two random models of the review's cases among them: 5,000 quick sessions of switch flips and Pastes through the rate, never subscribed at the host while the device's switch is off; three devices with macOS's answer changing, never a copy made while it asked or denied) and 72 of 72 mutants (22 new) caught, in CI's mutants matrix.Tests/checks/run-all.sh: all 41 passed (7 min 05 s). CI's "Build and check" passed on the head, 229934f (13 min 05 s).git archiveof a371cb6:swift build -c release42.9 s, only the CaptureProbe warning; the iOS app Debug and Release for the generic simulator and Debug for a generic device (CODE_SIGNING_ALLOWED=NO, arm64): BUILD SUCCEEDED (18 s, 36 s, 17 s), only the known StreamClient capture warning (StreamClient.swift:3469; main's 3430, the same code).git archive,--syntheticon loopback with the software encoder: idle 35 s side by side identical masked and sorted (7 lines); a client picking the Desktop for 15 s that never subscribes: identical in order without the[1s]lines (35 lines each), no clipboard line.SILL_TEST_CLIPBOARD_ACCESS(a file changed while a client shared): denied, asks, allowed, not asked, each with its line; copies made while it denied or asked never went, the one made after reads came back went (9.03 s), a device's text was written while not asked.-SillSettingsScript '3 paste …') gave "its receipt awaited for 4.0 s", then "the Mac has read it", and the host's pasteboard held the text with the launch's marker (also on the final build); throughsillrelay.py --blackhole-after 3, a Paste at 3 s gave "no receipt in time" and the chip again, and the host got nothing. The chip under the Settings panel: "waits, 18.3 s left", then after the panel closed "runs, 18.3 s left" and its time up; through an app switch: "waits, 14.6 s left", back "runs, 14.6 s left"; with VoiceOver's stand-in it stayed 30 s; VoiceOver told when it first showed. Harness photos of the chip (sideways, upright), a note, the Settings group and the stream: byte for byte the reviewed build's; the new note sideways and upright.pane-clipboard-*).sendClipboardthe control's tap reaches), the bare app's switch, the phone's arrangement and the Duo's poses (no layout changed). macOS's alert itself was never shown: nothing here reads the real clipboard.Untested, for Noah
On the iPad mini and the iPhone 15 Pro with this branch's builds and this Sill.app:
defaults write me.saffer.sill.mac EnablePasteboardPrivacyDeveloperPreview -bool yes, Quit Sill from its menu and open it again, connect the iPad. Settings › General shows "macOS asks before Sill reads the clipboard, so what you copy here stays on this Mac."; a copy in TextEdit brings no alert and does not arrive, and Sill.log has the line once. Click Allow in System Settings…: macOS's alert (note its words and buttons), then Paste from Other Apps with Sill listed; set Allow: the orange line goes, a copy arrives on the iPad, Sill.log says "macOS lets Sill read the clipboard again". Set Deny: Settings says so, copies stay, a Paste from the iPad still sets the Mac's clipboard. Thendefaults delete me.saffer.sill.mac EnablePasteboardPrivacyDeveloperPreviewand open Sill again.Photos are in
~/Downloads/sill-handoff/clipboard/photos/(from before the review) and itsreview/folder (the new note, Sill.app's clipboard section in each state, and the chip under the Settings panel and after it); the live ones show this Mac's windows and name.🤖 Generated with Claude Code