Claude Code cross-session messaging: sessies die elkaar berichten sturen
12m leestijd

Claude Code cross-session messaging: sessies die elkaar berichten sturen

Met cross-session messaging stuurt de ene Claude Code-sessie een bericht naar de andere. Hoe je het gebruikt, wat een bericht wel en niet mag zodra het aankomt, en welke instellingen bepalen of het überhaupt bezorgd wordt.

Twee terminals open. In de ene sessie is net een kolom hernoemd. De andere is intussen in drie bestanden bezig met code die de oude naam gebruikt.

Normaal merk je dat pas als de tests omvallen. Daarna wissel je van venster en leg je nog eens uit wat je een half uur eerder al had uitgelegd.

Sinds v2.1.224 kan die eerste sessie het de tweede zelf vertellen.

text
Peer sessions (1):
  chaos-mcp-e7 [15619a]  ·  interactive  ·  busy  ·  started 3h ago

Dat is deze machine. Twee Claude Code-sessies die allebei weten dat de ander er is.

Wat cross-session messaging precies is

Tekst. Meer zit er niet in.

De Claude van de ene sessie schrijft een zin, de Claude van de andere sessie leest hem. Geen gespreksgeschiedenis, geen bestanden, geen context. De ontvanger krijgt exact wat de afzender heeft ingetypt en verder niets.

Twee tools doen het werk. ListAgents zoekt uit welke sessies bereikbaar zijn, SendMessage bezorgt een bericht bij één daarvan op naam. Geen van beide roep je zelf aan. Jij praat met Claude, Claude verstuurt.

Eerst de voorwaarden, want daar zit vrijwel elk geval van "het werkt niet":

  • v2.1.224 of nieuwer. Controleer met claude --version.
  • macOS of Linux, WSL 2 inbegrepen. Op native Windows bestaat het niet.
  • Niet op Bedrock, Claude Platform op AWS, Google Cloud's Agent Platform of Microsoft Foundry.
  • De feature-flag-evaluatie moet aan staan. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_TELEMETRY, DO_NOT_TRACK en DISABLE_GROWTHBOOK zetten allemaal de flag-evaluatie uit die deze functie nodig heeft. Zet er één van aan, in je shell of in een env-blok in je settings, en messaging staat uit zonder dat je er iets van merkt.

Je controleert het in één seconde met /list-agents, ook te typen als /peers. Kent de sessie het commando niet, dan heeft die de functie niet. Werkt het wel, dan staat messaging aan en heeft elk probleem daarna een specifiekere oorzaak.

Een bericht sturen

Je adresseert nooit zelf iets. Je beschrijft wat er moet gebeuren:

text
Ask the session in my other terminal whether the migration finished

Of je laat de formulering helemaal aan Claude over:

text
Tell the session working on the payments API what we just changed

Claude kiest de ontvanger, schrijft de tekst en verstuurt. Aan de andere kant komt er één regel binnen:

text
Schema migration finished: the new column is tenant_id, and rebasing on main is safe now.

Het bericht komt binnen met de naam van de afzender en een antwoordadres. Zit de ontvangende Claude midden in een beurt, dan wordt het in de wachtrij gezet. Ligt die sessie stil, dan begint er meteen een nieuwe beurt. Zodra Claude het gelezen heeft, klapt het in tot een regel Message from, die je met Ctrl+O weer uitklapt.

Claude stuurt ook uit zichzelf een bericht, als hij doorheeft dat hij net iets veranderd heeft waar een andere sessie op voortbouwt.

De naam is het adres

Elke sessie luistert naar een naam.

bash
claude --name payments

Of /rename tijdens de sessie. Stel je niets in, dan leidt Claude Code er zelf een af uit de mapnaam plus een korte hash. De sessie waarin deze post geschreven is, staat geregistreerd als tim-vitepress-38.

Twee sessies in verschillende mappen kunnen dezelfde naam krijgen. /list-agents toont per lokale sessie de werkmap zodat je ze uit elkaar houdt, en in Claudes eigen overzicht staat de korte identifier die je bovenaan deze post ziet. Die gebruikt hij in het adres zodra namen botsen.

Het registratiebestand en de inbox-socket

Even doorlezen loont hier, want hier zit de verklaring voor elk geval van twee sessies die elkaar niet zien.

Elke sessie schrijft een klein JSON-bestand en opent een Unix-socket. Dit is de registratie van deze sessie, letterlijk:

json
{
  "pid": 1318539,
  "sessionId": "56593ac1-515e-4e33-b078-0810410f98b0",
  "cwd": "/root/tim-vitepress",
  "version": "2.1.226",
  "peerProtocol": 1,
  "kind": "interactive",
  "messagingSocketPath": "/run/user/0/cc-socks/1318539.sock",
  "name": "tim-vitepress-38",
  "nameSource": "derived",
  "status": "busy"
}

Sessies vinden elkaar dus via het bestandssysteem. Twee sessies bereiken elkaar alleen als ze dezelfde bestanden kunnen zien. Een sessie in een container en een sessie op de host komen nooit bij elkaar, wat je ook instelt, omdat die container zijn eigen bestandssysteem heeft. Twee sessies binnen dezelfde container bereiken elkaar wel, ook op een self-hosted runner.

De socket is per sessie en alleen toegankelijk voor je eigen systeemgebruiker:

text
srw------- 1 root root 0 Aug  9 15:14 1318539.sock

Op een gedeelde machine komen de sessies van een andere gebruiker er dus niet bij.

Je vindt die van jezelf op twee plekken: de regel Peer address in /status, met uds: ervoor, en de omgevingsvariabele CLAUDE_CODE_MESSAGING_SOCKET. Claude Code exporteert die naar elke hook en elk Bash-commando voordat er iets draait, SessionStart inbegrepen, en elke sessie exporteert zijn eigen socket in plaats van die van een bovenliggende sessie.

Op die variabele valt iets te bouwen. Een hook of een gewoon shellscript kan een bericht plaatsen in de sessie waar het bij hoort. Een lange testrun is klaar en laat een regel achter in de terminal die erop wacht. Een deployscript rapporteert terug zonder dat jij de logs zit te volgen.

Lees wel eerst de platformverschillen. Als er geen crossSessionInbound-waarde van toepassing is, bezorgt Claude Code een bericht waarvan hij kan vaststellen dat het van een kindproces van de sessie zelf komt. Op Linux, ook binnen WSL 2, lukt die controle zelfs nadat het kindproces is gestopt. Op macOS alleen zolang het verzendende proces nog draait. In een container waar Claude Code als PID 1 draait helemaal niet. Kan hij het niet vaststellen, dan behandelt hij het bericht als elk ander bericht zonder aangegeven permissieklasse, en houdt een sessie die permissievragen overslaat het vast tot jij het goedkeurt.

Een bericht is geen toestemming

Dit is het deel waar de meeste ontwerpaandacht in is gaan zitten, en het deel dat elke "mijn agents praten nu met elkaar"-post gaat overslaan.

De ontvangende Claude hoort er meteen bij van wie het bericht komt: een andere Claude-sessie, geen mens. Dat onderscheid wordt afgedwongen:

  • Het keurt niets goed. Een bericht van een andere sessie geldt nooit als jouw toestemming, dus het kan geen openstaande permissievraag namens jou beantwoorden.
  • Het verandert geen configuratie. De ontvangende Claude krijgt de instructie om nooit permissie-instellingen, CLAUDE.md of iets anders configuratie-achtigs aan te passen omdat een andere sessie erom vroeg.
  • Commando's komen aan als tekst. Een /compact in de tekst van het bericht is acht tekens. Claude Code voert het nooit uit.
  • Permissievragen komen gewoon langs. Wat er naar aanleiding van het bericht gebeurt, vraagt dezelfde goedkeuringen als werk dat jij zelf hebt gevraagd.

Ook de verzendkant is begrensd. Claude krijgt te horen dat hij een andere sessie nooit mag vragen om iets dat in zijn eigen sessie geweigerd of geblokkeerd is, of dat zijn eigen permissieregels zouden tegenhouden, en dat hij dat werk in plaats daarvan bij jou neerlegt.

Lees die twee samen en het dreigingsmodel spreekt voor zich: een sessie met minder beperkingen die het werk witwast voor een sessie met meer. Deze functie is gebouwd op de aanname dat één van je eigen agents dat vroeg of laat probeert.

Dezelfde release maakt dat punt nog een keer, in een regel waar de meeste artikelen overheen lazen. Uit de changelog van 2.1.224:

text
Fixed sandbox filesystem deny entries written with a trailing slash
(e.g. denyRead: "~/.aws/") being silently bypassable on Linux and macOS

Elke denyRead- of denyWrite-regel die je met een slash op het eind had geschreven, was dus versiering. Geen foutmelding, geen waarschuwing, en de credentials waarvan je dacht dat ze afgeschermd waren lagen al die tijd open. Draaien je sessies in een sandbox en eindigt een van die paden op een slash, dan is dat op zichzelf al reden genoeg om te updaten. Kijk daar even naar voordat je verderleest.

Bezorgd, vastgehouden of geweigerd

Tussen twee gewone interactieve sessies met standaardinstellingen komt een bericht zonder omhaal aan. Andere configuraties houden het vast of gooien het weg, en dat verschil telt zodra je iets gaat scripten.

Elk binnenkomend bericht wordt getoetst aan de inbound-instellingen van de ontvanger en eindigt in één van drie toestanden:

UitkomstWat er gebeurt
BezorgdClaude Code geeft de tekst door aan de ontvangende Claude
VastgehoudenApart gezet en niet bezorgd, tot jij het goedkeurt of de instellingen veranderen
GeweigerdWeggegooid zonder bezorging

crossSessionInbound bepaalt welke: accept, hold of refuse.

Staat er niets ingesteld, dan beslist de standaard per bericht, en wel op basis van permissiemodus. Sessies vallen uiteen in twee klassen: de sessies die permissievragen overslaan, en alle andere. Plan mode telt als overslaan in sessies waar bypass permissions beschikbaar zijn. auto, acceptEdits en dontAsk gelden alle drie als vragend.

  • De ontvanger stelt permissievragen: berichten worden bezorgd, behalve een bericht van een afzender die aangeeft dat hij permissievragen overslaat. Dat wordt voor jou vastgehouden.
  • De ontvanger slaat permissievragen over: berichten worden voor jou vastgehouden, behalve een bericht van een afzender die ze ook overslaat.

De sessie met --dangerously-skip-permissions gaat aan beide kanten in quarantaine. Terecht.

Een vastgehouden bericht opent in de ontvangende sessie een goedkeuringsvenster met de afzender en een voorvertoning van de tekst. Goedkeuren bezorgt dat ene bericht, weigeren gooit het weg, en negeren gooit het ook weg zodra dialogExpiry verstrijkt, standaard na vijf minuten. Verandert de permissieklasse van de sessie terwijl er berichten vastgehouden worden, dan gelden de regels opnieuw, dus vastgehouden berichten kunnen alsnog vrijkomen. Zet je crossSessionInbound op refuse terwijl er berichten wachten, dan gaan ze allemaal weg en krijgt elke bereikbare afzender een weigering terug.

Per sessie worden er maximaal 100 berichten vastgehouden en maximaal 50 geaccepteerde maar ongelezen berichten bewaard. Herhalingen van dezelfde afzender worden afgeknepen, en identieke herhalingen binnen een korte periode gaan direct de prullenbak in. Daarom kunnen twee sessies niet eindeloos tegen elkaar aan blijven praten.

De voorrang loopt bewust maar één kant op

Bij de meeste instellingen in Claude Code mag een projectbestand je gebruikersinstellingen overschrijven. Bij deze instelling niet:

Claude Code reads managed settings first, then the --settings flag, then user settings, and applies the first value found; a value in project or local settings applies only when it's stricter, on the accept < hold < refuse ladder, than the value those trusted sources give.

Een ingecheckt projectbestand mag dus aanscherpen en nooit versoepelen. Zet geen van de vertrouwde bronnen een waarde, dan geldt een hold of refuse uit project- of lokale instellingen alsnog, in plaats van de standaard per bericht.

Isolatie tussen machines heeft dezelfde vorm:

json
{
  "isolatePeerMachines": true
}

Een true uit welke laag dan ook telt, dus een projectbestand kan de eis aanzetten en niets kan hem uitzetten. Staat hij aan, dan vraagt Claude om je goedkeuring voordat een antwoord de machine verlaat, ook in bypassPermissions-modus. Voor berichten binnen dezelfde machine vraagt hij niets.

Uitzetten

Ontvangen en versturen zijn losse knoppen, dus je kunt de ene richting dichtzetten en de andere open laten.

Stoppen met ontvangen:

json
{
  "crossSessionInbound": "refuse"
}

Stoppen met versturen en met het opsommen van sessies, met deny-regels die alleen de kale toolnaam bevatten, zonder verdere specificatie:

json
{
  "permissions": {
    "deny": ["SendMessage", "ListAgents"]
  }
}

Allebei samen, in managed settings, is de uit-knop voor de hele organisatie:

json
{
  "permissions": {
    "deny": ["SendMessage", "ListAgents"]
  },
  "crossSessionInbound": "refuse"
}

Twee dingen om te weten voor je dat uitrolt. Met SendMessage op deny verdwijnt ook messaging naar subagents en teamgenoten in een agent team, want het is dezelfde tool. En een weigerende sessie ziet er volstrekt normaal uit, in zijn eigen /status en in de overzichten van andere sessies, dus je kunt het alleen vaststellen door de configuratie te lezen.

Andere machines, waar de documentatie al achterloopt

Waar de andere sessie draait, bepaalt hoe het bericht reist.

Waar hij draaitHoe het bericht reist
Deze machineSocket per sessie, nooit langs Anthropic-servers
Een andere machine van jouVia Anthropic-servers, over de Remote Control-verbinding van die machine
Claude Code op het webVia Anthropic-servers, rechtstreeks naar de cloudsessie

Volgens de documentatiepagina kun je bij alles buiten deze machine alleen antwoorden: Claude hier reageert wel op een bericht dat van je laptop kwam, maar begint het gesprek niet zelf. Dat gold precies één release lang. Uit de changelog van 2.1.225:

text
SendMessage can now start a conversation with your Remote Control sessions on
other machines by name (ListAgents shows them as name [ref]), instead of only
replying after they message you first

Op 2.1.226 beschrijft de ListAgents-tool zijn eigen bereik net zo, en nergens staat er nog dat je alleen mag antwoorden. Op de documentatiepagina staat nog de regel uit 2.1.224.

Het loont dus om te weten welke van de twee je zit te lezen. Precies zo'n beperking bouw je namelijk in je workflow in, en die ging er na één release alweer af. Remote Control moet hier nog steeds verbonden zijn voordat die sessies überhaupt in het overzicht verschijnen, en over websessies zegt de changelog niets.

Eén valkuil blijft hoe dan ook staan. Antwoord je een sessie buiten deze machine terwijl de antwoordende sessie zelf niet met Remote Control verbonden is, dan vertrekt het antwoord alsnog, als rechtstreeks verzoek aan Anthropic-servers, maar komt het aan zonder antwoordadres. De ontvanger kan er niets op terugsturen. Claude krijgt dat te horen op het moment dat hij verstuurt, en dat is meer waarschuwing dan de meeste tools je geven.

Onbemande workers

Een claude -p-sessie opent net als elke andere een socket en staat gewoon in het overzicht, dus een lange migratie of testrun kan terugrapporteren aan de sessie die jij in beeld hebt.

Wat zo'n sessie niet kan, is een goedkeuringsvenster tonen. Een vastgehouden bericht blijft daar vastgehouden, en komt alleen los door een latere wijziging in modus of instellingen. Wil je dat een onbemande worker berichten aanneemt, zeg dat dan bij het starten:

bash
claude -p "run the migration" --settings '{"crossSessionInbound":"accept"}'

Een accept in je gebruikersinstellingen werkt ook, maar geldt dan voor elke sessie die je start, en dat is meestal ruimer dan je bedoelde. Bare mode, de uitgeklede headless start die het meeste van de sessieopbouw overslaat, opent helemaal geen socket, dus die sessies zijn onzichtbaar en onbereikbaar.

Wat het niet is

De fout die je gaat maken is een bericht behandelen als gedeelde context. Het is één zin, gekozen door de afzender, en er hangt niets aan vast.

Wil je eigenlijk de context van die andere sessie, dan heb je een andere functie nodig:

Wat je wiltWat je gebruikt
Eén gesprek elders voortzettenDe sessie hervatten
Sessies op één repository op elkaar afstemmenMessaging, met worktrees voor de bestanden
Een team dat Claude zelf opstart en aanstuurtAgent teams
Veel sessies volgen en bijsturenAgent view
CI- of chatgebeurtenissen een sessie in sturenChannels

Messaging dekt het smalle geval: een sessie die jij aanstuurt weet één ding dat een andere sessie die jij aanstuurt nu nodig heeft, midden in de klus.

Elk ander multi-agentverhaal van dit jaar ging over agents méér bereik geven. Dit verhaal levert een kanaal en besteedt vervolgens de meeste regels aan wat er niet doorheen mag. Een bericht draagt informatie en geen gezag, en dat is precies wat je wilt van de terminal in het andere venster.

Terug naar die hernoemde kolom. De tweede sessie krijgt één zin over tenant_id en doet daarna wat hij ook had gedaan als jij die zin zelf had ingetypt: hij vraagt het aan jou voordat hij iets aanraakt.

(30 van 30)
01Het beste uit Claude Code halen02Superpowers: hoe je Claude Code leert eerst te denken03Claude Code hooks: deterministische controle over AI-workflows04Het CLAUDE.md bestand: geef je AI permanent geheugen05Stop met vriendelijk vragen aan je agent06Wat er nieuw is in Claude Code: notities van het Londen-event07Het beste cijfer in Opus 4.8 is geen benchmark08Verouderd geheugen is erger dan geen geheugen09De agent is gewoon een loop10Bouw een MCP-server, en vraag je dan af of die moet bestaan11Skill, subagent, hook of slash command? Kies de juiste12Inloggen op MCP-servers vanuit je shell13De dag dat 'default' ineens 'Manual' heette14Hoe je een goede Claude Code skill schrijft15Een goede Claude Code subagent schrijven16Claude Code permissions instellen: de gids die ik miste17Claude Code sandboxen: toestemming is geen muur18Prompt injection voorkomen: verdediging voor wie agents bouwt19Welk Claude-model voor welke programmeertaak20Legacy code refactoren met AI: begin met characterization tests21Je MCP-server beveiligen: authenticatie, scopes en rate limits22Opus 5 is er, en je effort-instellingen kloppen niet meer23Incident response voor AI-agents: wat je doet als je coding agent de fout in gaat24Claude Code /doctor: van installatiecheck naar contextaudit25Context beheren in Claude Code: wanneer /clear en wanneer /compact26Git worktrees uitgelegd: parallel werken met AI-agents zonder botsingen27Claude Code plan mode: beslissen voordat de agent schrijft28Debuggen met een coding agent: geef hem het zoekwerk, de hypothese houd je zelf29Claude Code checkpoints en /rewind: wijzigingen van je agent terugdraaien30Claude Code cross-session messaging: sessies die elkaar berichten sturen