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. Claude chose a 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
Two languages that have never talked to each other, one shared protocol, and encryption that has to match byte-for-byte. This one could’ve gone sideways fast. It mostly didn’t.
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 knew what a TLV protocol should look like from years of working with network tools—I just couldn’t write one in Swift or Go. So I’d describe what I expected (“encode this message, print the hex bytes”), run the test on one side, then the other, and compare. I could spot when output was wrong. Claude could fix why.
The big-endian bug showed up immediately. First test produced garbage—the lengths were nonsense. I’ve hit byte order issues before with network protocols, so I knew roughly what was happening, but I couldn’t have told you whether Swift defaults to big-endian or little-endian. Claude diagnosed the specifics and added explicit .bigEndian on the Swift side.
Clipboard monitoring and loop prevention were quick—Claude had those working in maybe 20 minutes. 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. I knew enough to know this was where things would break—crypto bugs are silent, either it works or it doesn’t, no helpful error messages. The actual crypto implementation was all Claude, and it nailed it. My contribution was paranoia: I kept having Claude print intermediate values on both sides so I could compare byte-by-byte. Claude wrote the crypto. I made sure both sides agreed.
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: messages parsed on the wrong platform showed garbage lengths. I recognized the symptoms—I’ve seen byte order issues before—but I couldn’t pinpoint which side was wrong in Swift vs Go. Claude traced it to the Swift side.
Root cause: UInt32(data.count) in Swift doesn’t specify byte order. Go’s binary.BigEndian.PutUint32 does.
Claude’s 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. I reported the symptom. Claude found the cause.
Root cause: Nonce generation XORed the counter into different byte positions. Swift started at byte 4, Go started at byte 0.
Claude’s 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. Claude figured out 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. I could reproduce it reliably—pair, disconnect, reconnect, fail. Claude dug into the logs.
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.
Claude’s 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. I’d hit entitlement issues before with Lock, so I suspected sandboxing. Claude confirmed and knew exactly which entitlement was missing.
Root cause: Missing network client entitlement for outgoing connections. macOS sandboxing kills apps that try to open sockets without permission.
Claude’s 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)