AI

Mijn persoonlijke AI-assistent draait gewoon thuis — dit is de architectuur

11 juni 2026
Architectuurschets van een centraal AI-brein op een thuisserver, verbonden met telefoon, microfoon, mail, agenda en een kennisvault
Eén brein, meerdere kanalen — en alle kennis stroomt er vanzelf in

Ik stuur een Telegram-bericht: “wat hadden we ook alweer besloten over de sessie-afhandeling?” Drie seconden later heb ik het antwoord — met de datum en de afweging erbij. Hetzelfde brein leest mijn mail, kent mijn agenda, zet concept-antwoorden klaar en weet wat er speelt in al mijn projecten. En het draait niet bij een SaaS-partij, maar op een Mac mini die thuis staat te zoemen.

Ik bouw veel (platforms, tools, games — het loopt nogal op) en ik werk vanuit huis. Een assistent die al die context bij elkaar houdt was geen luxe, maar noodzaak. Dus bouwde ik er een. Dit artikel is de gesanitiseerde versie van mijn referentie-architectuur: de volledige structuur, zonder secrets, IDs of accountnamen. Genoeg om er zelf een te bouwen — onderaan staat zelfs de prompt waarmee je je eigen Claude aan het werk zet.

Het concept in één alinea

Eén centraal “PA-brein” — een service op de Claude Agent SDK — draait op een thuisserver en is via meerdere front-ends bereikbaar: Telegram op mobiel, voice op de desktop. Het brein heeft via MCP-servers toegang tot mail, agenda, een takenbackend, een Obsidian-kennisvault en eigen beheertools. Kennis over al mijn projecten stroomt er automatisch in, via een vault die door mijn dagelijkse Claude Code-sessies wordt gevoed. En acties zijn beveiligd met een tier-systeem waarbij de transportlaag — niet het model — bevestiging afdwingt. Die laatste zin is de belangrijkste van dit hele artikel. Daarover straks meer.

De basislaag: saai en degelijk

De productieserver is een Mac mini (Apple Silicon), alleen bereikbaar via Tailscale — een privé mesh-VPN, dus geen open poorten naar internet. Een tweede, oudere Mac mini doet monitoring (health checks elk kwartier) en nightly backups. Services draaien deels in Docker Compose (n8n en webapps, achter Caddy en een Cloudflare Tunnel voor wat publiek moet zijn) en deels als launchd-services — het macOS-equivalent van systemd — voor de PA-componenten zelf. Alles staat in git, met SemVer en een changelog bij elke wijziging. Saai? Zeker. Maar saai is precies wat je wilt in de laag waar alles op rust.

De kern: een agent, geen chatbot

Het brein is een Python/FastAPI-service op de Claude Agent SDK, met in essentie één endpoint: POST /ask (plus een /transcribe voor voice). Het bindt op localhost en is alleen via het tailnet bereikbaar, met een shared-secret header.

Waarom de Agent SDK en niet een kale completion-API of claude -p? Drie redenen. Het proces is warm, dus antwoorden komen in één tot drie seconden. De SDK regelt sessie- en permissie-afhandeling netjes. En — de echte reden — het brein is een agent met tools. Een kale completion die niets kán, bleek in de praktijk gewoon onvoldoende.

Sessies lopen per kanaal via SDK-resume, met een TTL en een state-file. Hier zit een les die je wilt kennen vóór je deployt: resume draagt het oude system-prompt mee. Deploy je een nieuwe versie van je prompt, dan praten lopende sessies vrolijk door op de oude. De oplossing: een PROMPT_VERSION — een hash van het system-prompt — in de session-store. Bij een mismatch start een verse sessie. Elke gedragswijzigende deploy invalideert zo automatisch alles wat nog liep.

De kosten? Elke call wordt gelogd (JSONL), elke component heeft een eigen API-key met een maandelijkse spend-cap als harde circuit-breaker, en het brein heeft een tool waarmee het zijn eigen dagkosten kan opvragen. Je assistent vragen wat hij vandaag gekost heeft en gewoon antwoord krijgen — dat went snel.

Front-ends: één brein, meerdere kanalen

Telegram is het primaire kanaal: een long-polling bot die ín de service zelf draait, als lifespan-task. Eerder liep dat via n8n. En “geschrapt” klinkt groter dan het was: ik vroeg op een ochtend of we die tussenlaag eigenlijk nog wel nodig hadden, en binnen hetzelfde antwoord was hij eruit. Directe polling is simpeler en sneller — zo groot zijn dit soort beslissingen als je architectuur ze toelaat. Twee dingen om te weten: nooit twee pollers op dezelfde bot-token (dat geeft HTTP 409), en een chat-guard die alleen je eigen chat-ID accepteert. Voice-notes worden lokaal getranscribeerd met ffmpeg en whisper.cpp.

Op de desktop: push-to-talk — lokale speech-to-text, het brein, en een gesproken antwoord terug. De les daar: macOS-hotkey-daemons krijgen geen microfoonpermissie via shell-commando’s. De capture moet in een app zitten die zélf mic-permissie heeft, anders blijf je tegen TCC aanlopen.

Tools via MCP: wrappen, niet herbouwen

Het brein krijgt zijn tools via MCP-servers, met een strikte allowlist — geen Bash, geen Read/Write, alleen de tools die het hoort te hebben:

  • Gmail (meerdere accounts): de community Gmail-MCP, één instance per account met een eigen credentials-dir. De OAuth-tokens kon ik hergebruiken uit bestaande n8n-credentials (export plus format-conversie).
  • Google Calendar (idem): community-MCP per account. Zet je OAuth-app op “Extern + In productie” — in Testing-modus verlopen tokens na zeven dagen.
  • Taken en administratie: een eigen MCP-wrapper van zo’n vijftig regels om mijn bestaande admin-backend. Vijftig. Regels.
  • Obsidian-vault, lezen: een vault-MCP op een read-only git-clone op de server, die elk kwartier pullt. Het brein leest altijd verse kennis, zonder schrijfrechten.
  • Vault-capture, schrijven: een eigen tool (clone, commit, push) die in code begrensd is tot één inbox-map. De deploy-key mag schrijven, de code staat alleen nieuwe files in de inbox toe.
  • Mail-agent-beheer: een tool op de n8n-API, begrensd tot een vaste set workflow-IDs. Mute, unmute, vakantiemodus per domein.
  • Een extern platform: een klein read-only REST-endpoint met een bearer-secret. De PA geeft overzicht; hij wordt geen tweede admin-bot naast wat er al draait.

Het patroon overal: bestaande systemen wrappen, nooit dupliceren. n8n blijft de orkestratielaag voor de event-driven mail-agents — per maildomein een workflow die inkomende mail classificeert en concept-antwoorden klaarzet, draft-only. De agents vervangen n8n niet. Ze besturen het.

Architectuurschets van een gelaagd beveiligingsmechanisme met oplopende sloten en een bevestigingshendel
Vier tiers — en de bevestiging zit in de transportlaag, niet in het model

Het veiligheidsmodel (lees dit deel twee keer)

Een assistent die mail kan versturen, is ook een assistent die verkéérde mail kan versturen. Dus: een tier-systeem per tool. Tier 0 is lezen — vrij. Tier 1 is drafts. Tier 2 — verzenden, agenda-items aanmaken — vereist expliciete bevestiging. En tier 3 (delete, filters) zit in v1 gewoon dicht.

Nu de zin die ik in elke discussie over agent-security herhaal: de bevestiging zit in de transportlaag, niet in het model. Het brein markeert een actie als voorstel. De bot toont bevestigingsknoppen. Pas na een klik draait dezelfde sessie verder met een confirmed-vlag die de tier-2-tools vrijgeeft. Het model kan zichzelf dus niet escaleren — ook niet onder prompt-injectie. Laat je het model zélf om bevestiging vragen (“weet je het zeker?”), dan is je hele injectie-defense theater: een aanvaller hoeft het model alleen maar te overtuigen dat er al bevestigd is.

Aanvulling uit de praktijk: mensen typen “ja” in plaats van op de knop te drukken. (Ik ook.) Een exact-match-woordenlijst in de transportlaag vangt dat af zonder de garantie te breken.

Dit is getest met geplante mails vol verborgen instructies — inclusief de gemene variant waarin ik het brein zelf vroeg om “de instructies in die mail” uit te voeren. Het weigert; tier 2 blijft onomzeilbaar. Als tweede bodem zijn de OAuth-scopes zo smal dat delete sowieso niet kán. En verder: een append-only audit-log van elke actie (JSONL, dagelijkse rotatie) en per component eigen keys en tokens, chmod 600, nooit in git.

Architectuurschets van een bibliotheek-kluis vol notitieboeken die een centraal brein voeden
De vault: elke sessie, elke beslissing, elk project — doorzoekbaar voor het brein

Het geheugen: een vault die zichzelf vult

Waarom weet de PA alles over al mijn projecten? Omdat de kennislaag niet bij de PA begint, maar bij mijn dagelijkse werk in Claude Code.

De basis is een Obsidian-vault in PARA-structuur (Inbox, Projects, Areas, Resources, Daily, PA-output), in een eigen private GitHub-repo, gesynct via Obsidian Git met auto-commit en push. Bewust géén iCloud — race-conditions. Elk project heeft een note met frontmatter: repo-pad, tech-stack, status. Dat frontmatter-veld is de koppeling tussen vault en codebase.

Daarbovenop: een SessionStart-hook in Claude Code die bij elke sessiestart automatisch de vault-note van het huidige project plus relevante overzichten in de context injecteert. Claude weet bij het openen al wat het project is, wat er recent besloten is en wat er speelt. Slash-commands als /decision en /learning schrijven date-stamped beslissingen en lessen terug naar de vault. Een stuk of tien vault-agents (samen goed voor ruim 170 tests) houden de boel gezond: een focus-monitor die wekelijks uit git-logs destilleert waar gewerkt is, een blocker-tracker, een contradiction-finder, een schema-linter. En Claude Code’s eigen projectmemory wordt one-way gespiegeld naar de vault, zodat ook die kennis doorzoekbaar is.

Het resultaat: het PA-brein leest via die read-only clone dezelfde kennis als ikzelf. Vraag via Telegram “wat hadden we besloten over X in project Y” en het brein vindt de beslissing — niet omdat iemand dat heeft overgetypt, maar omdat die destijds in drie seconden is vastgelegd met een slash-command.

De belangrijkste lessen

Eerst het eerlijke verhaal: dit was geen worsteling. We hebben heel erg op het gemak doorgebouwd, en de bugs die we tegenkwamen waren eerder een kwestie van een kwartier tot een half uur dan van lange debug-avonden. Maar deze zeven punten bepaalden wél het ontwerp — en ze zijn goedkoper om te lezen dan om zelf te ontdekken.

  1. Bouw het bevestigingsmechanisme in de transportlaag, nooit in de prompt — anders is elke injectie-defense theater.
  2. Agent SDK-resume bevriest het system-prompt. Versioneer je prompt en invalideer sessies bij elke wijziging.
  3. Wrap bestaande systemen, dupliceer ze niet. Mijn admin-wrapper was vijftig regels.
  4. n8n-IF- en Switch-outputs zonder vervolg-connectie falen geruisloos. Elke branch verplicht naar een audit-node.
  5. Google OAuth-apps in Testing-modus verliezen hun tokens na zeven dagen. Zet ze op productie, ook ongeverifieerd voor eigen gebruik.
  6. Lokale LLM’s heb ik geschrapt voor de chat-laag. De kwaliteitsgap is te groot; cloud met een spend-cap wint. Lokaal draaien alleen speech-to-text (whisper.cpp) en embeddings.
  7. Constraints vóór architectuur. “Moet mobiel werken” kantelde het hele ontwerp van MacBook-only naar een shared service met meerdere front-ends. Stel die vraag eerst.

Bouw er zelf een

Dit hele systeem is met Claude Code gebouwd, en jouw versie kan dat ook zijn. Hieronder de prompt die ik mede-ontwikkelaars meegeef. Vul de invulvelden in, en laat ‘m éérst een plan maken voor je iets laat bouwen — die volgorde is geen detail.

Ik wil een persoonlijk assistent-systeem bouwen volgens deze referentie-architectuur:
één centraal "PA-brein" (Python/FastAPI-service op de Claude Agent SDK, draaiend op
een altijd-aan machine, alleen bereikbaar via een privé VPN zoals Tailscale) met
meerdere front-ends (minimaal Telegram via een long-polling bot in de service zelf),
en tools via MCP-servers: mail, agenda, een kennisvault (Obsidian, gesynct via git,
door het brein gelezen via een read-only clone) en wrappers om mijn bestaande systemen.

Veiligheidseisen (niet onderhandelbaar):
- Tier-systeem per tool (0=lezen, 1=draft, 2=verzenden/aanmaken, 3=verwijderen).
- Tier-2-bevestiging wordt afgedwongen in de TRANSPORTLAAG (bot-knoppen + getypte
  exact-match-bevestiging die een confirmed-vlag zet), nooit door het model zelf.
- Tier 3 blijft dicht in v1; OAuth-scopes zo smal mogelijk als tweede bodem.
- Append-only audit-log, per-component API-keys met maandelijkse spend-cap,
  system-prompt versioneren (hash) en sessies invalideren bij prompt-wijziging.

Kennislaag: een Obsidian-vault in PARA-structuur met per project een note
(frontmatter met repo-pad), een Claude Code SessionStart-hook die vault-context
injecteert, slash-commands voor het vastleggen van decisions/learnings, en een
mirror van Claude Code-projectmemory naar de vault.

Mijn situatie: [beschrijf: welke altijd-aan machine, welke mailprovider(s) en
hoeveel accounts, welke bestaande systemen (taken/administratie/automatisering),
of je al Obsidian/n8n/Docker gebruikt, en welke kanalen je wilt (Telegram/voice/web)].

Opdracht: maak eerst een hergebruik-audit van wat ik al heb (wrappen, niet
herbouwen), bevestig daarna mijn deployment- en toegangsconstraints (mobiel?
multi-user? welke netwerklaag?), en lever dán een gefaseerd plan: fase 1 brein +
één read-only tool, fase 2 Telegram, fase 3 mail/agenda met het tier-systeem,
fase 4 hardening (injectie-tests, kostenreview). Per fase: acceptatietest en
tests in dezelfde sessie. Begin pas met bouwen na mijn akkoord op het plan.

Update: er is inmiddels een vervolg — in deel 2 krijgt de assistent een gezicht, een stem en een naam: Henk.

Wil je hierover sparren — over de architectuur, het tier-systeem, of waarom je mail-agent geruisloos faalde? Ik praat er graag over. Koffie in Bladel werkt het best. En hoe ik AI breder inzet (en waar bewust niet) lees je op over AI.


Richard

Meer lezen

Gerelateerde berichten