Aller au contenu
codecrush®
Technology11 min read19 septembre 2026

Piloter Claude Code depuis des lunettes AR Rokid : un poste de code entièrement vocal

On a relié une paire de lunettes AR Rokid à Claude Code tournant sur un Mac resté à la maison, pour dicter une tâche depuis la rue et la suivre sur un HUD vert. Voici l'architecture, les deux vraies failles de sécurité trouvées en cours de route, et tous les échecs silencieux qui nous ont coûté une après-midi.

Code Crush Team

Gamification Agency

Un HUD monochrome vert flottant au-dessus d'une rue au crépuscule, représentant une interface de lunettes AR pour un agent de code distant

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.

L'idée en une image : un HUD monochrome flottant au-dessus de la rue pendant qu'un agent de code travaille sur une machine laissée à la maison.
L'idée en une image : un HUD monochrome flottant au-dessus de la rue pendant qu'un agent de code travaille sur une machine laissée à la maison.

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.

ComposantRôleDépend de
Client lunettesCapture le micro, affiche le HUD, lit les gestes, provisionne le Wi-Fi par QRAPK sideloadée, Wi-Fi
RelayTranscription, orchestration, protocole JSON/WebSocketNode ≥ 18, whisper.cpp, modèle ggml-small
Claude Code CLIFait le vrai travail, sur le MacInstallé et authentifié
Tailscale FunnelLe chemin HTTPS public entre la 5G d'un téléphone et le MacUn compte Tailscale

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.

Capacité adbÉtat sur cette unité
adb shell, monkey, svc, content, adb reverseFonctionnels
adb install, adb pushRefusés : error: closed ou not allow package install!
pm, am, input, screencapRefusés : error: closed

Trois conséquences découlent de ce tableau, et aucune n'est documentée nulle part dans le projet amont :

L'application ne peut pas s'installer par câble. Elle passe par l'application compagnon — Hi Rokid → Toolbox → Install a new app — un canal d'installation distinct (CXR-L) qu'un téléphone, y compris un iPhone, peut piloter alors même que la cible est Android.
Il n'existe aucun moyen de forcer l'arrêt d'une application. am force-stop, kill et pm sont tous bloqués, et l'application compagnon n'offre aucun bouton d'arrêt forcé. Le redémarrage est le seul qui existe.
Aucune duplication d'écran n'est possible. scrcpy a besoin d'adb push pour installer son composant côté Android, et cette voie est fermée ; screencap l'est aussi. Le seul substitut est une page HTTP locale que le relay lui-même sert, montrant un miroir en direct de la session.

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.

Un fichier de configuration écrit par adb appartient au mauvais utilisateur. Écrire le config.json du client via adb shell le laisse appartenir à shell ; l'application ne peut pas lire un fichier qu'elle ne possède pas, avale l'exception en silence, et retombe sur ses défauts — interface chinoise, token vide, connexion refusée. Un chmod 644 sur le fichier corrige le problème ; rien dans l'application ne dit jamais que c'est ce qui s'est passé.
L'état du Wi-Fi ne survit pas à un redémarrage. wifi_on retombe à zéro à chaque redémarrage des lunettes, et le réseau enregistré devient inopérant. cmd wifi est bloqué sur cette unité ; svc wifi enable ne l'est pas.
Un TMPDIR éphémère désactive silencieusement le hook de permission. Le relay écrit le fichier --settings portant le hook PreToolUse dans TMPDIR. Si ce répertoire est nettoyé par un outil en cours de session, Claude Code continue de tourner — sans plus jamais demander de confirmation, et sans qu'aucune ligne ne l'annonce. Le correctif est opérationnel, pas logiciel : lancer le relay depuis un TMPDIR qui survit à la session, et faire annoncer par le relay sa posture de permission à chaque démarrage pour qu'elle ne soit jamais implicite.
La langue par défaut est la mauvaise, silencieusement. Le relay part en chinois par défaut et impose cette langue à whisper.cpp. De l'anglais dicté sous ce réglage ressort en formules de fin de vidéo chinoises, transmises telles quelles à Claude. ROKID_LANG=en corrige cela et verrouille la langue contre un client dont la configuration serait périmée.
L'APK de la release publiée manque des permissions que sa propre source déclare. Comparer le manifeste de l'arbre source au contenu réel de l'APK v0.1.0 a montré qu'il lui manquait CAMERA, ACCESS_WIFI_STATE et CHANGE_WIFI_STATE. Le symptôme visible était un scanner de QR qui se refermait en silence et un provisionnement Wi-Fi qui ne fonctionnait tout simplement jamais — ce qui rendait tout usage en déplacement impossible tant qu'on n'a pas recompilé depuis les sources.

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.

DireEffet
new session ou resetRepartir à zéro
sessions ou list sessionsUne liste numérotée des sessions reprenables — glisser pour parcourir, appuyer pour reprendre
wifi ou networkOuvrir le panneau Wi-Fi
switch languageBasculer la langue de l'interface
exit, quit ou closeFermer l'application

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.

La dictée en français ne fonctionne pas. L'amorçage de vocabulaire corrige les mots courts et courants — « commit » et « push » se transcrivent correctement avec un prompt de jargon chargé — mais « commits » en fin de phrase ressort encore en « commis », un mot français parfaitement valide qu'aucun contrôle en aval ne peut signaler comme une erreur de transcription plutôt qu'une vraie instruction.
L'autonomie sur une longue session réelle n'a pas été mesurée. Le service batterie de l'appareil (dumpsys battery) ne fonctionne pas sur cette unité ; la seule lecture disponible passe par sysfs, et elle doit être prise immédiatement après débranchement, car la recharge fausse le chiffre en quelques secondes.
Toutes les mesures faites jusqu'ici utilisaient une voix de synthèse, ce qui est le cas le plus favorable. Personne n'a encore testé cela avec une vraie voix dans une rue bruyante.

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

Claude CodeRokidAR glassesvoice codingTailscalewhisper.cppdeveloper tools
Newsletter gratuite

Recevez des conseils sur la gamification chaque semaine

Rejoignez les responsables de marque et les marketeurs qui reçoivent nos meilleurs contenus sur les jeux de marque, les stratégies d'engagement et le marketing interactif — sans spam, désabonnement à tout moment.

Pas de spam. Désabonnement à tout moment. Nous respectons votre boîte mail.

Créons quelque chose que votre audience va adorer

Des jeux de marque aux expériences 3D immersives — nous donnons vie à l'engagement.