Votre Mac reste à la maison, allumé, connecté. Vous êtes dehors avec un téléphone et une paire de lunettes AR. Vous tapez sur la branche, dites une phrase, et un agent de code se met au travail sur ce Mac — il lit des fichiers, exécute des commandes, ouvre des pull requests — pendant qu'une simple ligne de texte vert vous dit ce qu'il fait. Pas d'ordinateur portable ouvert, pas de téléphone déverrouillé, aucun clavier dans la boucle.
C'est tout le pitch. Ce billet est le journal de construction : ce qu'on a utilisé, ce qui a cassé, et les deux vraies vulnérabilités qu'on a fini par corriger en chemin.

On a adopté, on n'a pas réécrit
Un projet open source nommé Rokid_Claude, par l'utilisateur GitHub williamlzz, implémente déjà presque exactement cette boucle : un client Android pour les lunettes intelligentes Rokid qui capture la voix et affiche un HUD, et parle en WebSocket à un petit relay Node/TypeScript ; le relay transcrit en local avec whisper.cpp et lance claude -p --output-format stream-json, en renvoyant chaque événement vers les lunettes. On l'a forké plutôt que de construire le nôtre, parce que l'écart entre ce qu'il faisait déjà et ce dont on avait besoin était marginal, et que le coût d'une réécriture ne l'était pas.
Ce qu'on y a apporté : une spec de conception, un plan d'implémentation exécuté tâche par tâche avec des commandes de vérification plutôt que des suppositions, huit correctifs sur le code amont, et une APK recompilée. Deux de ces correctifs ferment de vraies failles de sécurité. Le reste, ce sont des corrections qui n'existent que parce que quelqu'un a vraiment fait tourner la chose sur du matériel réel au lieu de se contenter de lire le README.
L'architecture
La boucle a quatre composants, et un seul d'entre eux sort de votre propre réseau.
Le flux : un appui sur la branche capture de l'audio PCM par WebSocket, le relay le transcrit en local avec whisper.cpp et compose un prompt, le relay lance claude -p en JSON de streaming et relaie chaque événement vers le HUD, et un hook PreToolUse intercepte les appels d'outils risqués — Bash, Write, Edit — pour pousser une demande de confirmation qu'un geste sur les lunettes vient trancher. À la fin de la tâche, le HUD affiche le résultat, le nombre de tokens et le coût.
Comme le relay parle un petit protocole JSON sur un simple WebSocket, le client lunettes n'est en réalité pas indispensable. Un navigateur ou un téléphone peuvent taper sur le même relay pour déboguer — les lunettes ne sont que le périphérique d'entrée qu'on a choisi.
Le matériel a résisté avant le logiciel
La spec de conception supposait un appareil Android relativement ouvert. L'unité réelle est une build de production verrouillée — ro.build.type=user, ro.debuggable=0 — et Rokid désactive la plupart de la surface adb sur laquelle compte normalement un développeur Android.
Trois conséquences découlent de ce tableau, et aucune n'est documentée nulle part dans le projet amont :
Rien de tout cela ne se manifeste par un message d'erreur. Cela se manifeste par un échec plausible : le scanner de QR se ferme en silence, l'application retombe sur une interface chinoise avec un token vide, le Wi-Fi refuse de se reconnecter après un redémarrage. Chacun de ces cas a une cause réelle, et aucun ne l'annonce.
Deux vraies vulnérabilités, trouvées en lisant la source
Le relay exécute Claude Code sur votre Mac. Son token équivaut fonctionnellement à une exécution de code à distance. Lire la source avant de lui faire confiance a révélé deux bugs qui n'avaient rien de cosmétique.
Le token fuitait dans une page servie publiquement. Le relay sert un miroir de session local et y injecte automatiquement le token d'authentification, mais seulement pour les requêtes qu'il juge locales — un jugement fondé sur l'adresse source du socket. Tailscale Funnel, le tunnel qui expose le relay à l'internet public, se connecte à ce même relay depuis 127.0.0.1 tout en relayant du trafic venu de n'importe où. L'ancien contrôle voyait une adresse loopback et injectait le token dans du HTML servi à l'internet entier. Le correctif vérifie la présence d'en-têtes de relais — x-forwarded-for, x-forwarded-proto, x-forwarded-host, forwarded — et refuse de considérer une requête comme locale si l'un d'eux est présent, même depuis 127.0.0.1.
Les demandes de confirmation expiraient en approbation silencieuse — ou en refus silencieux, selon le côté où l'on se trouvait. Une demande de permission était diffusée aux clients connectés exactement une fois. Si les lunettes se déconnectaient et se reconnectaient en cours de demande — ce qui arrive constamment sur une connexion mobile — la nouvelle connexion ne voyait jamais la demande en attente, qui expirait au bout de soixante secondes sur un choix par défaut sans que personne ait pu répondre. Le correctif retient la charge utile de chaque demande encore ouverte et la rejoue à tout client qui se connecte pendant qu'elle est en attente.
Les deux correctifs sont arrivés avec des tests de régression : un test vérifie qu'une requête réellement locale reçoit toujours le token, qu'une requête relayée depuis la même adresse ne le reçoit pas, et qu'un client se connectant après la diffusion d'une demande de permission la reçoit quand même et peut y répondre.
Des échecs silencieux qui ont chacun coûté du temps réel
Chaque panne sur ce projet ressemblait à autre chose. Aucune n'a levé d'exception. Voici la liste qu'on aurait aimé avoir avant de commencer.
Tailscale, puis un compromis sur Tailscale
La conception initiale prévoyait que les lunettes rejoignent directement un réseau Tailscale privé, pour qu'il n'existe aucun point d'entrée public — exactement la classe d'exposition que l'option ngrok du projet crée, entièrement évitée. Cela n'a pas survécu au contact du matériel. Installer le client Tailscale Android passe encore par le canal d'installation compagnon, qui fonctionne, mais connecter ce client à un compte exige un flux OAuth dans un navigateur — que le HUD n'a pas, et que scrcpy ne peut pas suppléer puisqu'il a besoin du même adb push que l'appareil refuse déjà.
Le repli est Tailscale Funnel : le Mac expose le relay sur une URL HTTPS stable, atteinte via la 5G du téléphone. C'est la même catégorie d'exposition que la voie ngrok que la conception voulait éviter, avec trois différences — un domaine stable, un certificat géré, et aucun compte tiers supplémentaire. Le compte honnête : l'URL est publiquement découvrable, et seuls le token et le geste de confirmation se tiennent entre cette URL et une exécution de code sur le Mac. La spec voulait mieux ; le matériel ne l'a pas permis.
À quoi ça ressemble au quotidien
Depuis les lunettes, un vocabulaire fixe de phrases exactes pilote tout — comparé phrase entière plutôt que sous-chaîne, pour qu'une consigne contenant par hasard le mot « sessions » ne déclenche pas la liste des sessions par accident. Dire help relit la liste courante depuis le code qui la définit, pour qu'elle ne puisse jamais se désynchroniser.
Depuis le Mac, un petit CLI encapsule le même relay : envoyer une consigne ponctuelle, en envoyer une et suivre le résultat dans le terminal, observer la session en direct, rejouer le fil complet de tous les tours (lunettes comprises, puisqu'une phrase dite aux lunettes n'apparaît jamais dans une fenêtre Claude Code déjà ouverte, même quand tous les processus lisent le même fichier de transcript), lister les sessions reprenables, en reprendre une par numéro, et accorder ou refuser une demande de permission en attente depuis le clavier plutôt que par un geste.
Chaque appel d'outil risqué attend toujours un geste humain avant de s'exécuter, que ce geste vienne des lunettes ou des commandes allow/deny du CLI — le point de contrôle est la phrase qu'on a validée, pas chaque outil individuel que l'agent appelle ensuite pour l'exécuter.
La preuve
Le critère de réussite de la conception était précis : depuis un réseau extérieur au domicile, énoncer une tâche, voir sa progression sur le HUD, approuver une action risquée d'un geste, et la voir aboutir sur le Mac sans toucher ni au Mac ni au téléphone.
On a mené ce test avec le câble USB toujours physiquement branché mais sa redirection adb reverse délibérément retirée, pour qu'une réussite par le câble ne soit pas possible, et les lunettes sur la connexion mobile du téléphone plutôt que sur le Wi-Fi domestique. L'aller-retour a abouti avec la connexion arrivant par le processus tailscale plutôt que par adb, confirmant que le chemin réseau testé était bien celui visé. L'échange complet, lunettes vers Mac et retour : une réponse est revenue correspondant exactement à ce qui était demandé, le bon répertoire de travail était déjà chargé comme session active, et la tâche s'est terminée avec succès pour un coût de quelques centimes.
Ce qui reste cassé
La liste honnête des limites, parce qu'un journal de construction qui ne liste que des victoires n'en est pas un.
Ce qu'il faut en retenir
Si vous branchez de la voix sur un agent capable d'exécuter du code, trois choses issues de ce projet dépassent le cas particulier des lunettes AR. La confirmation doit porter sur la phrase, pas sur l'outil — le vrai point de défaillance ici n'a jamais été un appel d'outil mal intentionné, c'était une instruction mal transcrite, c'est donc à ce niveau-là que doit se trouver le verrou humain. Un tunnel public n'est pas automatiquement le repli honnête qu'il paraît être — Tailscale Funnel relaie chaque requête depuis 127.0.0.1, ce qui met silencieusement en échec tout contrôle « est-ce local ? » fondé sur la seule adresse du socket, et c'est exactement cette brèche qui a fait fuiter notre token. Et les contraintes matérielles non documentées coûtent plus cher que le code applicatif lui-même — presque chaque heure de ce projet est partie dans une surface adb verrouillée, un fichier de configuration au mauvais propriétaire, ou un TMPDIR qui désactivait silencieusement un garde-fou de sécurité, pas dans le relay, le HUD, ou Claude Code lui-même.
Questions fréquentes
- Is this built on an open-source project, or from scratch?
- It forks an existing open-source project, Rokid_Claude by GitHub user williamlzz, which already implemented the glasses-to-relay-to-Claude-Code loop. We added a design spec, a task-by-task implementation plan, eight patches to the upstream code — two of which close real security vulnerabilities — and a recompiled APK with the permissions the published release was missing.
- How is the connection secured, given the relay can execute code on a Mac?
- Four layers: the relay listens only on 127.0.0.1 unless explicitly told otherwise, every connection needs a 48-character token or it's refused with a 1008 close code, every risky tool call (Bash, Write, Edit) requires a human gesture on the glasses before it runs, and the tunnel to the public internet is TLS end-to-end. The honest caveat is that the public tunnel is discoverable by URL, and only the token and the gesture confirmation protect it from there.
- Why Tailscale Funnel instead of keeping the glasses on a fully private network?
- That was the original plan, but the glasses' HUD has no browser, and joining a private Tailscale network requires an OAuth login in one. Screen mirroring tools that could work around that need adb push, which this hardware refuses outright. Tailscale Funnel was the fallback that kept a stable domain and a managed certificate without adding a third-party tunneling account.
- Does dictation work in languages other than English?
- Not reliably yet. The relay currently locks to English because the project defaults to Chinese for its whisper.cpp transcription. French was tested with vocabulary priming and improved short jargon like 'commit' and 'push', but longer phrases can still transcribe into a completely different, grammatically valid word — a failure mode nothing downstream can automatically detect.
Mots-clés


