Ga naar inhoud

Sandbox en beveiliging

Een agent leest bestanden, draait shellopdrachten en roept externe API’s aan namens jou. De fout waar je je zorgen over maakt is zelden kwaadwilligheid — het is een agent die een pad verkeerd leest en je werkcheckout herschrijft, of een die een branch pusht die je nog niet klaar had gereviewd. De vraag die het product dus moet beantwoorden is niet “is de agent te vertrouwen”, maar “hoe groot is het impactbereik als hij het mis heeft”.

Control Center antwoordt daarop in lagen, en de lagen zijn bewust onafhankelijk. Ontbreekt er één, dan houden de andere nog stand. Dat is belangrijk, want de buitenste laag — de OS-sandbox — is de enige voorwaardelijke: hij hangt ervan af of de host een backend biedt, en op Windows doet geen enkele host dat.

Vijf dingen begrenzen een agentrun op elke host, met of zonder sandbox. Geen ervan is een kernelgrens; alle vijf zijn ze reëel. Waar de host een backend biedt, zit de OS-sandbox daarbovenop, en “Wat de sandbox toevoegt en waar hij stopt” hieronder beschrijft wat dat verandert.

Zijn eigen copy-on-write-worktree. Elk gesprek zet zijn eigen checkout klaar van elke gekoppelde repo, copy-on-write gekloond via de meegeleverde rift-native in plaats van byte voor byte gekopieerd, zodat isolatie goedkoop genoeg is om de standaard te zijn. De geregistreerde repository die je aan de werkruimte hebt toegevoegd, is niet de map waarin de agent werkt — twee gesprekken die dezelfde repo aanraken, zien elkaars wijzigingen niet en geen van beide bewerkt jouw checkout.

De korrel is het preciseeren waard: de worktree is per gesprek, niet per agent. Twee agents in hetzelfde gesprek krijgen aparte overlay-werkmappen die de repos/ van dat gesprek delen — wat precies de bedoeling is, want ze werken samen aan één wijziging — terwijl hun privémappen per agent gescheiden blijven. Copy-on-write is de enige backend overal waar rift meelevert: een bestandssysteem dat niet kan reflinken, of een datadir op een ander volume dan de repo, laat het klaarzetten mislukken in plaats van terug te vallen. Windows is de uitzondering — daar is geen copy-on-write-backend, dus is git worktree de backend, en geen stille verslechtering.

Padbeperking in de bestandstools van de ingebouwde harness. De tools read, write, edit, apply_patch, search, find en search_files van de harness herleiden elk pad tegen de werkmap van de run plus gedeelde rootpaden — de map repos/ van het gesprek, waar de worktrees staan, en de bijlagenmap van de ruimte als die bestaat — en weigeren alles wat daarbuiten uitkomt. Absolute paden worden alleen geaccepteerd als ze al binnen lagen. Een symlink binnen de worktree die naar buiten wijst, wordt ook herleid en geweigerd. Daarom kan een agent zelfs zonder sandbox in beeld niet met write naar ~/.ssh schrijven.

Het tooloppervlak van de modus. De modus van een gesprek bepaalt welke tools überhaupt voor de run worden gematerialiseerd. In de alleen-lezen-modi (plan, review, orchestrate) staan de muteerende tools simpelweg niet in het register en weigert het shell-opdrachtbeleid voor die modi elke muteerende opdracht onmiddellijk — git commit, git push, npm install, docker en de rest — in plaats van erom te vragen. Een tool die er niet is, kan niet worden aangeroepen, en dat is een sterkere garantie dan een tool die er wel is en achter een poort zit.

De uniforme actiebeveiligingen. Elke muteerende tool verklaart welke effectklassen hij kan veroorzaken, en de policy-engine herleidt elk daarvan naar toestaan, eerst vragen of weigeren voordat de aanroep draait. Een prompt zonder verbonden goedkeurder wordt geweigerd — een vraag die niemand kan beantwoorden is nooit een ja. Zie Beveiligingen voor het herleidingsmodel.

Inloggegevens via de broker en een gesaneerde omgeving. Inloggegevens worden per start aangemaakt en ingetrokken wanneer de run afloopt, en het GitHub-token van een run kan lezen maar niet pushen — zie “Pushes lopen via Control Center” hieronder. ACP, de bash van de harness en Claude Code onder de native wrap krijgen een omgeving waarin de injectievectoren via de dynamic linker en interpreters (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND en hun verwanten) zijn gestript voordat het proces start. Claude Code via NoSandboxAdapter erft de omgeving van de host.

De bestandstools van de harness draaien in het serverproces, niet in een kindproces, dus geen enkel sandboxprofiel zou ze kunnen beperken, ook niet als er een zou zijn bevestigd — alleen bash gaat door de gesandboxte opdrachtuitvoerder. Hun beperking is de padcontrole die hierboven is beschreven, en die controle is daarom dragend in plaats van een tweede verdedigingslinie.

De bash-tool is de andere plek waar precisie loont. De opdrachten ervan worden getoetst aan het opdrachtbeleid van de modus en daarna gestart met een gesaneerde omgeving en de worktree als werkmap. Waar een backend bestaat, wordt die start omhuld, dus houdt de sandbox een shellopdracht tegen die buiten de worktree wil schrijven; waar dat niet zo is — Windows, of Linux zonder bwrap en socat — is het opdrachtbeleid het enige tussen de opdracht en de rest van de schijf. In de modus chat weigert het beleid hard een vaste lijst (rechtenescalatie, vernietiging van systeem en schijf, ontsnapping uit containers en namespaces) en vraagt het om bevestiging bij mutaties aan de remote-status (git push, gh pr create, het publiceren en installeren van pakketten). Alles wat op geen van beide lijsten staat, draait gewoon. Pijplijnen, ketens en subshells worden geparsed en elke deelopdracht wordt apart getoetst, waarbij de meest beperkende beslissing wint.

Claude Code, wanneer het als adapter wordt gebruikt, is het geval waarin de sandbox het meeste gewicht draagt. Control Center start het met --dangerously-skip-permissions, omdat een niet-interactieve claude -p anders voor altijd blijft hangen op zijn eigen goedkeuringsprompt. De lees-, schrijf- en bewerkingsaanroepen ervan gebeuren binnen zijn eigen proces en Control Center ziet ze nooit. Zijn Bash-aanroepen gaan vóór uitvoering naar een hook op de server die de opdrachtregel indeelt en de bijbehorende regel raadpleegt, en de mcp__*-tools die Control Center serveert gaan zoals altijd langs de poort. Waar een backend bestaat, gebeuren die ongeziene aanroepen nog steeds binnen de sandboxwrap, dus is het besturingssysteem de grens eromheen. Waar dat niet zo is, is er niets. Elke adapter vermeldt dit in zijn afdwingingsmatrix — zie de eerlijkheidsmatrix per adapter.

Er zijn geen machtigingsvlaggen per agent. Of een agent een commit mag maken, mag pushen, een pull request mag openen, naar een tracker mag schrijven of het netwerk mag bereiken, beslissen de beveiligingen — toestaan, eerst vragen of weigeren — en dezelfde regels gelden welke adapter de agent ook draait.

Een push is de actie die moet standhouden, zelfs tegen een agent die een regel probeert te omzeilen. Daarom wordt hij afgedwongen via wat de agent bezit, niet via wat hij uitvoert. Met een geconfigureerde GitHub App krijgt een run in een GitHub-worktree een tot de repo beperkt installatietoken dat inhoud kan lezen en met pull requests kan werken, maar niet kan pushen. De git-configuratie van de run herschrijft elke GitHub-push-URL (https://github.com/, git@github.com:, ssh://git@github.com/) naar de agentrun-gateway, een loopback-eindpunt op cc_server. De gateway leest welke refs de push bijwerkt, raadpleegt de regel Naar een remote pushen — een regel die tot branches is beperkt, zoals “pushes naar main weigeren”, geldt dus ook — en stuurt de push pas daarna naar GitHub door, met een schrijftoken dat hij daarvoor aanmaakt en nooit aan de agent geeft. Een geweigerde push verschijnt in de eigen uitvoer van git:

! [remote rejected] main -> main (Control Center policy: …)

De gateway stuurt pushes alleen door naar de eigen repo van de worktree of naar een GitHub-repo die aan de werkruimte is gekoppeld. Fetches gaan met het leestoken rechtstreeks naar GitHub.

Andere shellopdrachten stuiten op een zachtere poort. De bash-tool van de harness en, via een hook, de Bash-tool van Claude Code delen elke opdrachtregel in en raadplegen de regel waaronder die valt: git commit, merge, rebase, cherry-pick, revert en am zijn Een commit maken; gh pr create is Een pull request openen; gh pr merge, review, comment en close zijn Een review publiceren of mergen; gh issue create, edit, comment en close zijn Naar een externe tracker schrijven; pakketinstallaties zijn Een pakket installeren; curl en wget zijn Toegang tot het netwerk. Dit herkent de gebruikelijke schrijfwijze van een actie, en een agent die er een wil verbergen (eval, een scriptbestand, een directe API-aanroep) kan dat. Pushes zijn er niet van afhankelijk.

In een sandbox wordt geen ticketing-API-sleutel gezet. Agenten schrijven tickets via de MCP-tools van Control Center, die Naar een externe tracker schrijven raadplegen.

De regel Toegang tot het netwerk wordt één keer gelezen, wanneer de run start. Het netwerk van een sandbox staat aan of uit, dus weigeren zet het uit, terwijl toestaan en eerst vragen het aan laten; de tools web_fetch en web_search van de harness blijven bij elke aanroep vragen. Inloggegevens worden ingetrokken wanneer de run wordt afgebroken, samen met elk schrijftoken dat de gateway heeft aangemaakt, zodat een token de run niet overleeft.

De enum SandboxBackend heeft drie waarden — native (Seatbelt op macOS, bubblewrap op Linux en WSL2), none en microvm. Het omhullen van agentopdrachten gebruikt alleen de eerste twee: microvm is de backend met een omhulde VM voor interactieve terminals en rigs, geen stille upgrade van een bash-wrap. Als je de native vorm begrijpt, weet je wat je wint waar er een is bevestigd en wat die je nooit had kunnen geven.

cc_server probeert de host één keer bij het opstarten en voorziet, waar een backend antwoordt, beide aansluitpunten die het dispatchpad heeft, omdat de transporten er geen delen. Het eerste (SandboxPort) omhult de Claude Code CLI. Het tweede (SandboxManager) omhult ACP-adapters en de bash-tool van de ingebouwde harness. Slechts één ervan voeden zou de andere ongehuld laten starten, en dat gebeurde vroeger ook. Op een host zonder backend, of onder --sandbox off, starten ze allemaal ongehuld en zegt de server op zijn opstartregel in welke situatie je zit. ACP en de bash van de harness saneren de omgeving nog steeds; Claude Code via NoSandboxAdapter erft de omgeving van de host.

Eén oppervlak stapt er bewust buiten: de door de server gehoste terminal accepteert een backend-argument per sessie en cc_server construeert die met de hostshell als standaard, dus een terminal die je vanuit de app opent, draait zonder sandbox tenzij een client expliciet native noemt. Dat is een echte shell op de serverhost, geen agentrun.

Op Linux bouwt bubblewrap een mount-namespace, dus ziet de agent echt alleen wat erin is gebonden. Op macOS past sandbox-exec een Seatbelt-profiel toe dat standaard toestaat — een profiel dat standaard alles weigert, maakt een interactieve shell onbruikbaar, omdat het aantal dyld-, XPC- en Mach-aanroepen om te enumereren onpraktisch is. Het macOS-profiel begint daarom bij (allow default) en haalt daar weigeringen uit: het lezen van geheime paden, een schrijfbeleid dat op weigeren wordt gezet met expliciete toestemmingen daarbovenop, blokkering van unlink op elk geweigerd pad en zijn voorouders zodat een rename een schrijfweigering niet kan omzeilen, exec-weigeringen voor altijd gevaarlijke binaries en voor alles wat naar een beschrijfbare map is gekopieerd, en een netwerk dat beperkt blijft tot de loopback-proxypoorten plus DNS.

De praktische consequentie: op Linux wordt het beeld dat de agent van het bestandssysteem heeft smaller; op macOS kan de agent nog steeds het grootste deel van de host lezen en worden je geregistreerde checkouts beschermd door expliciete schrijf- en renameweigeringen in plaats van door onzichtbaarheid. Beide zijn isolatie op namespaceniveau, geen kernelbeveiligingsgrens — de backends zeggen dat zelf in hun eigen beschrijving van hun mogelijkheden.

De set bind-mounts is waar de grens tussen agents wordt getrokken. Overlay-dispatch (Claude Code en ACP) mount maximaal vier paden, per run bepaald: de overlay-werkmap van de agent read-write, de configuratiemap van de agent read-only, de gedeelde repos/-map van het gesprek read-write als die bestaat, en de bijlagenmap van de ruimte read-only als die bestaat. De map van een zusteragent wordt nooit gemount. De bash-wrap van de harness is smaller: die bind-mount alleen de werkmap van de run. Dat is dezelfde vorm die de in-process padcontrole van de harness in software afdwingt, dus de twee komen overeen — maar alleen de padcontrole dekt de eigen bestandstools van de harness, die nooit een mount-namespace bereiken.

Je geregistreerde checkouts komen daar bovenop in de deny-write-set, in elke modus en op alle drie de omhulde paden (Claude Code, ACP, bash van de harness), en elke regel wordt in zowel zijn letterlijke als zijn symlink-opgeloste spelling uitgevaardigd. De tweede spelling is geen luxe: de kernel matcht een weigeringsregel tegen het opgeloste pad en macOS maakt herleiding het gewone geval in plaats van het exotische — /tmp is in werkelijkheid /private/tmp, /var/folders/… is in werkelijkheid /private/var/folders/… — dus een checkout die via een van beide werd bereikt, zou beschrijfbaar zijn geweest terwijl het profiel het leek te verbieden.

Netwerk is onder een sandbox een domein-allowlist per sessie, afgedwongen door in-process loopbackproxies. Uitgaand verkeer is standaard geweigerd: staat het netwerk uit, dan starten de proxies wel, maar is hun allowlist leeg, dus wordt elke uitgaande verbinding geweigerd. Staat het aan, dan bouwt de resolver de allowlist uit een samengestelde basis — LLM-API-providers, githosting, pakketregisters, de modelcatalogus — plus de domeinen van GitHub, en matcht hij de doelhost van elke verbinding daartegen. Jokers werken (*.example.com matcht subdomeinen, niet het hoofddomein). Een aparte lijst die altijd geweigerd wordt, wint van de allowlist en dekt cloudmetadata-eindpunten en telemetrie-sinks. Wat niet configureerbaar is, is de lijst zelf: de spec draagt een veld egressAllowlist voor extra domeinen, maar niets in het dispatchpad vult dat, dus in de praktijk krijgt elke run de basis plus GitHub en niets meer.

Weigeringen zijn op macOS waarneembaar: een monitor tapt het systeemlogboek af op Sandbox-weigeringen, filtert bekende ruis, dedupliceert herhaalstormen en brengt elke weigering naar buiten als een SandboxViolation die noemt wat werd geprobeerd en wat werd tegengehouden. Op Linux parseert het wrap-pad stderr op EPERM, Operation not permitted en Permission denied en zendt hetzelfde gebeurtenistype uit — er is geen tap op het systeemlogboek. Die komen binnen op de eigen gebeurtenisstroom van de run, dus een geweigerde systeemaanroep verschijnt in het transcript in plaats van alleen in een log. De telemetrie bestaat alleen zolang een native sandbox het proces daadwerkelijk omhult — op een host zonder backend valt er niets waar te nemen. Ze staat ook los van de beveiligingen: een schending komt van een geweigerde systeemaanroep, nooit van het opzoeken van een regel.

Bevestigingen komen van de beveiligingen, niet van de sandbox

Sectie getiteld “Bevestigingen komen van de beveiligingen, niet van de sandbox”

De sandbox heeft geen goedkeuringspad. Hij start, voert uit, zendt gebeurtenissen uit en breekt af; er is nergens in hem ruimte voor een mens die een vraag beantwoordt. Wanneer je een bevestiging ziet in een ruimte — keur deze push goed, keur deze verwijdering goed — dan is die opgeworpen door de beveiligingsengine (of het opdrachtbeleid van de harness) voordat de actie de runtime überhaupt bereikte.

Deze volgorde is bewust. Een goedkeuring die na de systeemaanroep plaatsvond, zou een auditregistratie zijn, geen poort. Daarom ook zit de fail-closed-regel in de beveiligingen: zonder verbonden goedkeurder wordt de beslissing genomen voordat er iets draait, en die is negatief.

Een agent diagnosticeren is de enige plek die beschrijft wat je met de hand kunt controleren.

  • Rigs en omhullingen: de stap boven de sandbox — wanneer het werk een hele wegwerp-VM nodig heeft met een kernelgrens, niet een omhuld proces
  • Beveiligingen: de beleidslaag boven de ondergrens van het besturingssysteem en waar goedkeuringen echt vandaan komen
  • Levenscyclus van agent-dispatch: waar isolatie past in de dispatchstroom
  • Modi: hoe een modus het tooloppervlak en het opdrachtbeleid verkleint
  • Sandboxbackends: de backends en hun configuratie