I work across a MacBook Pro and a Windows workstation. Different machines, different operating systems, no shared clipboard. Usually I connect via RustDesk—copy-paste works great. But my Windows dev machine started acting up. Blue screen after blue screen. RustDesk became unreliable.
Fell back to my NanoKVM Pro. Hardware KVM, rock solid—but no clipboard sync. Tried TightVNC. Connected with macOS’s built-in VNC client. Still no clipboard. I’m sitting there, retyping URLs and paths between machines like it’s 1998.
This is Drift—a clipboard sync tool that creates instant copy-paste between macOS and Windows. Named after the neural handshake in Pacific Rim: two systems, one shared mind. (I keep a naming theme across my projects—Holly, Bishop, Drift. Drift fit: brain-meld between macOS and Windows.) Copy on Mac, paste on Windows. Copy on Windows, paste on Mac. Encrypted.
Why This Exists
The problem: Clipboard doesn’t sync between macOS and Windows. Apple’s Universal Clipboard only works within their ecosystem.
Existing solutions didn’t fit:
- Cloud clipboard services: Require accounts, send data to third parties, monthly fees
- KVM switches: Hardware solution for a software problem, expensive
- Remote desktop: Overkill for just clipboard, adds latency to everything
- Manual workarounds: Email, Slack, shared files—friction every single time
The gap: A simple, local-network-only clipboard sync with no cloud dependency and proper encryption.
The goal: Copy-paste works between my Mac and Windows PC. That’s it.
What Problems Needed Solving?
Cross-Platform Protocol Design
Two different languages (Swift on macOS, Go on Windows) need to speak the same protocol. Designed a simple TLV (Type-Length-Value) format:
┌──────────┬──────────────┬─────────────────────┐
│ Type │ Length │ Data │
│ 1 byte │ 4 bytes BE │ N bytes │
└──────────┴──────────────┴─────────────────────┘
Message types: TEXT (0x01), IMAGE (0x02), HELLO (0x10), WELCOME (0x11), PING (0xFE), PONG (0xFF). Later added pairing and encryption messages. Simple enough to debug with Wireshark, can push 10MB images without choking.
The Infinite Loop Problem
Clipboard sync has a notorious bug: infinite loops. Mac copies “hello” → sends to Windows → Windows sets clipboard → Windows clipboard change fires → Windows sends “hello” back → Mac sets clipboard → Mac clipboard change fires → infinite loop.
Solution: SHA-256 hash tracking. Before sending, compute hash of content. If it matches what we just received (lastReceivedHash), don’t send—it’s an echo. Both sides implement identical logic.
Security: PIN Pairing and Encryption
The original plan had “no encryption, plaintext TCP, relies on network security.” That felt wrong. I copy passwords. I don’t want that flying over the network in plaintext, even on my home LAN. Minimum possible chance of interception.
Added: 6-digit PIN pairing with ChaCha20-Poly1305 encryption.
Windows generates a random PIN on startup, displays it in the system tray. Mac user enters the PIN when adding a machine. Both sides derive a shared secret via HKDF-SHA256, then all clipboard data flows encrypted. PIN is one-time use—cleared after successful pairing.
Two Platforms, Two Languages
macOS app: Swift with SwiftUI, using Network.framework for TCP and CryptoKit for encryption.
Windows daemon: Go with systray for system tray and golang.org/x/crypto for ChaCha20-Poly1305.
Both implementations need identical protocol encoding, identical key derivation, identical encryption. Any mismatch = decryption fails = no sync.
The Plan
Here’s the implementation plan I gave Claude. The original plan was simpler—encryption came later when I realized plaintext clipboard sync felt sketchy.
Plan: drift-project-plan.md
Drift Implementation Plan
Overview
Build a lightweight clipboard synchronization tool between macOS (client) and Windows (server). Mac initiates connection, both sides monitor clipboard and sync changes bidirectionally over TCP.
Goals
- Transparent sync - Copy on Mac, paste on Windows (and vice versa)
- Text and images - Support both content types
- No cloud - Direct TCP connection on local network
- Encrypted - ChaCha20-Poly1305 with PIN-based pairing
- Minimal UI - Menubar on Mac, system tray on Windows
Architecture Decisions
Protocol: TLV over TCP
- TLV format over raw TCP: Simple, debuggable, extensible
- Big-endian lengths: Matches network byte order convention
- 10MB max message: Handles large images without chunking complexity
Client/Server Model
- Mac is client: Initiates connection to Windows
- Windows is server: Listens on port 9847
- One connection at a time: Simplifies encryption state management
Encryption: ChaCha20-Poly1305
- ChaCha20-Poly1305 over AES-GCM: Faster in software, no timing attacks
- HKDF-SHA256 for key derivation: Standard, well-tested
- Counter-based nonces: No nonce reuse, simple increment
PIN Pairing
- 6-digit PIN: ~1M combinations, acceptable for one-time use
- PIN + ephemeral keys: Both sides contribute randomness
- One-time use: PIN cleared after successful pairing
Implementation Tasks
Task 1: Protocol Layer
- Define message types (TEXT, IMAGE, HELLO, WELCOME, PING, PONG)
- Implement TLV encode/decode in both Swift and Go
- Add pairing messages (PAIR_REQUEST, PAIR_ACCEPT, PAIR_REJECT)
- Add encrypted messages (ENCRYPTED_TEXT, ENCRYPTED_IMAGE)
Files:
Drift-macOS/Drift/Network/Protocol.swiftdrift-windows/internal/protocol/protocol.go
Verification: Unit tests for encode/decode round-trips
Task 2: Clipboard Monitoring
- macOS: Poll NSPasteboard.general every 250ms
- Windows: Use golang.design/x/clipboard watch channels
- Track changeCount to detect modifications
- Support text and PNG images
Files:
Drift-macOS/Drift/Clipboard/ClipboardMonitor.swiftdrift-windows/internal/clipboard/clipboard.go
Verification: Console logs show clipboard changes
Task 3: Loop Prevention
- Compute SHA-256 hash of clipboard content
- Track lastSentHash and lastReceivedHash
- Skip sending if hash matches lastReceivedHash
Verification: Rapid copy-paste doesn’t cause network storm
Task 4: Network Layer
- macOS: NWConnection TCP client with reconnection
- Windows: net.Listen TCP server accepting one client
- Keepalive: PING/PONG every 5 seconds, timeout after 15
Files:
Drift-macOS/Drift/Network/DriftClient.swiftdrift-windows/internal/server/server.go
Verification: Connection survives network hiccups
Task 5: Crypto Layer
- Implement ChaCha20-Poly1305 encrypt/decrypt
- HKDF key derivation with info strings
- Counter-based nonce generation
Files:
Drift-macOS/Drift/Security/CryptoManager.swiftdrift-windows/internal/crypto/crypto.go
Verification: Cross-platform encrypt/decrypt works
Task 6: Pairing Flow
- Windows: Generate PIN + salt on startup, display in tray
- Mac: PIN field in “Add Machine” dialog
- Exchange ephemeral keys, derive shared secret
- Store pairing for encrypted reconnection
Verification: Pairing completes, subsequent connections encrypted
Task 7: UI
- macOS: SwiftUI menubar app with settings popover
- Windows: systray with PIN display and status
- Machine list with pairing status (lock icon)
Files:
Drift-macOS/Drift/Views/SettingsView.swiftdrift-windows/internal/tray/tray.go
Verification: UI shows connection state and pairing status
Critical Files
drift/
├── Drift-macOS/
│ ├── Package.swift
│ ├── Makefile
│ └── Drift/
│ ├── App/
│ │ └── AppDelegate.swift # Menubar controller
│ ├── Models/
│ │ ├── Machine.swift # Machine with sharedSecret
│ │ └── Config.swift # Configuration manager
│ ├── Network/
│ │ ├── Protocol.swift # TLV encode/decode
│ │ ├── DriftClient.swift # TCP client + pairing
│ │ └── ConnectionManager.swift # Connection lifecycle
│ ├── Clipboard/
│ │ └── ClipboardMonitor.swift # Clipboard polling
│ ├── Security/
│ │ └── CryptoManager.swift # ChaCha20-Poly1305
│ └── Views/
│ └── SettingsView.swift # SwiftUI settings
│
└── drift-windows/
├── go.mod
├── build.bat
├── cmd/drift/
│ └── main.go # Entry point, PIN generation
└── internal/
├── config/
│ └── config.go # Config with pairings
├── protocol/
│ └── protocol.go # TLV encode/decode
├── server/
│ └── server.go # TCP server + pairing
├── clipboard/
│ └── clipboard.go # Clipboard monitor
├── crypto/
│ └── crypto.go # ChaCha20-Poly1305
└── tray/
└── tray.go # System tray with PIN
Verification
Protocol Tests
# macOS
swift test # Protocol encode/decode tests
# Windows
go test ./internal/protocol/...
End-to-End Test
- Start Windows daemon (note PIN in tray)
- Launch Mac app, add machine with IP and PIN
- Copy text on Mac → should appear on Windows clipboard
- Copy text on Windows → should appear on Mac clipboard
- Verify lock icon shows “Paired & Encrypted”
Trade-offs
ChaCha20-Poly1305 vs AES-GCM
- Chose ChaCha20: Faster in software, no hardware dependency
- Trade-off: Less common than AES
- Benefit: Simpler implementation, no timing attacks
TCP vs UDP
- Chose TCP: Reliable delivery, ordered messages
- Trade-off: Slightly higher latency than UDP
- Benefit: No packet loss handling, simpler protocol
Polling vs Notifications
- Chose polling (250ms) for clipboard monitoring
- Trade-off: Slight delay, uses some CPU
- Benefit: More reliable than notification APIs across platforms
How Claude Code Actually Worked
The plan shows clean phases. Reality had more back-and-forth, but surprisingly little.
Started with the protocol layer. This was the part I was most worried about—two languages that have never talked to each other need to encode and decode messages identically. Claude wrote both implementations in parallel. I’d run a test on the Swift side, encode a message, print the hex bytes. Then run the same test on Go, compare. Any mismatch showed up immediately.
The big-endian thing caught me off guard. I’d assumed “write a 32-bit integer” would just work the same everywhere. Nope. Swift uses host byte order by default. Go’s binary.BigEndian is explicit. First test produced garbage because the lengths were byte-swapped. Quick fix once I understood the problem.
Clipboard monitoring was quick. Loop prevention worked first try—the algorithm is simple enough that there’s nothing to get wrong. Then lunch.
The encryption phase took the most time. Not the crypto itself—both CryptoKit and golang.org/x/crypto have ChaCha20-Poly1305 ready to go. But key derivation had to match exactly. Same salt, same info string (“drift-pairing-v1”), same byte ordering for the ephemeral keys. One character typo would mean Mac and Windows derive different keys, and you’d never know why decryption failed.
I kept double-checking the HKDF parameters. Print the intermediate values on both sides. Compare byte-by-byte. Paranoid? Maybe. But crypto bugs are silent—either it works or it doesn’t, with no helpful error messages.
UI came last. SwiftUI for the Mac settings view, systray for Windows. Borrowed the dark “Jaeger” theme from Lock (another project)—fitting since both apps use the same naming convention.
Timeline:
- Protocol + clipboard: ~2 hours
- Network layer + reconnection: ~1 hour
- Lunch and family time: ~1 hour
- Crypto + pairing: ~2 hours
- UI polish: ~1 hour
Most of the work was getting the two platforms to agree, not the implementation itself.
Fun Challenges Encountered
Big-Endian Byte Order Mismatch
First attempt at protocol encoding: Swift used host byte order, Go used big-endian. Messages parsed on the wrong platform showed garbage lengths.
Root cause: UInt32(data.count) in Swift doesn’t specify byte order. Go’s binary.BigEndian.PutUint32 does.
Fix: Explicit .bigEndian in Swift:
var length = UInt32(message.data.count).bigEndian
Lesson: Network protocols need explicit byte order. Always.
Nonce Counter XOR Off-by-One
First encryption test: Mac could decrypt its own messages, Windows couldn’t decrypt Mac’s messages. Same key, same algorithm, different results.
Root cause: Nonce generation XORed the counter into different byte positions. Swift started at byte 4, Go started at byte 0.
Fix: Both implementations XOR into bytes 4-11:
// Swift
for i in 0..<8 {
nonceBytes[4 + i] ^= ptr[i]
}
// Go
for i := 0; i < 8; i++ {
nonce[4+i] ^= counterBytes[i]
}
Lesson: Crypto implementations must match exactly. “Close enough” doesn’t decrypt.
Windows Tray Icon Embedding
Go’s systray library needs an embedded icon. First build had no icon—Windows showed a blank tray space. Quick fix: the //go:embed icon.ico directive needs the file in the same package directory, not the module root. Moved the file, done.
Pairing Proof Verification Race
First pairing test: Mac paired successfully, but reconnection failed. The pairing proof (first 16 bytes of SHA256(sharedSecret)) wasn’t being verified.
Root cause: Windows stored pairings by machine name, but the Mac’s name came from Host.current().localizedName which could return nil or different values between sessions.
Fix: Consistent machine identification. Mac sends its hostname in HELLO, Windows stores pairing against that exact string.
Lesson: Distributed systems need stable identifiers. “What’s my name?” isn’t a simple question across platforms.
macOS Network Entitlements
Release build crashed on launch with “killed by signal 9.” Debug build worked fine.
Root cause: Missing network client entitlement for outgoing connections. macOS sandboxing kills apps that try to open sockets without permission.
Fix: Added to Drift.entitlements:
<key>com.apple.security.network.client</key>
<true/>
Lesson: macOS sandboxing is strict. Entitlements must match capabilities.
Results
Build time: One day (~6 hours active development, plus lunch)
First successful sync: Copied a URL on my Mac, switched to Windows via KVM, Ctrl+V—it pasted. Finally. No more retyping. No more emailing myself links. Just… copy-paste, the way it should work.
Already using it: This is now my daily workflow. Both machines run Drift at startup. I don’t think about clipboard sync anymore—it just works.
What it handles:
- Text: Any size, any encoding (UTF-8)
- Images: PNG up to 10MB (screenshots work great)
- Reconnection: Automatic with exponential backoff
- Encryption: ChaCha20-Poly1305, session keys per connection
What it doesn’t handle:
- Files: Not implemented (would need temp file staging)
- Multiple machines: Can configure many, but only one active connection
- Discovery: Must manually enter IP addresses
Limitations:
- Windows must be reachable from Mac (same network, or port forwarding)
- PIN pairing requires physical access to see the Windows tray
- No iOS/Android (desktop only)
What’s next: Nothing. It works. Mac and Windows share a clipboard now. That was the goal.
Tech stack: Swift (macOS), Go (Windows), ChaCha20-Poly1305, HKDF-SHA256 Build time: 1 day Lines of code: ~2,500 (Swift: ~1,400, Go: ~1,100)