LirafyBloggWendo ↗
Svenska

Blogg

En mobilapp utan backend: aviseringar och godkännanden i Claude Code via SSH och filer

Wendo är en mobilapp utan backend: det finns ingen server från oss mellan din dator och din telefon. När Claude Code ber om lov eller blir klar med en uppgift skriver ett skalskript på din maskin ned händelsen i en fil, och appen läser den via SSH med samma nyckel som den använder för att öppna terminalen. Svaret på ett godkännande går tillbaka samma väg, som en till fil.

Vill du ställa in det finns det i guiden om Claude Code-aviseringar på mobilen.

Varför en mobilapp utan backend

Wendo är en SSH-klient: för att den ska fungera måste din mobil redan nå din dator och autentisera sig mot den. Med den kanalen öppen och krypterad vore en server i mitten för aviseringarna en del för mycket.

Att ta bort den har tre följder som vi var ute efter:

  • Det din agent gör (kommandot den vill köra, filen den ska ändra, det senaste meddelandet) passerar inte någon maskin hos oss. Det stannar på din dator tills din mobil frågar efter det.
  • Det finns inga konton. Ingen e-post att spara och inget lösenord som kan läcka. Identiteten är din SSH-nyckel.
  • Det finns ingen infrastruktur att betala för eller underhålla för varje ny användare.

I gengäld måste allt en server skulle göra (spara historik, avisera när appen är stängd, para ihop enheter, kontrollera köp) lösas med det som redan finns i båda ändar.

Kontraktet: en JSON-rad per händelse

Kärnan i allt är ~/.wendo/events.jsonl. Installationsprogrammet registrerar wendo-hook.sh för sex händelser i Claude Code (PermissionRequest, Notification, Stop, UserPromptSubmit, SessionStart och SessionEnd), och varje gång en av dem utlöses läser hooken den JSON som Claude Code skickar på standard in och lägger till en rad i filen. En behörighetsrad ser ungefär ut så här (förkortad):

{"id":"1793530000-4242-12345","time":1793530000,"host":"portatil","agent":"claude",
 "event":"PermissionRequest","category":"approval_required","kind":"permission",
 "title":"Claude Code — wendo","message":"Bash: flutter test","sessionId":"…",
 "toolName":"Bash","pendingActionId":"p1793530000-4242-6789",
 "expiresAt":1793530090000,"options":["Approve","Deny"],
 "mux":{"kind":"tmux","session":"dev","window":"1","pane":"%3","socket":"/tmp/tmux-501/default"}}

Filen växer bara i slutet. Ingen redigerar rader: ett besvarat godkännande skrivs som en ny rad av typen resolution. Det förenklar samtidigheten mycket, eftersom flera hooks kan skriva samtidigt och korta rader läggs till atomiskt. När filen passerar 1000 rader behåller hooken de 500 sista (med flock om det finns, så att bara en roterar åt gången). Mappen skapas med umask 077 och filen får 600: bara din användare kan läsa den.

Fältet mux anger i vilken panel i tmux, zellij eller herdr agenten kör, så att appen kan ta dig dit.

Hur appen läser: pollning via SSH

Appen tar inte emot något, den frågar. Vid varje läsning öppnar den en SSH-anslutning till varje värd som har inkorgen påslagen och kör, utan terminal, något så enkelt som det här:

tail -n 1000 ~/.wendo/events.jsonl 2>/dev/null; true

Hur ofta beror på inkorgens tillstånd. Medan någon rad väntar på svar läser appen var 5:e sekund, eftersom hooken snart går ut. När agenter arbetar, var 15:e. Är inget aktivt läser den bara när du öppnar fliken. Efter att du har svarat läser den efter 1,2 sekunder och igen efter 3 för att se resultatet.

Med raderna i handen sorterar inbox.dart dem efter tid och viker ihop dem till rader i inkorgen: en per värd och session. Där bor nästan all logik som i en annan app skulle ligga på en server. Den avgör till exempel att en begäran besvarades i terminalen om en Stop eller en UserPromptSubmit kommer efteråt utan något svar emellan, eller att en SessionStart på grund av komprimering inte ändrar något.

Eftersom filen bara går att lägga till i betyder radering i appen att komma ihåg vad som raderades. Wendo sparar ”gravstenar” på själva mobilen, per rad eller per värd. Skriver agenten i den sessionen igen dyker raden upp igen. Svarar inte en värd behåller appen det den senast läste från den i stället för att tömma listan.

När appen är stängd på Android

På Android finns ett WorkManager-jobb som vaknar högst var 15:e minut (systemet tillåter inte kortare intervall och kan skjuta upp det för att spara batteri), läser inkorgen precis som den öppna appen och skickar lokala aviseringar om det som är nytt. Den här läsningen ber aldrig om biometri: är din SSH-nyckel skyddad och låst misslyckas den värden, helt enkelt. Det som redan har aviserats sparas i mobilens lagring, så att den öppna appen och bakgrundsjobbet inte upprepar samma avisering. På iOS finns inget sådant jobb.

Ett godkännande fram och tillbaka

PermissionRequest är den enda händelse där hooken stannar och väntar. Hela vägen:

  1. Hooken skapar ett pendingActionId (bokstäver, siffror, _ och -; appen validerar det på samma sätt innan den använder det i en sökväg), räknar ut expiresAt och skriver raden med options: ["Approve","Deny"].
  2. Appen ser en rad med pendingActionId, utan svar och inte utgången, och visar knapparna.
  3. När du trycker på en skriver appen {"decision":"approve"} till ~/.wendo/answers/<id>.tmp och byter namn på den till ~/.wendo/answers/<id>. Namnbytet gör att hooken aldrig läser ett halvskrivet svar.
  4. Hooken tittar efter filen varje halv sekund. När den hittar den skriver den ut beslutet i det format Claude Code förväntar sig och lägger till sin svarsrad:
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}

Det känsliga är vad som händer när du inte svarar. Väntan varar 90 sekunder som standard (WENDO_APPROVAL_TIMEOUT). När tiden går ut skriver hooken inget, och Claude Code, utan svar från hooken, ställer sin vanliga fråga i terminalen. Hooken noterar ett timeout-svar och avslutas. Svarar du tidigare i terminalen avbryter Claude Code hooken, och en trap noterar svaret som local.

Designregeln var att ett fel på mobilen aldrig skulle lämna agenten blockerad. Utan batteri, utan täckning eller med appen stängd är det värsta fallet detsamma som alltid: frågan i terminalen.

Det finns en lucka kvar: att svara från mobilen precis när hooken redan har gett upp. För att täcka den väntar samma SSH-kommando, efter att ha lämnat svaret, upp till 3 sekunder på en svarsrad med det id:t. Är svaret timeout, eller hämtar ingen filen, tar kommandot bort den och appen säger ”Utgånget — värden faller tillbaka lokalt” i stället för att räkna det som skickat.

Wendos inkorg med ett väntande godkännande från Claude Code (npm test i my-app) och knapparna Neka och Godkänn Ett godkännandes väg: Claude Code ber om tillstånd, hooken skriver det i events.jsonl, Wendo läser det över SSH, du svarar på mobilen och hooken lämnar allow eller deny tillbaka till Claude Code

Pushnotiser utan egen server: ntfy, och det är valfritt

För att avisera när appen är stängd på iOS, eller tidigare än de 15 minuterna på Android, behövs någon som skickar ut aviseringen, och det kräver en push-tjänst. Vi valde ntfy eftersom man publicerar med en HTTP-förfrågan och kan drifta den själv.

Om du sätter NTFY_TOPIC publicerar hooken aviseringar om behörigheter, notiser och avslutade uppgifter (inte start eller slut på sessioner, och inte varje prompt). Aviseringen innehåller en länk wendo://attach?event=…&host=…&sid=… med multiplexerns panel, så att appen öppnar exakt rätt ställe när du trycker på den.

Appen läser också topicen (/json?poll=1&since=24h) som ytterligare en källa för inkorgen, men det som läses via SSH gäller. ntfy vet bara att något behövde dig, inte om det redan har lösts, så om SSH-läsningen från en värd misslyckas räknas inte dess behörighetsaviseringar från ntfy. Och här finns integritetskostnaden: på den publika servern fungerar topicen som lösenord, och aviseringen innehåller projektet och ett utdrag ur kommandot. Därför måste topicen vara lång och slumpmässig, och därför finns NTFY_URL för att använda en egen server.

Para ihop mobilen med datorn via QR, utan server

Utan konton, hur lägger mobilen till en dator? Med en QR-kod som wendo-pair.sh genererar och som innehåller en URL:

wendo://pair?h=<host>&p=<puerto>&u=<usuario>&n=<nombre>&hk=<huella>

hk är SHA256-fingeravtrycket för värdens ed25519-nyckel, läst med ssh-keyscan mot sshd på den porten. Appen validerar det med ett strikt mönster (SHA256: och 43 tecken base64) och låser det för den värden. Den första anslutningen verifieras i stället för att accepteras blint, och om servern en annan dag visar en annan nyckel avvisas anslutningen. En QR-kod utan fingeravtryck fungerar fortfarande, med förtroende vid första anslutningen. En med felformaterat fingeravtryck gör det inte. Sista steget är att installera mobilens publika nyckel i authorized_keys med kommandot som appen visar. Inget av det lämnar ditt nätverk.

Pro utan licensserver

Att sälja en prenumeration utan backend är den minst bekväma delen. Wendo Pro debiteras via Google Plays och App Stores betalningar, och kontrollen bor i native-modulen, inte i Dart. På Android (ProLicense.kt) kommer varje köp med sin JSON och en RSA-signatur gjord med appens licensnyckel, och den verifieras med den publika nyckeln. Vi kontrollerar också att appen inte har signerats om, och tillståndet sparas förseglat med en nyckel från Keystore, så att redigera en fil ger inte Pro. Varje fullständig synk med butiken förnyar det. Utan den håller Pro i 7 dagar med prenumeration eller 30 med livstidsköpet, och att ställa tillbaka klockan förlänger det inte. På iOS verifierar native-koden med StoreKit 2. De 5 testgodkännandena i gratisplanen räknas också av native-koden.

Koden säger själv hur långt det räcker: med root och Frida, eller genom att patcha appen, går allt att kringgå, eftersom det utan en server som frågar Google inte finns något sätt att förhindra det. Målet är mer blygsamt: att det inte ska räcka med ett verktyg med ett klick.

De verkliga begränsningarna för en app utan backend

  • Mobilen måste nå din dator. På samma wifi räcker den lokala IP-adressen, utifrån behöver du något som Tailscale. Är datorn avstängd eller i vila har appen ingen att fråga.
  • Aviseringarna dröjer så länge pollningen dröjer: några sekunder med appen öppen (5 om något väntar på svar, 15 om bara agenter arbetar), upp till 15 minuter eller mer i bakgrunden på Android, och på iOS med appen stängd kommer de bara via ntfy.
  • Varje läsning är en SSH-anslutning. Med flera värdar blir det flera anslutningar med några sekunders mellanrum medan något väntar.
  • Det du raderar på en mobil kommer bara den mobilen ihåg.
  • Hooken behöver jq på datorn och registreras än så länge bara i Claude Code.
  • Den publika ntfy-servern ser det du publicerar i topicen. Stör det dig, drifta en egen.

Vill du se hur de här delarna passar ihop i appen finns resten (terminal, tmux-arbetsytor, chattvy) på Wendos sida, och referensen för hookens variabler finns i dokumentationen om aviseringar och godkännanden. SSH-delen som allt det här vilar på beskriver vi i SSH, Mosh och Eternal Terminal i Flutter, och en annan del som klarar sig utan server, dikteringen, i Whisper på mobilen utan internet. Wendo finns ännu inte i butikerna.