Llevaba desde diciembre soportando pacientemente una de esas frustraciones cotidianas que, sin darte cuenta, se convierten en proyecto. Trabajo con dos máquinas a la vez: mi portátil con Ubuntu corriendo Wayland, y un PC con Windows que manejo desde el propio Linux usando Deskflow — la evolución libre de aquel viejo Synergy. Para teclado y ratón funciona perfecto: una sola entrada controla las dos pantallas. Pero el portapapeles no se sincroniza. Y resulta que Deskflow, que sí lo hace en X11, aún no es compatible con Wayland.

Probé las alternativas obvias: KDE Connect y algún otro tinglado parecido. Pero tan pronto funcionaban como dejaban de hacerlo. Un día sincronizaba bien, al siguiente no, y al siguiente había que reiniciar algo o reemparejar los dispositivos. Para una herramienta que tiene que ser invisible y estar siempre lista, esa inconsistencia es peor que no tenerla.
El baile feo era siempre el mismo: copio algo en Linux, abro Google Chat, me lo envío a mí mismo, salto a Windows, lo pego desde Google Chat allí. O al revés. Un disgusto cada vez. Para cualquiera que use dos sistemas a la vez, la sincronización del portapapeles es de esas cosas que, cuando no las tienes, te das cuenta de que las dabas por hechas.
En foros encontré una alternativa que prometía: ClipCascade. Cliente para Linux, cliente para Windows, un servidor central que les hace de concentrador. Justo lo que buscaba. Lo abrí dispuesto a instalarlo… y vi que era una aplicación Java sobre Spring Boot. El servidor recomendaba medio giga de heap mínimo. Para sincronizar un trozo de texto entre dos máquinas de mi propia red.
Mi reacción fue inmediata: no, gracias. No voy a tener un proceso comiendo medio gigabyte de RAM permanente solo para que un Ctrl+C en una máquina llegue a la otra. Cerré el navegador y abrí Claude Code.
«Vamos a convertir esto a Rust»
Mi prompt inicial fue, prácticamente literal: «Vamos a convertir esta aplicación a Rust, manteniendo la idea pero más ligera. Quiero un solo binario que pueda hacer de cliente y servidor». Le pasé el repositorio de ClipCascade y le di vía libre.
Yo nunca había escrito una línea seria de Rust. Había leído algún capítulo del libro, lo había mirado de reojo en alguna tarde de curiosidad, pero nada de producción. Lo elegí porque cumplía dos requisitos innegociables para este caso de uso: binario único, sin runtime que cargar, y un lenguaje de memoria segura. Si voy a tener un proceso escuchando en un puerto de mi red, prefiero que cualquier descuido acabe en un panic y no en un buffer overflow.
Le indiqué también una cosa importante desde el principio: Wayland tenía prioridad sobre X11. La mitad de los problemas con aplicaciones de bandeja en Linux vienen de asumir que estamos todos todavía en X11. No quería arrastrar esa deuda en mi propio código.
El ritmo: prompts en mi background
Trabajar con Claude Code en esto tuvo un ritmo concreto que merece la pena contar, porque no es como yo me imaginaba que funcionaría. No fue una sesión de horas pegado a la pantalla. Fue más como tener un becario silencioso al que dejas una tarea y vuelves al rato a ver qué ha hecho.
Le daba una indicación de alto nivel, dejaba que se pusiese, y mientras tanto yo seguía con mi trabajo de cliente, o me iba a comer, o atendía un correo. Cada cierto rato me acercaba a la consola, leía el diff de lo que había hecho, le decía por dónde tirar a continuación, y lo dejaba otra vez. Sesiones cortas de revisión, separadas por bloques largos en los que él construía y yo hacía otra cosa.
En un par de jornadas — porque eso fue lo que llevó — había algo que funcionaba en mi red. Dos clientes, un concentrador, mensajes WebSocket, autenticación básica. Suficiente para empezar a usarlo.
El bache: bugs que iban y venían
Y entonces empezó la parte que no contábamos. Encontraba un bug, se lo describía a Claude, lo arreglaba. Yo seguía probando. Aparecía otro, se lo describía, lo arreglaba… y al rato volvía a aparecer el primero. O uno parecido. O uno nuevo en la zona que acababa de tocar.
Esto, que con un humano sería frustrante, con una IA es directamente desesperante. Porque no hay memoria persistente del «ya pasamos por aquí». No hay un *«sí, ya sé, lo arreglé la semana pasada»*. Hay un sistema que, en cada turno, hace lo mejor que puede con el código que ve delante, sin recordar haber tocado esa zona antes.
Llevaba un par de días así cuando me di cuenta de que el problema no era de Claude. El problema era que no tenía manera de saber si una corrección rompía algo ya bueno. Ni él, ni yo. La aplicación es de bandeja del sistema, con UI gráfica y comportamiento asíncrono — exactamente el tipo de cosa que cuesta probar a ojo, y donde un bug puede tardar varios usos en volverse a manifestar.
La inversión que lo cambió todo: tests, incluida la UI
Así que dejé de pedir features durante un día entero y me puse a pedir una cosa distinta: una batería de tests que cubriera todo, incluida la UI gráfica.
Aquí tuve que insistir bastante. Claude empezaba diciendo cosas como «los tests de UI son complicados de hacer bien», «requieren entornos gráficos», «esta parte es difícil de aislar». Todo cierto, pero ninguno era razón suficiente. Le pedí que se buscase la vida: si hacía falta un compositor Wayland en modo headless para los tests, que lo levantara. Si hacía falta interactuar con DBus para verificar que el menú de la bandeja respondía, que se hablara con DBus. Si en Windows había que usar la API de UI Automation, que la usase.
Y poco a poco fue saliendo. Acabamos con:
- Smoke tests tier-1 que se ejecutan en cada push.
- Tests del menú de bandeja en Linux conducidos por DBus, validando que cada entrada responde como debe.
- Tests de descubrimiento del icono en Windows usando la API de UI Automation.
- Tests de UI del diálogo de Settings con egui_kittest.
- Un test de integración que verifica que el lock de instancia única caza los lanzamientos duplicados.
CI corre todo en Linux y Windows en cada push. Y a partir de ese momento, todo cambió. La IA se volvió mucho más fiable. Porque cuando rompía algo, lo veíamos en seguida. Y cuando arreglaba algo, sabíamos que de verdad estaba arreglado, no que parecía estarlo.
Esa es probablemente la lección más reutilizable de todo este proyecto: los tests no son una carga que añades porque «hay que hacerlos». Son los guardarraíles que necesita un agente para no salirse de la carretera. Con tests sólidos, una IA puede iterar agresivamente sin romper. Sin ellos, va dando vueltas en el mismo sitio.
Las decisiones que me apropié yo
Aunque la mayor parte del código lo escribió Claude, hubo varias decisiones de fondo que tomé yo, deliberadamente, y que cambian la naturaleza del proyecto. Las cuento porque creo que es la parte interesante para quien también está experimentando con este flujo.
Simplifiqué el sistema de autenticación
ClipCascade venía con un esquema de autenticación bastante complejo, pensado para un escenario más amplio que el mío. Cuando vi lo que se estaba portando, paré la traducción y le dije a Claude que quitase la mayor parte. Para mi caso de uso — mi LAN, mi VPN, mis máquinas — no necesitaba PBKDF2 ni rondas de derivación ni nada parecido. Una autenticación básica HTTP sobre TLS me sobra. Lo demás es ingeniería sobrante.
Asumí un modelo de amenaza honesto
El concentrador ve el portapapeles en texto plano una vez ha terminado la conexión TLS. No hay cifrado extremo a extremo entre cliente y cliente. Esto es una decisión consciente, no un descuido: el concentrador lo levanto yo en una máquina que controlo, dentro de una red que controlo. Si confío en mi propia máquina (que tiene mi clave SSH, mis cookies, mi historial), añadir E2EE sería un teatro de seguridad. Lo escribí explícitamente en la documentación.
Para un uso por internet público o entre infraestructura no confiable, la herramienta no es la adecuada — y lo dejo claro. El día que quiera ir ahí, montaré E2EE con emparejamiento TOFU entre dispositivos. Pero esa es otra herramienta.
Tres modos en un solo binario
El mismo ejecutable puede comportarse como cliente (connect), como concentrador (serve), o como las dos cosas a la vez en el mismo proceso (host, que es el modo de mi workstation: participa Y hace de concentrador para las otras máquinas). Esto me parece la manera natural de empaquetar la cosa: un único .deb, un único .msi, y dependiendo de cómo se lance, asume el papel.
App de bandeja, no daemon
Quería que la aplicación se instalara y estuviese siempre visible en la bandeja del sistema, con icono y estado en vivo (Connecting / Connected / Disconnected — retrying in N s). Esto no es solo cosmética: cuando algo no funciona en una herramienta de portapapeles, lo notas después de que ha fallado. Tener un indicador visible permanente cambia esa experiencia: ves el estado antes de pegar.
Wayland, por cierto, no dio guerra
Una nota corta para los que vengan a esto desde un dolor parecido al mío: Wayland funcionó sin sustos. Una vez le dejé claro a Claude que era el entorno prioritario, la implementación con GTK + libayatana-appindicator salió bien a la primera. Nada de hacks raros, nada de fallback a X11 disfrazado. El icono de bandeja aparece, el menú responde, los eventos del portapapeles llegan vía arboard. Punto.
Quizá esto ya no sorprenda a nadie en 2026, pero teniendo en cuenta que esta historia empezó porque Deskflow no soporta Wayland, me parece digno de mención.
¿Qué me llevo de todo esto?
Tres cosas que no esperaba antes de empezar:
- Que el cuello de botella iba a ser la calidad del feedback que recibe la IA, no su capacidad de escribir código. Mientras los bugs se diagnosticaban por inspección manual, íbamos a velocidad cero. En cuanto hubo tests que daban una señal binaria — «funciona / no funciona» — la velocidad se multiplicó.
- Que mi trabajo real fue de dirección, no de programación. Las decisiones que más impactaron — simplificar el auth, asumir Wayland prioritario, definir el modelo de amenaza, exigir tests UI a pesar de las protestas — son decisiones de producto y arquitectura, no de código. Esa es la zona donde sigo aportando valor.
- Que el «no sé Rust» dejó de ser un obstáculo en cuestión de horas. No porque haya aprendido Rust — sigo sin saber Rust en el sentido tradicional — sino porque puedo dirigir un proyecto en Rust leyendo el diff, entendiendo la estructura general, y dejando que el detalle idiomático lo cuide otro.
Esto último me genera sentimientos encontrados, que no voy a esconder. Por un lado es liberador: las herramientas que necesito ya no están limitadas por los lenguajes que domino. Por otro lado, hay algo que perder ahí, y todavía no sé qué nombre ponerle.
El resultado
El proyecto se llama clipboardwire y vive en github.com/davefx/clipboardwire. Hay paquetes .deb, .rpm y .msi en cada release. macOS llegará. La página del proyecto en este sitio está aquí.
Lo uso todos los días entre mi Linux y mi Windows. El servidor consume unos pocos megas de RAM, no medio gigabyte. Y el Ctrl+C en una máquina ya llega a la otra sin tener que pasar por Google Chat.
Si te encuentras en una situación parecida — o si simplemente te tienta el experimento de construir algo en un lenguaje que no conoces — quizá te sirva.

