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, et il n'a pas changé. Ce qui a changé, c'est tout ce qu'il y a dessous : ce projet a démarré avec huit correctifs sur le travail de quelqu'un d'autre, et il en compte aujourd'hui cinquante — gestion de sessions, changement de projet, enregistrement caméra avec sa propre chaîne de fabrication de vidéo de démo, statistiques d'usage exportées directement dans la galerie photo, et une interface en français. Ce billet est le journal de construction complet, réécrit de zéro pour couvrir tout cela.
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.
La première vague de travail portait sur la fiabilité et la sécurité : 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 fermant de vraies failles de sécurité. Ça a été livré, on s'en est servi tous les jours, et c'est cet usage quotidien qui a produit tout le reste. Chacun des quarante-deux correctifs suivants est né de quelque chose qui a cassé, qui gênait, ou qui manquait à l'usage réel dans la rue : choisir à quel projet parler, reprendre une session en rentrant, voir ce qu'a coûté la dernière heure, filmer une démo pour un client. Le compteur est aujourd'hui à cinquante correctifs, sur environ 3 300 lignes de Kotlin et 3 400 lignes de TypeScript, couvertes par 196 tests côté relay et 181 tests côté Android.
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 puis applique un petit dictionnaire de raccourcis vocaux (dire « combien » devient /cost, par exemple), le relay lance claude -p en JSON de streaming et relaie chaque événement vers le HUD, et un hook PreToolUse peut intercepter les appels d'outils risqués — Bash, Write, Edit, MultiEdit, NotebookEdit — 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 protocole lui-même a grandi avec les fonctionnalités : il partait avec une poignée de types de messages et en porte aujourd'hui près de trente, couvrant sessions, projets, statistiques et enregistrement, en plus de la boucle prompt/événement d'origine.


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.
Des 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 contraintes propres au niveau d'API Android ont mordu directement sur des fonctionnalités plus tardives. WifiManager.setWifiEnabled() est sans effet depuis l'API 29 — mesuré après un redémarrage qui a laissé le Wi-Fi éteint, l'application tournant sans qu'aucun chemin de code ne puisse le rallumer ; le seul geste qui reste est d'ouvrir le panneau système et de demander à l'humain. Et le presse-papiers n'est lisible que par l'application qui a le focus, sans rien de partagé entre les lunettes et un téléphone — ce sont, après tout, deux appareils distincts, un fait facile à oublier quand ils sont posés à quinze centimètres l'un de l'autre.
De 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, et une règle défensive que les fonctionnalités suivantes ont dû hériter.
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.
Le changement de projet à la voix n'accepte qu'un projet que le relay connaît déjà. Dès que « changer de projet » est devenu une vraie commande, la version naïve évidente aurait lu n'importe quel chemin fourni par une transcription et ouvert une session Claude Code à cet endroit. Une phrase mal transcrite aurait alors été libre de pointer le relay — et donc Claude Code — vers un dossier arbitraire du Mac. La règle retenue à la place : setProject vérifie contre une liste fixe de projets connus, et la même règle s'applique au répertoire de travail restauré depuis le disque après un redémarrage du relay, pour qu'un fichier d'état modifié à la main ne puisse pas rouvrir n'importe quoi non plus.
Les deux correctifs d'origine 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.
Cinquante correctifs plus tard : ce qui s'est construit par-dessus
Vivre avec la première version au quotidien, c'est ce qui a produit la seconde. Quatre capacités sont sorties de cet usage, chacune avec sa propre aspérité.
Sessions et projets, entièrement pilotés à la voix. Dire mes sessions liste les sessions Claude Code reprenables sous forme de menu numéroté et défilable ; dire projets fait de même à travers tous les répertoires de travail que le relay connaît, du plus récent au plus ancien. Reprendre une session rappelle ses derniers messages avant que la voix ne la continue — délibérément pas le mot resume lui-même, essayé puis retiré, parce que ce mot s'adresse à Claude (« reprends où on en était ») et que se l'approprier pour la navigation privait l'utilisateur d'une phrase qu'il voulait vraiment pouvoir dire. Une commande vocale lancée depuis le terminal du Mac et une lancée depuis les lunettes restent maintenant synchronisées : changer de session ou de projet est désormais diffusé à tous les clients connectés, parce qu'une bascule qui ne mettait à jour que celui qui l'avait lancée laissait sinon les lunettes afficher en silence un fil périmé.


Un menu plutôt qu'une interruption. La toute première version traitait n'importe quel appui pendant un tour en cours comme un interrupteur général — la seule façon de vérifier une session était de la tuer. Le correctif ouvre un menu à la place : interrompre, mettre en file la nouvelle consigne pour après celle en cours, changer de session, ou changer de projet, sans jamais perdre le tour en vol. Faire tenir la comptabilité correctement a demandé plusieurs correctifs de suite : quelle session est « active » se décide désormais à la bascule elle-même, pas à celle qui tournait par hasard quand le premier événement d'un tour en fond est arrivé, et chaque tour est épinglé à sa propre session pour que deux processus claude simultanés ne puissent plus écrire dans le même transcript.

L'enregistrement caméra, et toute une chaîne pour en faire une démo. Les lunettes peuvent filmer en silence pendant qu'une tâche tourne, piloté à la voix (enregistrer / stop vidéo), avec une machine à états — repos, démarrage, enregistrement, arrêt, échec — entièrement testable sans le matériel branché. Produire une vraie vidéo de démo à partir de cette prise est devenu son propre petit processus de production : puisque le véritable écran ne peut pas être capturé, une page presentation.html servie par le relay tient lieu de HUD, filmée en plein écran par ffmpeg sur le Mac pendant que les lunettes filment la scène, et un script de composition superpose les deux pistes. Les deux prises ne démarrent jamais au même instant — le relay annonce « enregistrement démarré » dès qu'il reçoit l'ordre, pas quand la caméra a réellement démarré, et ce retard a été mesuré entre 0,6 seconde (caméra déjà chaude) et 4,7 secondes (toute première prise d'une session). Trouver le bon décalage a demandé de repérer l'instant où le point rouge « ● REC » s'allume réellement à l'écran, et d'y ajouter le retard caméra mesuré — pas l'un des deux chiffres seul.
Des statistiques d'usage, dessinées sur les lunettes et déposées directement dans la galerie photo. Dire statistiques affiche requêtes, sessions, tokens et dollars dépensés sur trois fenêtres glissantes — sept jours, trente jours, et le total — délibérément glissantes plutôt que calendaires, puisqu'une semaine calendaire répondrait « presque rien » tous les lundis matin. Les chiffres en dollars viennent de l'événement done de Claude Code lui-même, parce que le relay est la seule partie du système à les conserver ; les transcripts de Claude Code eux-mêmes gardent les tokens mais pas le coût. Dire exporte écrit cette même carte dans la pellicule via l'API MediaStore d'Android, en marquant le fichier comme en attente jusqu'à la fin de l'écriture pour que la galerie ne montre jamais une image à moitié dessinée — le seul chemin qu'une application cloisonnée a vers le stockage partagé.

Une interface en français, avec de vraies mesures plutôt que des suppositions. Le HUD, ses menus et ses commandes vocales existent désormais en chinois, en anglais et en français, et le vocabulaire français n'a pas été écrit à partir d'un guide de conversation — il vient d'avoir vraiment fait parler une voix française dans ce même modèle whisper.cpp (ggml-small, -l fr) et d'avoir corrigé ce qui cassait. L'appariement des commandes passe chaque phrase par une forme canonique qui la met en minuscules, retire la ponctuation finale, replie les accents et ramène les pluriels au singulier, parce que la sortie de whisper sur ce matériel n'est pas assez stable pour une comparaison exacte : « réglages » ressort « Réglage », « enregistrer » ressort « enregistré », et un simple « aide » ressort parfois le mot anglais « head » parce que whisper bascule en anglais sur des énoncés très courts. Les trois cas sont rattrapés — les deux premiers par la forme canonique, le troisième en ajoutant « head » lui-même à l'ensemble des phrases qui ouvrent l'aide, puisque ce mot n'ouvre aucune autre porte dans cette application. La réserve honnête reste la même qu'avant : cela corrige des mots de commande courts et bien définis, pas la dictée libre. Une phrase complète prononcée en français pour décrire une vraie tâche de code reste bien plus susceptible de tomber sur un mot faux mais valide, qu'aucun contrôle en aval ne peut signaler comme une erreur de transcription plutôt qu'une vraie instruction.


Des notifications qui survivent à un écran éteint. Une session lancée ailleurs qui se termine, ou une demande de permission en attente, se dessinait auparavant sur un écran déjà endormi — le message existait, mais sous le même voile noir qui masque tout le reste, et une demande de permission expirait en silence sans avoir été lue. Le correctif réveille l'écran pour une notification flottante, positionnée pour survivre à la seule vraie bizarrerie du combineur optique de l'appareil : le noir pur y est transparent, donc un fond plein écran sombre fait flotter tout le reste de la pièce — une barre de menus, un onglet ouvert, le Dock — par-dessus le HUD comme si cela en faisait partie. La boîte de notification flotte désormais directement sur la session en cours au lieu d'être posée dans son propre cadre opaque.

Une découverte plus discrète au passage : la posture de permission du relay elle-même cache un piège. ROKID_PERMISSION_MODE peut valoir default (le comportement d'origine — chaque appel Bash, Write, Edit bloque sur un geste des lunettes), auto (un classificateur tranche, pas de confirmation par outil — ce que fait tourner le service permanent aujourd'hui), ou bypassPermissions (plus aucun garde-fou côté outils). Le hook PreToolUse et ce réglage sont indépendants, et passer auto sans retirer aussi le hook laissait chaque appel d'outil attendre une confirmation que personne n'allait jamais envoyer, expirant en refus silencieux au bout de soixante secondes. Le relay n'installe plus le hook en dehors du mode default, et annonce désormais sa posture de permission à chaque ligne de démarrage, pour qu'elle ne soit plus jamais quelque chose à deviner.
Ce dernier changement est aussi un vrai basculement du modèle de sécurité, pas seulement une correction de bug : la configuration utilisée au quotidien tourne maintenant en auto, ce qui veut dire que les appels d'outils individuels n'attendent plus du tout un geste sur les lunettes. Le vrai verrou s'est déplacé plus tôt — vers confirmPrompts, l'étape qui montre la phrase transcrite et attend Send ou Discard avant que quoi que ce soit ne soit soumis à Claude. Ce n'est pas un contournement ; c'est la même conclusion vers laquelle pointaient déjà les deux vulnérabilités d'origine, désormais intégrée dans la configuration par défaut plutôt que simplement défendue à la fin d'un billet de blog.
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. Toute cette chaîne se relance désormais toute seule : un service de lancement en arrière-plan remonte le relay et son tunnel à chaque reconnexion et à chaque redémarrage du Mac, plutôt que d'exiger un redémarrage manuel avant la première tentative « en déplacement » de la journée. La spec voulait mieux que Funnel ; le matériel ne l'a pas permis — mais au moins, ce qui existe aujourd'hui reste debout sans qu'on ait à le lui demander.
À 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 aide relit la liste courante depuis le code qui la définit, pour qu'elle ne puisse jamais se désynchroniser ; chacune de ces phrases existe aussi en chinois et en anglais.


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, lister et reprendre des sessions par numéro, 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 — et accorder ou refuser une demande de permission en attente depuis le clavier plutôt que par un geste. Ce rejeu de transcript compte en échanges, pas en messages bruts : sur ce projet, l'assistant écrit environ sept messages d'appel d'outil pour chaque message écrit par l'utilisateur, si bien qu'un simple compte de messages noierait les vraies consignes sous le bavardage d'outils de l'agent.
Chaque appel d'outil risqué en mode default attend toujours un geste humain avant de s'exécuter, que ce geste vienne des lunettes ou des commandes allow/deny du CLI — mais comme l'explique la discussion sur les modes de permission ci-dessus, le point de contrôle qui compte vraiment au quotidien est la phrase qu'on a validée avant qu'elle ne parte, pas chaque outil individuel que l'agent appelle ensuite pour l'exécuter.

La preuve
Le critère de réussite d'origine 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. Tout ce qui s'est construit depuis — sessions, projets, enregistrement, statistiques, français — a tourné sur cette même installation permanente, pas sur un banc de démo séparé.
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, quatre choses issues de ce projet dépassent le cas particulier des lunettes AR.
La confirmation doit porter sur la phrase, pas sur l'outil. C'était vrai quand ce projet comptait deux correctifs, et c'est toujours vrai à cinquante : le vrai point de défaillance ici n'a jamais été un appel d'outil mal intentionné, c'était une instruction mal transcrite, et la configuration déployée aujourd'hui le reflète directement — la confirmation par outil est désactivée, et le contrôle au niveau de la phrase est la seule chose restée non négociable.
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 au départ.
La comparaison de chaînes exactes ne survit pas au contact d'une vraie reconnaissance vocale. Chaque nouvelle commande vocale ajoutée après la première version a eu besoin du même repli d'accents, de la même singularisation et du même filet multilingue que les commandes d'origine n'avaient pas — mesuré sur une vraie voix, pas supposé depuis une spec — parce que la sortie de whisper sur ce matériel n'est tout simplement pas assez stable pour une comparaison littérale.
Et une plateforme non testable n'excuse pas du code non testé. Le point d'entrée du client Android ne peut tourner sous aucun harnais de test JVM, si bien que la règle née de cette contrainte — tout ce qui peut s'exprimer comme une fonction pure sort de ce fichier non testable et reçoit son propre test, vérifié par mutation pour qu'un test qui passerait encore avec la logique cassée ne compte pas — est ce qui a permis à cinquante correctifs d'atterrir sur un appareil qui, de son propre aveu, ne peut être ni capturé, ni dupliqué à l'écran, ni débogué de façon conventionnelle.
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. The first wave added a design spec, a task-by-task implementation plan, and eight patches to the upstream code — two of which closed real security vulnerabilities — plus a recompiled APK. Fifty patches in, it now also has voice-driven session and project switching, camera recording with its own demo-video pipeline, usage statistics exported to the photo gallery, and a French interface.
- How is the connection secured, given the relay can execute code on a Mac?
- 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, the tunnel to the public internet is TLS end-to-end, and voice-driven project switching only ever resolves to a project the relay already knows about. Per-tool confirmation on the glasses is now off by default in favor of a sentence-level check — you approve the transcribed instruction before it's ever sent to Claude, rather than approving each tool call it makes to carry it out. The honest caveat is that the public tunnel is discoverable by URL, and only the token and that sentence-level check 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, and it now restarts itself automatically after a reconnect or a Mac reboot.
- Does dictation work in languages other than English?
- The interface and its fixed voice commands now work in Chinese, English and French, with the French vocabulary tuned against a real recorded voice rather than guessed — accent-folding and singularization catch most of whisper's quirks on short command words. Free-form dictation of an actual task is a different problem: a full French sentence can still transcribe into a completely different, grammatically valid word — 'commits' becomes 'commis,' for instance — a failure mode nothing downstream can automatically detect.
- What can the glasses actually do beyond sending a single instruction?
- List and resume Claude Code sessions by voice, switch between known projects, queue or interrupt a running turn instead of only being able to kill it, film a silent demo of a session, pull up usage statistics over rolling 7-day/30-day/all-time windows and export that card straight to the photo gallery, and get a screen-waking notification when a task started elsewhere finishes.
Mots-clés


