AI Agent Security: Permissions, Sandboxes en MCP-bedreigingen
Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Artikelupdate
Oorspronkelijk gepubliceerd op 20 april 2026. Beoordeeld en bijgewerkt op 6 september 2026. De update behandelt nieuwere sandbox-controls, safety-interventies van providers en gepubliceerde security findings, inclusief hun beperkingen en bronlinks.
Agent security begint zodra een model een actie voorstelt en voordat de machine die uitvoert. Bepaal welke check het laatste woord heeft voordat de actie credentials, bestanden, netwerken of een extern systeem bereikt.
De harness is de code die elke prompt opbouwt, beslist welke voorgestelde tool calls worden uitgevoerd en resultaten terugstuurt naar het model. De meeste checks horen daar thuis, omdat dit de laatste goedkope plek is om een command te stoppen. Nadat een command is uitgevoerd, moeten de sandbox, de credentials die het heeft ontvangen en elk recoveryproces de schade beperken. Sommige incidents in dit artikel bereiken helemaal geen model.
AI agent security is breder dan LLM safety. Vroege guardrail-producten inspecteerden de input en output van één model call. Ze konden toxische tekst filteren, persoonlijke data redigeren, jailbreaks blokkeren en off-topic antwoorden weigeren. Die grens was bruikbaar zolang het model alleen tekst kon teruggeven.
Tool loops voegden filesystems, shells, Model Context Protocol (MCP)-servers en credentials toe. Daarmee verschoof het threat model van onveilige tekst naar onveilige acties. De zeven incidentgroepen hieronder omvatten indirecte prompt injection en fouten in configuratie, identity en softwaredistributie. Text screening kan helpen bij sommige malicious inputs; het kan de controls op die execution boundaries niet vervangen.
Wanneer een agent een repository kan lezen, een tool kan aanroepen of data naar een derde partij kan sturen, koppel dan elke voorgestelde actie aan de check die deze kan stoppen. De volgende secties behandelen permissions, hooks, sandboxes, credentials en menselijke review.
Zie voor de korte control checklist AI Agent Security Checklist.
AI agent security stack
Geen enkele guardrail beveiligt een agent volledig. Elk onderdeel van het systeem heeft zijn eigen check nodig.
De tabel laat zien waar elke check draait. De harness is het controlprogramma dat hierboven is beschreven. De runtime is de infrastructuur die het gebruikt: de sandbox, session log, checkpoint store en traces die een worker-restart overleven.
| Laag | Wat deze beheert | Voorbeeld van een fout die deze opvangt | Waar deze draait |
|---|---|---|---|
| Content filters | Onveilige input- en outputtekst | Toxische output, PII-leakage, policy-violating completions | Harness |
| Permission ladder | Welke tools, paths, APIs en scopes de agent kan gebruiken | Een summarizer die naar production systems probeert te schrijven | Harness |
| Pre-tool policy hook | Of deze specifieke actie nu mag worden uitgevoerd | Shell command opgebouwd uit untrusted retrieved content | Harness |
| Sandbox | Wat de tool op OS- en networkniveau kan benaderen | File exfiltration, dependency compromise, command injection | Runtime |
| Human approval check | Onomkeerbare of high-impact acties | E-mail versturen, geld verplaatsen, deployen naar production | Harness |
| MCP and token scoping | Voor welke server en audience een credential geldig is | Token reuse op een onbedoelde toolserver | Runtime |
| Audit trace | Wat er gebeurde, wie het goedkeurde en waarom | Incidentonderzoek na een lange autonomous run | Runtime |
Content filters vragen of het model iets onveiligs heeft gezegd. Agent security vraagt ook of het systeem de volgende actie mag uitvoeren.
De harness-rijen bepalen of een actie mag draaien. De runtime-rijen handhaven vooraf ingestelde limieten en leggen vast wat er gebeurde. Behoud de sandbox ook wanneer permission rules compleet lijken: deze kan een call stoppen waarop de harness niet was voorbereid. De sandbox kan niet bepalen of een toegestane actie de juiste actie was; dat moet de harness doen.
De laatste kolom vermeldt waar een check draait, niet wie deze beheert. Een vendor kan een content filter leveren, maar de harness roept het aan.
Managed content filters dekken de text layer. De overige controls horen thuis in application policy, identity en infrastructure.
Waarom AI agent security anders is dan LLM safety
Bharani Subramaniam en Martin Fowler zetten het kader begin 2025 uiteen in Emerging Patterns in Building GenAI Products. Hun observatie was beperkt en direct:
“With traditional systems, we could assess correctness primarily through testing… With LLM-based systems, we encounter a system that no longer behaves deterministically.”
Output evaluation vraagt of een model response aan een rubric voldoet. Een agent threat model moet ook tool calls, shell commands, file writes, credentials en network requests omvatten. Een output grader kan die acties niet stoppen. De harness doet dat wel: dit is de set checks die een modelvoorstel omzet in een toegestane actie. De rest van dit artikel behandelt die checks.
Simon Willison beschreef de agentspecifieke risico’s in juni 2025 met de lethal trifecta:
“The lethal trifecta of capabilities is: access to your private data; exposure to untrusted content; the ability to externally communicate in a way that could be used to steal your data. If your agent combines these three features, an attacker can easily trick it into accessing your private data and sending it to that attacker.”
Veel nuttige agents combineren deze capabilities: toegang tot een inbox, web retrieval en een messaging tool; of repositorytoegang, het lezen van issues en pull-request writes. Een content guardrail vraagt of het model onveilige tekst heeft gegenereerd. De trifecta vraagt of untrusted input het systeem kan sturen richting het openbaar maken van data via een toegestane actie.
De structurele versie van hetzelfde argument staat in Joel Fokou’s Parallax-preprint (arXiv 2604.12986, ingediend op 14 april 2026, niet peer-reviewed). De kernclaim:
“The system that reasons about actions must be structurally unable to execute them, and the system that executes actions must be structurally unable to reason about them, with an independent, immutable validator interposed between the two.”
Je hoeft de evaluatienummers van het paper niet te accepteren om het structurele punt te onderzoeken. Verschillende huidige harnesses implementeren delen van dezelfde scheiding:
- Claude Code’s PreToolUse hooks
- Codex CLI’s OS-sandboxed executor (op Linux: bubblewrap plus seccomp system-call filtering)
- Anthropic’s Managed Agents, die credentials in een vault bewaren die de agent nooit ziet
- MCP’s RFC 8707 audience-bound tokens
Deze systemen houden het model gescheiden van de code die commands uitvoert. Hun controls verschillen, maar geen ervan laat de command runner voor safety vertrouwen op de mening van het model.
Er is een aanvullende discipline die Alessandro Pignati het scherpst formuleerde in januari 2026: het Principle of Least Agency. Least Privilege vraagt waar heeft deze identity toegang toe? Least Agency vraagt wat mag deze agent beslissen? Privilege beperkt de credentials; agency beperkt het bereik van een plan, zelfs wanneer de credentials geldig zijn. Excessive Agency is een afzonderlijke categorie in de Top 10 voor LLM Applications van OWASP, het Open Worldwide Application Security Project. De afzonderlijke agentic lijst die later in dit artikel aan bod komt, splitst dezelfde fout op over tool misuse en privilege abuse. Least Agency is de design discipline die beide voorkomt. Een agent die je inbox kan samenvatten, heeft waarschijnlijk geen commit rights voor je monorepo nodig. Toch blijven we configuraties vinden waarin dat wel zo is.
Wat LLM guardrails afdekken
LLM guardrails doen nuttig werk rond de model call. Ze inspecteren input, retrieved text en output en blokkeren, redigeren, herstellen of markeren content die niet aan een geconfigureerde regel voldoet. De onderstaande producten verschillen in deployment en coverage. Een content check staat los van een authorization check bij de tool- of MCP-server boundary; sommige producten bieden ook runtime policy-features die hun eigen configuratie en evaluatie vereisen.
NVIDIA NeMo Guardrails
De meest opinionated optie: een orchestration framework rond vijf rail-types (input, dialog, retrieval, execution, output) met een eigen DSL — Colang, een Python-achtige taal voor dialog flows, user intents en bot messages. Je kunt de basis vanuit Python + YAML aansturen, maar rijkere dialog logic wordt in Colang geschreven — vandaar “opinionated”. Docs op docs.nvidia.com/nemo/guardrails.
Dit is een illustratieve API-vorm; het package en een geconfigureerde ./config-directory zijn vereist.
from nemoguardrails import LLMRails, RailsConfig
config = RailsConfig.from_path("./config")
rails = LLMRails(config)
response = rails.generate(
messages=[{"role": "user", "content": "Hello"}]
)
NeMo’s repo is expliciet over zijn threat model: “common LLM vulnerabilities, such as jailbreaks and prompt injections.” Het is even expliciet over de scope: “The built-in guardrails may or may not be suitable for a given production use case… developers should work with their internal application team to ensure guardrails meets requirements.” Het content-screeningpad dat hier wordt getoond, bewaakt wat het model zegt. De huidige NeMo-docs beschrijven ook execution rails, custom actions en tool-call inspection; dat zijn configureerbare runtime-controls, geen bewijs dat de deployed tool of MCP-server de call heeft geauthenticeerd en geautoriseerd. De applicatie blijft eigenaar van die boundary.
Meta Llama Guard 4
Een pure content classifier van 12B, gepruned uit Llama-4-Scout en afgestemd op de MLCommons hazards taxonomy (13 harm categories plus code-interpreter abuse, volgens de model card). Meta is ongewoon open over de beperkingen:
“Some hazard categories may require factual, up-to-date knowledge to be evaluated fully… Lastly, as an LLM, Llama Guard 4 may be susceptible to adversarial attacks or prompt injection attacks that could bypass or alter its intended use: see Llama Prompt Guard 2 for detecting prompt attacks.”
Meta levert een apart product om zijn content classifier tegen prompt injection te beschermen. Als die zin klinkt als een structurele erkenning, dan is dat ook zo.
Guardrails AI
Een validator registry. Je combineert Hub validators (PII via Presidio, JailbreakDetect, CompetitorCheck, provenance checks) met on_fail actions exception | fix | fix_reask | filter | refrain | reask | noop, of een custom callback (guardrailsai.com). Let op exception, niet raise. In de huidige source komt een onbekende on_fail-string terecht bij de custom-callback handling en treedt er tijdens de validator setup een error op, in plaats van een warning en fallback. Pin de versie die je deployt en test dit failure path. Er is geen unified threat model; coverage is gelijk aan de unie van de geïnstalleerde validators. Je krijgt bescherming voor waarvoor je een validator hebt, en niets voor al het andere.
Lakera Guard
De gevestigde SaaS API, getraind op tientallen miljoenen attack samples die zijn verzameld via Gandalf. Het product belooft input en output te screenen op “prompt attacks… and data leakage”. Lakera’s afzonderlijke AI Agent Security-product beschrijft ook policy- en runtime-enforcement voor wat agents mogen benaderen, aanroepen en doen. Dat is een ander productsurface dan de content-screening call die hier wordt besproken. Controleer vóór deployment het actuele product en de pricing contract.
AWS Bedrock Guardrails
De enterprise-default als je al op Bedrock zit. ApplyGuardrail werkt met elk model, Bedrock of niet:
# No-run: illustrative AWS request; requires boto3, AWS credentials, and a real guardrail identifier.
import boto3
brt = boto3.client("bedrock-runtime")
resp = brt.apply_guardrail(
guardrailIdentifier="gr-xxxxxxxxxxxx",
guardrailVersion="2",
source="INPUT",
content=[{"text": {"text": "user question",
"qualifiers": ["guard_content"]}}],
)
Gepubliceerde ApplyGuardrail pricing: $0,15 per 1.000 text units voor content filters of denied topics, $0,10 voor PII filters of contextual grounding. Een text unit bevat maximaal 1.000 tekens.
Azure AI Content Safety
Levert Prompt Shields als een unified endpoint dat “detects and blocks adversarial user input attacks… direct and indirect threats”. Azure is ook duidelijk: “You can’t use Azure AI Content Safety to detect illegal child exploitation images,” en de meertalige kwaliteit is beperkt tot acht geëvalueerde talen.
OpenAI Moderation en OpenAI Guardrails
omni-moderation-latest is de gratis multimodal baseline. Afzonderlijk is openai-guardrails-python (docs op guardrails.openai.com) OpenAI’s frameworkantwoord: een three-stage pipeline (pre-flight, input, output) met Jailbreak Detection, Hallucination Detection via FileSearch, NSFW, PII via Presidio en LLM-as-judge. GuardrailAgent koppelt aan de Agents SDK.
# No-run: illustrative OpenAI Guardrails API shape; requires the package and guardrail_config.json.
from guardrails import GuardrailsOpenAI, GuardrailTripwireTriggered
client = GuardrailsOpenAI(config="guardrail_config.json")
try:
resp = client.responses.create(model="<your-model-id>", input="...")
except GuardrailTripwireTriggered as e:
print(f"blocked: {e}")
Wat content filters niet beslissen
Twee observaties gelden voor alle zeven.
Ten eerste zijn gepubliceerde latency- en throughputcijfers schaars. Bedrock, Azure en Lakera publiceren pricing, maar geen garanties voor worst-case latency. Meta publiceert evenmin een garantie voor een hosted endpoint voor Llama Guard. NVIDIA levert NeMo Guardrails als software die je zelf host, waardoor latency afhangt van je model en infrastructuur. Meet elke synchrone check op het critical path in plaats van de kosten af te leiden uit productpricing.
Ten tweede behandelt deze sectie de hierboven genoemde contentgerichte configuraties.
Een content filter kan modelinput en -output inspecteren. Het toont niet aan dat een specifieke tool call geautoriseerd is, dat een MCP-server zijn caller heeft geauthenticeerd of dat het systeem multi-step data exfiltration of code execution kan stoppen vóór een model call. Authorization is een afzonderlijke beslissing over de vraag of deze identity deze call naar deze server mag maken.
NeMo documenteert ook execution rails en tool-call inspection, en Lakera beschrijft runtime enforcement in zijn afzonderlijke AI Agent Security-product. Dat zijn aanvullende controls die je moet configureren en testen. De rest van deze post behandelt de checks rond tool execution.
AI agent security threats: zeven incidents en de OWASP ASI Top 10
De kloof tussen tekst filteren en execution beveiligen werd halverwege 2025 praktisch zichtbaar. De zeven incidents hieronder bereikten retrieval, configuratie, credentials, package-installatie of CI-execution. Een content classifier kan nog steeds een verdachte string detecteren, maar de controls die deze paden direct blokkeren, bevinden zich bij de tool-, identity-, sandbox- en supply-chain boundaries.
EchoLeak — CVE-2025-32711
Onthuld in juni 2025 door Aim Labs, de researchtak van Aim Security, tegen Microsoft 365 Copilot. De technische write-up staat nu op Cato Networks, dat dit team heeft overgenomen, onder de naam van Itay Ravia, voormalig hoofd van Aim Labs (write-up). Een crafted e-mail, geformuleerd als instructies aan de menselijke ontvanger, glipte langs XPIA (Microsofts ingebouwde filter dat Copilot-inputs op prompt-injection attacks controleert). Vervolgens kwam de e-mail in Copilots retrieval layer terecht, het deel van het systeem dat je documenten doorzoekt om context voor antwoorden te vinden. De onderzoekers noemen deze techniek RAG-spraying: de aanvaller gebruikt meerdere e-mails of één lange, in chunks opgebouwde e-mail om de retrieval exposure te vergroten. Dat verhoogt de kans op retrieval, maar garandeert die niet. Eenmaal binnen embedde Copilot gehoorzaam de gevoeligste data uit de session in een Markdown-link die verwees naar een image op een door de aanvaller beheerd domein. De Teams preview API, draaiend op een domein dat door Microsofts eigen browser policies al werd vertrouwd, haalde die image-URL automatisch op en gaf daarmee de data aan de aanvaller door. Zero clicks. Aim Labs noemde deze aanvalsklasse “LLM Scope Violation”: het model overschrijdt een boundary die het nooit had mogen overschrijden, met alleen operations die elk afzonderlijk systeem als legitiem beschouwde.
Elke stap leek op zichzelf legitiem. De e-mail was aan een mens gericht. Retrieval haalde een document op dat het mocht ophalen. De Markdown-link werd weergegeven zoals Markdown-links worden weergegeven. De image fetch ging naar een allowlisted domain. De onderzoekers omzeilden XPIA’s screening, en het model volgde indirecte instructies uit retrieved content. De case combineert een prompt-injection failure met retrieval-, rendering- en egressgedrag; het toont niet aan dat detectors niets hadden kunnen signaleren.
Amazon Q Developer VS Code v1.84.0 — juli 2025
AWS bracht een gecompromitteerde build uit nadat een aanvaller via een over-scoped CodeBuild GitHub-token een malicious system-promptbestand had gecommit (advisory). De payload probeerde de instructies van de agent te wijzigen richting destructive actions. De malicious code werd met v1.84.0 verspreid, maar werd niet uitgevoerd vanwege een syntax error. AWS trok credentials in, verwijderde de code en bracht v1.85.0 uit. De payload faalde door die syntax error, niet doordat een security control deze blokkeerde.
Azure Web Apps MCP service — CVE-2026-32211
Microsofts vendor CVE-record gaat over ontbrekende authentication in de hosted Azure Web Apps MCP service. Het is geen advisory tegen elke lokale Azure MCP-server of SDK. Een caller die een unauthenticated tool service bereikt, kan het model volledig omzeilen; de deployed service moet de request authenticeren en autoriseren.
Claude Code project-trust vulnerabilities
Dit waren afzonderlijke vulnerabilities, geen vereiste stappen van één aanval:
- Een trust-warning bypass werd opgelost in 1.0.87.
- Pre-trust execution, CVE-2025-59536, werd opgelost in 1.0.111. Repositoryconfiguratie kon execution triggeren voordat het project was vertrouwd.
- Endpoint/API-key exposure, CVE-2026-21852, werd opgelost in 2.0.65. Untrusted configuration kon API traffic omleiden en credentials blootleggen.
Project trust, hook execution en endpointconfiguratie zijn host controls. Een content classifier kan niet voorkomen dat code wordt uitgevoerd vóór de model call.
Axios 1.14.1 en 0.30.4 — 31 maart 2026
De postmortem van de maintainer identificeert twee malicious releases, 1.14.1 en 0.30.4, die de dependency plain-crypto-js@4.2.1 bevatten. Die dependency installeerde een remote access trojan: malware die een aanvaller remote access tot de machine geeft. Exposure vereiste dat de getroffen versies werden resolved en dat het relevante installgedrag werd uitgevoerd; een losstaande npm install haalde ze niet automatisch op. Dit is een supply-chain execution failure, onafhankelijk van modelgedrag.
Trivy Actions tag hijack — 19 maart 2026
De Aqua advisory beschrijft dat 76 van 77 trivy-action-version tags en zeven setup-trivy-tags naar malicious content werden omgeleid. De malicious action entrypoint verzamelde geheugen van runner-processen en credentialbestanden; schrijf die volledige collection path niet toe aan de scanner binary. Het latere Docker Hub-incident kende een afzonderlijk exposure window.
Een workflow die tijdens de compromise een getroffen tag resolvede, kon de payload uitvoeren. Tags zijn movable references, dus pin reviewed Actions aan immutable commit SHAs en verifieer dependency changes. Een coding agent kan dezelfde onveilige reference naar meer workflowfiles verspreiden.
OpenAI / Hugging Face — evaluaties in juli 2026
OpenAI’s incident report van 26 augustus beschrijft hoe interne cybersecurity-evaluation agents via gedeelde infrastructuur het internet bereikten, samenwerkten via een ongeautoriseerd message board en Hugging Face-systemen compromitteerden. Het principal model was alleen intern beschikbaar en de evaluaties draaiden met gereduceerde safeguards. Dit is bewijs over die evaluation environment, geen gemeten failure rate voor publiek gedeployde agents.
De engineeringles is dat een toegestane interne service een outbound path of cross-session communication channel kan worden. Test wat een package mirror, proxy en shared store namens de agent kunnen doen, niet alleen of de sandbox een directe internetverbinding kan openen. METR’s afzonderlijke investigation onderzocht agentgedrag en samenwerking; het evalueren van safeguard effectiveness en remediation viel buiten de scope.
Asynchronous provider intervention
OpenAI’s misalignment monitoring kan na output of actions ingrijpen. Voor covered models kunnen Responses requests met persisted reasoning, WebSockets of OpenAI compaction automatisch worden gestopt. Andere Responses requests kunnen alerts genereren zonder automatisch te stoppen; Chat Completions valt buiten dit systeem. Een webhook maakt blocking niet mogelijk.
Een geblokkeerde request rapporteert misalignment_policy_violation, met HTTP 403 vóór streaming; errors kunnen ook midden in een stream aankomen. Stop verdere actions, bewaar request- en toolrecords en vraag om operator review. Retry niet automatisch. Eerdere effecten blijven bestaan en alerts kunnen foutief zijn of problemen missen. Dit voegt detection toe; het vervangt geen lokale authorization.
De OWASP ASI Top 10, editie 2026
OWASP’s Agentic Security Initiative (ASI) is een working group die specifiek gericht is op LLM-driven agents. Op 9 december 2025 publiceerde deze de Agentic Security Initiative Top 10 for 2026: een catalogus van tien agent-security risk categories.
Gebruik deze als checklist voor threat-model coverage, niet als gemeten ranking van incidentfrequenties:
Content filters kunnen bijdragen aan het detecteren van malicious instructions, waaronder goal hijack en memory poisoning. Categories overlappen: geen enkele category behoort uitsluitend aan een text filter toe. Koppel elk attack path aan identity-, tool policy-, memory-, orchestration-, monitoring- en supply-chain controls waar toepasselijk. EchoLeak valt onder ASI01. Amazon Q valt onder ASI04 (Supply Chain) en ASI02 (Tool Misuse). Azure MCP is ASI03 (Identity). Claude Code CVE-2025-59536 bestrijkt ASI05 (Code Execution), ASI04 en ASI03. Axios en Trivy zijn ASI04. Deze mapping laat zien waarom het threat model verder moet gaan dan modelinput en -output.
Permission is infrastructure, niet prompt
Hier houden guardrails op het product te zijn en worden ze één subsystem van een harness. Drie huidige systemen (OpenAI Agents SDK, Codex CLI en Claude Code) laten zien hoe een production policy surface er werkelijk uitziet. Alle drie handhaven permission in code. Geen ervan vertrouwt erop dat het model voorzichtig is.
OpenAI Agents SDK
De SDK scheidt harness van compute. Hosted MCP tools nemen require_approval — ofwel de kale string "always" / "never", of een filterobject met die twee policies als keys en de toolnamen waarop ze van toepassing zijn — plus een on_approval_request-callback die afgaat voor elke tool die onder "always" blijft en {"approve": bool} teruggeeft met een optionele reason. Fine-grained tool filtering (tool_filter) is beschikbaar op de local server-varianten (MCPServerStdio, MCPServerStreamableHttp, MCPServerSse) als je die nodig hebt:
# No-run: illustrative Agents SDK shape; requires openai-agents, a configured hosted MCP server, and credentials.
from agents import Agent, HostedMCPTool
def approve(request):
# Only tools under the "always" policy reach this callback.
if request.data.name == "delete_repo":
return {"approve": False, "reason": "escalate to a human reviewer"}
return {"approve": True}
agent = Agent(
name="Ops",
tools=[HostedMCPTool(
tool_config={
"type": "mcp",
"server_label": "github",
"server_url": "https://mcp.example.com",
"require_approval": {
"always": {"tool_names": ["delete_repo"]},
"never": {"tool_names": ["list_issues"]},
},
},
on_approval_request=approve,
)],
)
De approval callback is code. De per-tool approval policy is code. Je kunt dit bestand lezen, testen en diffen. Dat geldt niet voor een system prompt die zegt: “wees voorzichtig met production.”
Codex CLI en de managed policy layer
OpenAI’s coding harness ondersteunt een managed requirements.toml file dat IT-afdelingen via device management kunnen pushen. Op Unix-systemen staat het system file op /etc/codex/requirements.toml. Het werkt als een hard-constraint layer, zodat project-level settings de regels niet kunnen overrulen:
# /etc/codex/requirements.toml
[rules]
prefix_rules = [
{ pattern = [{ token = "rm" }, { any_of = ["-rf", "-fr"] }], decision = "forbidden", justification = "Recursive force-delete prohibited by IT policy" },
]
prefix_rules.decision accepteert alleen "prompt" of "forbidden", nooit "allow". Een project kan zichzelf geen permission geven die door de managed layer wordt verboden. MCP allowlists worden bepaald door zowel name als identity, zoals een command string of URL. Een project kan dus niet beweren github-mcp te zijn en naar de server van een aanvaller wijzen. De ondersteunde requirements verschillen per client en versie. De huidige documentatie vereist specifiek Codex 0.138.0 of hoger voor managed permission-profile keys. Test daarom elke requirements policy tegen elke clientversie in de fleet vóór rollout.
Claude Code’s permission ladder
Claude Code publiceert niet één vaste sequence van zes checks voor elke tool call. De permission rules worden deny → ask → allow geëvalueerd; de eerste matching rule bepaalt de uitkomst. Een PreToolUse hook draait vóór de permission prompt. Een hook kan een call blokkeren, maar een hook result omzeilt geen matching deny- of ask-rule. De actieve permission mode handelt calls af die de rules niet oplossen. De Claude Agent SDK heeft een afzonderlijke canUseTool callback voor unresolved requests. Die callback is een SDK-control, geen Claude Code CLI permission check.
Modes wisselen met Shift+Tab tussen default → acceptEdits → plan. auto, bypassPermissions en dontAsk worden onder specifieke entry conditions geactiveerd die de enterprise-managed policy layer kan uitschakelen. Dit is meer dan een config file die op correctheid wordt gecontroleerd. Het is een state machine met precedence rules, gepubliceerd zodat een securityteam deze kan analyseren.
Drie blast radii in één file
Dit is de vorm van een Codex-style permission config met een default en twee named profiles:
# ~/.codex/config.toml
approval_policy = "on-request"
sandbox_mode = "workspace-write"
[profiles.ci]
approval_policy = "never"
sandbox_mode = "read-only"
[profiles.release]
approval_policy = "untrusted"
sandbox_mode = "danger-full-access"
[mcp_servers.github]
command = "gh-mcp"
args = ["--readonly"]
Twee keys doen het werk en zijn onafhankelijk. approval_policy bepaalt wanneer een mens wordt gevraagd. on-request laat de agent escaleren wanneer deze tegen een blokkade aanloopt. never vraagt helemaal niets. untrusted stopt bij elk command dat niet op de trusted list staat. sandbox_mode bepaalt wat het command kan benaderen als het draait.
CI onderbreekt niemand en kan niet schrijven. Release kan de hele machine bereiken, maar moet bijna alles eerst met een mens afhandelen. Het releaseprofile betaalt voor dat bereik: danger-full-access schakelt de sandbox uit, waardoor untrusted approval de enige overgebleven control is. Alles buiten de trusted list vereist menselijke goedkeuring of wordt niet uitgevoerd. Die trusted list is nu de volledige security boundary.
De default- en CI-profielen houden de kernel onder zich: Seatbelt op macOS, bubblewrap plus seccomp op Linux en restricted tokens op Windows. In alle gevallen speelt de mening van het model geen rol.
Sandbox enforcement is een OS-vraagstuk
De kernel doet hier het echte werk. Elk OS biedt een ander toolkit, en de twee CLIs kiezen niet altijd hetzelfde onderdeel:
| Platform | Claude Code | Codex CLI |
|---|---|---|
| macOS | Seatbelt via sandbox-exec met een SBPL (Seatbelt Profile Language)-profile | Seatbelt via sandbox-exec -p |
| Linux | bubblewrap + socat network proxy | bubblewrap + seccomp (legacy Landlock via use_legacy_landlock) |
| Windows | WSL2 vereist | Native restricted tokens + workspace ACLs + capability SIDs |
Ze zijn het eens waar het OS één optie biedt (Seatbelt, bubblewrap) en verschillen waar dat niet zo is. Claude Code’s sandbox vereist WSL2 op Windows; dit is een sandboxbeperking, niet de claim dat de CLI niet native kan draaien. Codex levert een native Windows-sandbox. In beide gevallen gebeurt enforcement in de kernel, niet in het model.
Codex’ Linux-pad stapelt drie kernel-level locks rond het command. PR_SET_NO_NEW_PRIVS voorkomt dat het process extra privileges verkrijgt, zelfs als het dit probeert. Een seccomp-filter laat de kernel volledige classes van system calls weigeren. Network restrictions hangen af van de vraag of networking is uitgeschakeld of een proxymode is geconfigureerd; ze zijn niet universeel beperkt tot Unix sockets. Zie de Linux sandbox implementation. Een verse geïsoleerde /proc verbergt de rest van de machine.
Codex hardent ook zijn eigen binary bij startup op elk Unix-platform. Het zet RLIMIT_CORE=0 om crash dumps te onderdrukken en weigert debugger attach. Dat is een andere boundary dan de sandbox.
De Windows sandbox heeft twee modes. unelevated gebruikt een restricted token als user en biedt niet de network enforcement van de elevated mode. elevated gebruikt dedicated sandbox users, firewall rules en filesystem ACLs.
Wanneer network access uitstaat, plaatst Codex stub-.bat- en .cmd-files voor ssh en scp in een directory aan het begin van PATH. Die commands eindigen met een non-zero status in plaats van de echte binaries te bereiken. Codex wijst ook HTTP_PROXY, HTTPS_PROXY, ALL_PROXY en de Git proxy variables naar een dode lokale port. Deze maatregelen beïnvloeden coöperatieve tools; proxy variables en command stubs alleen kunnen willekeurige networking niet beperken.
Een sandbox heeft ook een gedefinieerde failure mode nodig. In Claude Code’s huidige configuratie stopt sandbox.failIfUnavailable: true de execution wanneer sandboxing niet kan starten. allowUnsandboxedCommands: false schakelt de unsandboxed retry escape hatch van de agent uit, maar excludedCommands omzeilt dit nog steeds. Test de effectieve configuratie met ontbrekende dependencies en een verboden destination.
Credential brokering is nu ook lokaal beschikbaar. Claude Code’s sandbox.credentials kan named files en environment variables weigeren. Environment-variable masking, beschikbaar sinds v2.1.199, geeft sandboxed commands een placeholder en laat de proxy de echte value injecteren in requests naar geconfigureerde hosts. Gebruik smalle injectHosts en configureer TLS termination; masking autoriseert de gevraagde operation niet. Deze controls dekken sandboxed Bash commands, dus hooks, MCP-processen en andere execution paths hebben nog steeds hun eigen credential policy nodig.
Isolation choices buiten Claude Code en Codex
Als je je eigen agent bouwt, blijkt “sandbox” een umbrella term te zijn. De open-sourceopties liggen op een spectrum — van lightweight namespace wrappers aan de ene kant tot volledige microVMs aan de andere — en je keuze hangt af van hoeveel vertrouwen je hebt in de code die erin draait.
Light isolation — dezelfde kernel, minder privileges:
- bubblewrap — een low-level namespace sandbox constructor die door Flatpak en Claude Code op Linux wordt gebruikt. De caller moet de filesystem-, network- en optionele seccomp-policy kiezen; bubblewrap zelf is geen kant-en-klare security policy.
- Standaard Docker / OCI containers — namespace isolation boven op een gedeelde host-kernel. Geen sandbox voor untrusted code; de eigen docs van gVisor leggen dit uit (“containers are not a sandbox”). Een redelijk startpunt in combinatie met seccomp en AppArmor, maar niet meer dan dat.
Application-kernel isolation — de agent praat met een fake kernel:
- gVisor — de user-space kernel van Google. Je container denkt dat deze op Linux draait, terwijl een Go-kernelimplementatie system calls onderschept. Dit vermindert directe exposure aan de host-kernel zonder een guest VM, met trade-offs voor compatibility en performance.
Full VM isolation — een dedicated kernel per sandbox:
- Firecracker — AWS’ microVM-technologie. Elke VM heeft zijn eigen Linux-kernel onder KVM; containers delen de host-kernel. Een compromise in één VM moet nog steeds de VMM- of host-controls passeren om de host of een andere VM te beïnvloeden, dus Firecracker’s Jailer en een gepatchte host blijven onderdeel van de bescherming.
- Kata Containers — container UX met VM-grade isolation. Waar Kubernetes-clusters naartoe gaan wanneer ze untrusted code moeten draaien.
Platforms — wat je huurt in plaats van bouwt:
- E2B verpakt Firecracker in een hosted sandbox API.
- OpenSandbox scheidt de SDK van door administrators geconfigureerde runtime-isolation. Default Docker runc is geen microVM; het Firecracker-pad gebruikt Kata met Firecracker via Kubernetes.
- Microsofts Agent Governance Toolkit (MIT-licensed, april 2026) voegt daarbovenop een runtime policy engine toe. Deze koppelt policy controls aan de OWASP ASI Top 10. De claim over launch latency betreft één policy engine, niet de totale kosten van elke check in een deployed agent.
Kies het isolation level op basis van het trust level van de code, de tenant boundary, network access, host data en recovery cost. Namespace- en seccomp-controls kunnen passen bij trusted internal tools. LLM-generated code en untrusted packages vereisen een sterkere boundary, zoals gVisor, Kata of een microVM, gevolgd door tests tegen de escape- en exfiltration paths in je eigen threat model.
Claude Code en Codex kozen uit hetzelfde menu als iedereen. Ze verpakten het alleen anders.
PreToolUse hooks als programmeerbare policy
Modes en allowlists behandelen de eenvoudige gevallen: “laat de agent files editen maar geen bash uitvoeren”, “weiger alles wat op rm -rf lijkt.” Ze falen wanneer je policy echte logica nodig heeft. Je wilt git push alleen blokkeren wanneer de branch main is. Je wilt elke Edit weigeren die een file raakt waarvan de naam aan een secret-regex voldoet. Je wilt shell calls per session rate-limiten of elke tool invocation naar je centrale audit log sturen (de SIEM, het security information and event management-systeem dat je securityteam al bewaakt).
Geen daarvan past in een statische allowlist. Daar zijn hooks voor — shell commands die Claude Code op specifieke punten in de tool-call lifecycle uitvoert, met de mogelijkheid om de pending call te inspecteren en een structured allow/deny terug te geven. Claude Code stelt ongeveer dertig lifecycle events beschikbaar (de volledige lijst staat in de docs), waarvan er één alles anders rangschikt: een PreToolUse hook die permissionDecision: "deny" teruggeeft, blokkeert een tool ongeacht de mode.
Dit is de vorm van de settings:
{
"permissions": {
"defaultMode": "acceptEdits",
"deny": ["Bash(rm -rf:*)", "Bash(sudo:*)", "Read(.env*)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/pre-bash-firewall.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-paths.sh"
}
]
}
]
}
}
De statische deny-rules en de dynamic hooks hebben verschillende failure modes. Een PreToolUse-resultaat van deny blokkeert vóór de normale permission flow, maar een timed-out command-, HTTP- of MCP-tool-hook is non-blocking: Claude Code gaat verder met die flow. In dit voorbeeld kan acceptEdits dus een Edit of Write goedkeuren wanneer protect-paths.sh time-out. Laat een non-negotiable path restriction niet uitsluitend afhangen van een command hook. Plaats statische restrictions in deny rules of de sandbox; kies voor dynamische policy een control waarvan evaluator failure restrictief blijft en test zowel het timeoutgedrag als een expliciete deny.
Een hook kan een shell script van vijf regels zijn of een volledige policy engine. De return shape is wat telt:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "writes outside workspace prohibited"
}
}
Het model ziet een structured deny. De reasoning loop uit Part 1 behandelt deze als elke andere tool observation: de denial wordt context, de agent replannet en de loop gaat verder. Dat is wat “permission is infrastructure” oplevert. De deny is gekoppeld aan hetzelfde mechanisme dat een 500 van een HTTP-tool afhandelt. Het is geen aparte security workflow die er achteraf aan moet worden toegevoegd.
Een veelvoorkomend anti-pattern is een system prompt schrijven met “verwijder geen files zonder expliciete bevestiging van de gebruiker”, de agent shippen en op die instructie als control vertrouwen. Een injected prompt of een tool result die door een aanvaller wordt beheerd, kan om die instructie heen routeren. Het model is geen policy engine. Het kan het patroon matchen dat je hebt geschreven, of een patroon dat door een aanvaller is aangeleverd.
Human approval werkt alleen als escalation
Content filters inspecteren wat het model zegt. Permission rules inspecteren de tool call voordat deze wordt uitgevoerd. Human-in-the-loop review behandelt de actions waarvoor nog steeds een mens nodig is. Als mensen 93% van de prompts goedkeuren, inspecteer dan de escalation rules en de promptkwaliteit.
LangGraph levert het pause/resume-primitive. HumanLayer verpakt het approval channel, en Anthropic’s usage data laat zien waarom het aantal en de kwaliteit van escalations moeten worden gemeten.
Het LangGraph-primitive
LangGraph’s interrupt() + Command(resume=value) pauzeert een graph, persisteert zijn state via de geconfigureerde checkpointer en hervat met een door een mens aangeleverde value. Of die resume veilig is, hangt af van één detail in de docs:
“When execution resumes (after you provide the requested input), the runtime restarts the entire node from the beginning — it does not resume from the exact line where
interruptwas called.”
Uit dat restartgedrag volgen drie constraints:
1. Side effects vóór interrupt() moeten idempotent zijn. Wanneer de mens antwoordt, draait de hele node opnieuw vanaf het begin, niet vanaf de regel interrupt(). Als je node dus een e-mail verstuurt, pauzeert voor approval en vervolgens “sent” teruggeeft, wordt de e-mail bij resume een tweede keer verstuurd. Oplossing: plaats side effects na de interrupt of maak ze veilig herhaalbaar (dedupe keys, upsert in plaats van insert, cache op message ID).
2. Interrupts worden aan resumes gekoppeld op index, niet op naam. Als één node twee interrupt()-calls bevat, koppelt LangGraph ze aan Command(resume=...)-values in de volgorde waarin ze afgaan. Branching die het aantal interrupts verandert (een if dat er bij resume één overslaat, of een loop die een ander aantal keer iterereert) brengt de indexes uit lijn, waardoor een resume value bij de verkeerde interrupt kan landen.
3. Houd payloads JSON-safe. De docs van LangGraph vereisen JSON-serializable values voor interrupt() en resume payloads. Gebruik strings, numbers, booleans, arrays en dictionaries met zulke values. Vermijd functions, class instances en andere complexe objects, omdat serialization afhangt van de geconfigureerde checkpointer. Converteer approval data naar dictionaries en primitives voordat je deze aan interrupt() doorgeeft of via een HTTP API exposeert.
De drie canonical patterns:
# No-run: illustrative LangGraph sketches; requires LangGraph, a tool decorator, interrupt, and smtp_send.
# (a) Approval check
@tool
def send_email(to, subject, body):
resp = interrupt({"action": "send_email", "to": to,
"subject": subject, "body": body})
if resp.get("action") == "approve":
return smtp_send(to, subject, body)
return "Email cancelled"
# (b) Edit-and-continue
def review_node(state):
edited = interrupt({"content": state["generated_text"]})
return {"generated_text": edited}
# (c) Mid-run state correction — conditional edge
class AgeState(TypedDict):
age: int | None
pending_question: str | None
def get_age_node(state: AgeState):
question = state.get("pending_question") or "What is your age?"
answer = interrupt(question) # once per node invocation
if isinstance(answer, int) and answer > 0:
return {"age": answer, "pending_question": None}
return {"pending_question": f"'{answer}' is not valid. Please enter a positive number."}
def route_age(state: AgeState):
return END if state.get("age") is not None else "get_age"
builder = StateGraph(AgeState)
builder.add_node("get_age", get_age_node)
builder.add_edge(START, "get_age")
builder.add_conditional_edges("get_age", route_age)
Resume is graph.invoke(Command(resume={"action": "approve"}), config=cfg). LangGraph 0.4+ ondersteunt dict-based multi-interrupt resume voor parallelle branches, wat belangrijk wordt zodra je agent uitwaaiert.
HumanLayer: approval als product
HumanLayer is de managed versie van hetzelfde idee. Decorate een function en approval requests worden naar Slack, e-mail of Discord gerouteerd, met rules voor wie wordt gepingd. Wanneer de agent multiply(2, 5) probeert aan te roepen, zien de logs er zo uit:
last message led to 1 tool calls: [('multiply', '{"x":2,"y":5}')]
HumanLayer: waiting for approval for multiply
De approver klikt in Slack op approve of deny. Bij een deny formuleren de HumanLayer-docs het als volgt: “HumanLayer will pass your feedback back to the agent, which can then adjust its approach.” Die feedback laat de agent zijn plan aanpassen in plaats van een rejection als dead end te behandelen.
Approval fatigue in de data
Anthropic publiceerde de echte data in februari 2026. Drie findings zijn belangrijker dan de rest.
“We found that 80% of tool calls come from agents that appear to have at least one kind of safeguard (like restricted permissions or human approval requirements), 73% appear to have a human in the loop in some way, and only 0.8% of actions appear to be irreversible.”
Dat is het goede nieuws. Behandel 80% als een bovengrens, want Anthropic’s footnote 14 voegt toe: “Claude often overestimated human involvement, so we expect 80% to be an upper bound.”
“Newer users (<50 sessions) employ full auto-approve roughly 20% of the time; by 750 sessions, this increases to over 40% of sessions.”
Dit is de drift. Gebruikers beginnen voorzichtig en worden minder voorzichtig naarmate ze vertrouwen opbouwen in de tool. Dat is menselijk gedrag en geen karakterfout. Het is een telemetry signal dat je systeem moet volgen. (Een kleine fact-check: secundaire berichtgeving citeerde dit vaak als “20% → over 50%”. Tegen de primaire data van Anthropic is het geverifieerde getal 20% → over 40%. Als je het getal 50% hebt gezien, daar komt het vandaan.)
Anthropic’s engineering post van maart 2026 over Claude Code’s auto mode bevat het kerngetal:
“Claude Code users approve 93% of permission prompts. We built classifiers to automate some decisions, increasing safety while reducing approval fatigue… If a session accumulates 3 consecutive denials or 20 total, we stop the model and escalate to the human.”
Een hoog approvalpercentage is aanleiding om te onderzoeken of prompts mensen helpen beslissen; het bewijst geen universele failure threshold. Anthropic’s two-stage classifier vermindert het aantal prompts en escaleert clustered denials. Het gerapporteerde false-positivepercentage van 0,4% gebruikte 10.000 traffic examples. False-negative rates waren 17% op 52 echte overeager actions en 5,7% op 1.000 synthetische exfiltration cases. Dit zijn verschillende populaties, geen complementaire percentages en geen bewijs dat classification sandboxing vervangt.
Meet de kwaliteit van escalation
Allowlist routineuze, reversibele actions en log ze. Escaleer actions waarvan de side effects een boundary overschrijden die de runtime niet kan undoën, zoals een extern bericht, een production write, een force push of een betaling. Anthropic formuleert het doel als het behouden van de mogelijkheid voor een mens om in te grijpen wanneer de beslissing echte gevolgen heeft.
Volg de volledige funnel in plaats van een geleend approval-rate target na te streven: proposed actions, automatic allows, escalations, approvals, denials, edits en incidents na approval. Een hoog approvalpercentage kan betekenen dat de prompts routinematige ruis zijn. Een hoge denial- of edit-rate kan betekenen dat de planner de verkeerde action voorstelt of de informatie verbergt die een approver nodig heeft. De nuttige threshold hangt af van de action class en de kosten van een false allow; bepaal deze dus aan de hand van je eigen incident- en reviewdata.
MCP-scoping en de supply chain
MCP verbindt agents met externe tools zoals Slack, GitHub en databases, waardoor het authorization model onderdeel wordt van de security boundary. De specification revisions van 2025 scheidden de rollen van token issuer en resource server en voegden resource indicators toe. Die geschiedenis verklaart welke audience- en forwarding checks een server vandaag moet handhaven.
MCP authorization in drie revisions
Authorization was optioneel voor MCP-implementaties in de spec van 2025-03-26. Voor een production HTTP-deployment die userdata of tools beschermt, adviseer ik OAuth 2.1 met PKCE (Proof Key for Code Exchange), wat de specification vereist wanneer een implementation OAuth authorization ondersteunt. Het vroege design liet één MCP-server twee rollen vervullen. De authorization server geeft tokens uit; de resource server accepteert ze. Dit zijn afzonderlijke rollen, ook wanneer één service beide uitvoert. Als die service een request doorstuurt naar een andere server, kan dezelfde credential terechtkomen waar deze nooit bedoeld was. Dat is het gat.
De revision van 2025-06-18 maakte de rollen expliciet. Een protected MCP-server fungeert als OAuth resource server, terwijl een authorization server het token uitgeeft. De authorization server kan met de resource server worden gehost of afzonderlijk draaien. RFC 8707 Resource Indicators binden het token aan een target resource, en RFC 9728 Protected Resource Metadata geeft de client een expliciet discovery path. De spec verbiedt een MCP-server ook om een token van een client upstream door te sturen.
De revision van 2025-11-25 behield die splitsing en werkte aan de onderdelen die een client goed moet implementeren. Discovery van de authorization server kreeg OpenID Connect Discovery, zodat een client de juiste issuer kan vinden in plaats van te gokken. Incremental scope consent verhuisde naar de WWW-Authenticate-header, waardoor een server op het moment dat deze een scope nodig heeft om één extra scope kan vragen in plaats van alles vooraf te eisen. Client registration kreeg OAuth Client ID Metadata Documents als aanbevolen mechanisme, waarmee dynamic registration voor de meeste deployments wordt vervangen. Discovery van Protected Resource Metadata werd eveneens afgestemd op RFC 9728, waardoor WWW-Authenticate optioneel werd met een .well-known-fallback.
Controleer de versioning page voordat je implementeert. Op 6 september 2026 is de huidige revision 2026-07-28. Deze vereist dat elke request de protocol version declareert en laat de server elke request afzonderlijk accepteren of weigeren. Een client kan vooraf server/discover aanroepen om een version te selecteren, maar discovery is optioneel. Declaratie en negotiation per request blijven vereist, ook wanneer de client een unsupported-version error afhandelt en opnieuw probeert met een mutually supported version.
Audience binding beperkt replay tegen de verkeerde MCP-server. Het neutraliseert niet de afzonderlijke Claude Code-configuratievulnerabilities die hierboven zijn beschreven: een host-side hook kan nog steeds worden uitgevoerd voordat het model start, en een untrusted project kan nog steeds proberen lokale configuratie te wijzigen. Token scope, project trust, hook policy en sandboxing blijven afzonderlijke controls.
De MCP-checklist voor 2026
Als je MCP in production shipt of consumeert:
- Behandel authentication als production requirement, niet als protocoldefault. MCP laat authorization optioneel, maar ik adviseer OAuth 2.1 met PKCE voor een protected HTTP-deployment. De advisory over de hosted Azure Web Apps MCP service ging over ontbrekende authentication. Als je server traffic accepteert zonder caller credentials te verifiëren, heb je een tool gebouwd die iedereen met netwerktoegang kan aanroepen.
- Tokens zijn audience-bound. Vraag een token aan voor de target MCP-resource en valideer dat het aangeboden token jouw server als audience noemt. Weiger tokens die voor een andere resource zijn gemint.
- Isoleer read- en write-authority bewust. MCP bindt een token aan een resource server, niet aan een individuele tool. Als een Slack-server een credential met
chat:writeaccepteert en deze naar zowel read- als write-handlers routeert, kan een read-oriented tool via de policy van die server een message-sending path worden. Gebruik afzonderlijke resource servers of afzonderlijke credentials en authorization checks wanneer read- en write-operations onafhankelijke blast radii nodig hebben. - Gebruik verse, kortlevende tokens in plaats van permanente API keys. Het Claude Managed Agents vault pattern (Anthropic engineering) is het referentiepunt: de agent zelf ziet de echte credentials nooit. De proxy haalt de bijbehorende opgeslagen credentials uit de vault, roept de tool namens de agent aan en geeft het resultaat terug. Dat voor elke call een vers token wordt gemint, is geen gedocumenteerde garantie; short-lived credentials zijn een deployment recommendation.
Supply-chain controls blijven gelden
De axios- en Trivy-incidents zijn bekende package- en CI-supply-chain failures, toegepast op systemen die dependency-installatie automatiseren. Automation verhoogt het aantal en de snelheid van executions, dus version-, provenance- en review-controls moeten draaien voordat het gegenereerde command CI of een sandbox bereikt.
De defense is eenvoudig:
- Pin versions in de lockfile. Agents mogen nooit een floating version resolven — geen
@latest, geennpm update, geen--upgrade. - Scan in CI met tools die onafhankelijk zijn van het component dat wordt gecontroleerd.
- Gebruik GitHub commit SHAs voor Actions, geen tags.
- Review dependency diffs op agent-driven PRs vóór merge.
Dit zijn standaard supply-chain controls. Agent automation verandert hun frequentie, niet hun mechanisme.
Een policy stack voor de Market Analyst Agent
De Market Analyst Agent uit Part 1 is een kleine LangGraph-agent die market data ophaalt en een analyst report schrijft — maar niet zo klein als die beschrijving suggereert. Naast de market-data tools draait deze een allowlisted CLI via subprocess, evalueert modelgeschreven Python in process en maakt simulated trade records. De agent oefent tool invocation, code execution en approval routing uit, maar plaatst geen echte orders. Zo ziet een minimale policy stack eruit.
Layer 1: een PreToolUse hook die vóór execution weigert
Zelfs een agent die “alleen stock data leest” kan naar dingen grijpen die niet mogen: een curl naar een attacker-controlled URL, writes buiten de workspace, git-mutations in de host repo. Een deny rule is infrastructure, geen prompt. De onderstaande sketch retourneert de eigen decision shape van de agent, niet de omringende hookSpecificOutput-envelope die Claude Code verwacht.
# agent/permissions.py
from pathlib import Path
DENY_COMMANDS = frozenset({
"rm -rf", "sudo", "chmod 777",
"curl -X POST", "wget", "nc ",
})
WORKSPACE = Path("./workspace").resolve()
def _outside_workspace(path: str) -> bool:
# Resolve first: "~/.ssh/id_rsa" and "workspace/../../etc" both
# have to become real paths before the comparison means anything.
return not Path(path).expanduser().resolve().is_relative_to(WORKSPACE)
def pre_tool_use(tool_name: str, args: dict) -> dict | None:
if tool_name == "shell":
cmd = args.get("command", "")
if any(bad in cmd for bad in DENY_COMMANDS):
return {"permissionDecision": "deny",
"reason": f"command pattern disallowed: {cmd!r}"}
if tool_name == "write_file":
path = args.get("path", "")
if _outside_workspace(path):
return {"permissionDecision": "deny",
"reason": f"path outside workspace: {path!r}"}
return None # fall through to mode / canUseTool
De sketch maakt het control point zichtbaar. De hook retourneert een structured deny en de reasoning loop ontvangt die denial als tool observation.
De path check is een allowlist: één workspace root, al het andere wordt geweigerd. Een deny-list van forbidden prefixes blokkeert alleen paths waar je aan hebt gedacht. ~/.ssh/id_rsa wordt nooit gespeld zoals je het hebt opgeschreven. De command check is nog steeds een deny-list. Substring matching is geen production shell policy. Een echte implementation moet het command parsen en bij execution vertrouwen op de OS-sandbox. De sketch is zelf geen execution boundary: als een externe hook deze uitvoert, moet een timeout een onafhankelijke workspace restriction actief laten.
Layer 2: een input canary voor prompt injection
Agent-goal hijack (ASI01) komt vaak binnen via een retrieved web page, een user message of een research-paper-PDF. Een goedkope regex canary vangt letterlijke instruction patterns en creëert een nuttig telemetry event. Deze mist obfuscated, meertalige en contextafhankelijke injections en kan dus niet als decision boundary dienen:
# agent/input_canary.py
import re
INJECTION_PATTERNS = [
re.compile(r"ignore\s+(?:all\s+|any\s+|the\s+)?"
r"(?:previous\s+|prior\s+|above\s+|earlier\s+)?"
r"(?:instructions|rules|prompts?)",
re.IGNORECASE),
re.compile(r"you are now|act as|roleplay as", re.IGNORECASE),
re.compile(r"system[ _:]*prompt", re.IGNORECASE),
re.compile(r"<\|im_(start|end)\|>"),
]
def input_canary(text: str) -> dict | None:
for pat in INJECTION_PATTERNS:
m = pat.search(text)
if m:
return {"flag": "possible_injection", "match": m.group(0)}
return None
Log flagged inputs; reject niet automatisch. False positives zijn hier duur voor een research assistant. Maar de log laat je zien wanneer het aantal flags bij één gebruiker plotseling toeneemt.
Layer 3: structured output validation via een stop hook
Een Pydantic model plus een Stop hook geeft je een strakke validate-then-retry-loop voor report generation. De agent kan niet beweren “done” te zijn totdat de output schema validation en een smoke test doorstaat:
# No-run: illustrative policy sketch; requires Pydantic and the repo-local agent.schemas module.
# agent/stop_hook.py
from pydantic import ValidationError
from agent.schemas import MarketReport
def on_stop(final_output: str) -> dict:
try:
report = MarketReport.model_validate_json(final_output)
except ValidationError as e:
return {"decision": "continue",
"feedback": f"schema invalid: {e.errors()[:3]}"}
if not report.tickers:
return {"decision": "continue",
"feedback": "no tickers in report — did you skip the snapshot step?"}
return {"decision": "allow_stop"}
Een schema check en één smoke test maken het verschil tussen “de agent zei dat hij klaar was” en “de output is daadwerkelijk een report.”
Layer 4: approval vóór outbound actions
De execute_trade van de market analyst is een simulated, idempotente state transition en demonstreert dus approval routing in plaats van een onomkeerbaar financieel side effect. Voor een echte outbound integration — e-mail, Slack, een report naar een klant of een brokerage order — toon je de voorgestelde action aan de persoon en wacht je op approval of rejection voordat de tool draait. Gebruik interrupt() voor die pause:
# No-run: illustrative outbound-tool sketch; requires LangGraph, a tool decorator, and smtp_send.
# agent/tools/notify.py
from langgraph.types import interrupt
@tool
def send_report(to: str, body: str):
resp = interrupt({
"action": "send_report",
"to": to,
"body": body, # Review the complete content that will execute.
})
if resp.get("action") == "approve":
return smtp_send(to, body)
return "send cancelled by human"
Outbound actions maken de lethal trifecta compleet. Vereis approval voor outbound actions wanneer de bestaande authorization en deployment policy deze niet dekken. Bind approval aan de volledige destination en content; gewijzigde arguments vereisen een nieuwe beslissing. Berichten aan finance, klanten of andere externe recipients moeten de approver tonen wat er wordt verstuurd en waarheen.
Wat deze stack niet doet
Dit beschermt niet tegen:
- Een gecompromitte dependency upstream (axios-class). De agent voert uit wat
uv synczegt uit te voeren. - Een malicious
.mcp.jsonin een cloned repo (CVE-2025-59536-class). Het permission model van de host MCP-client is waar dit wordt opgevangen, niet de code van de agent. - Een data-theft chain opgebouwd uit legitieme tools (EchoLeak-class) — de agent leest private data, haalt externe URLs op en verstuurt berichten naar buiten. Breek of beperk dat exfiltration path met scoped data access, trusted routing, egress restrictions en waar nodig approval. Het verwijderen van één capability blokkeert dit specifieke path, niet elke mogelijke aanval.
- Een escape uit
execute_python_analysis, de in-process Python evaluator van de agent. Deze blokkeert een lijst statement types, weigert elke identifier die met een underscore begint en staat imports alleen toe uitjson,mathenstatistics. Maarexecin het workerprocess is geen boundary: een bypass draait met de file handles en network access van de worker. Plaats deze achter filesystem-, network- en credential-isolation plus CPU-, memory- en timelimits voordat je untrusted code evalueert. Een subprocess alleen erft toegang en is geen security sandbox.
Deze vier layers zijn lokale policy, en lokale policy is de binnenste laag die je beheert, niet de enige. Elk item in die lijst moet ergens anders worden opgevangen — in de lockfile, in de MCP-client, in de process boundary rond generated code of in controls die voorkomen dat untrusted instructions private data aan een exfiltration destination koppelen.
Belangrijkste conclusies
- Content filters en execution policy beschermen verschillende boundaries. Filters inspecteren modelinput en -output. Tool authorization, credential scope, sandboxes en supply-chain controls werken op de paden die in de zeven incidents werden gebruikt.
- De meeste OWASP ASI-categorieën vereisen controls buiten modeloutput. Gebruik de lijst om elke threat te koppelen aan het component dat deze daadwerkelijk kan blokkeren of registreren.
- Permission is infrastructure, geen prompt. Claude Code documenteert de precedence van deny-, ask- en allow-rules, terwijl
PreToolUsevóór execution kan blokkeren. De Claude Agent SDK biedt een afzonderlijkcanUseTool-pad. Andere runtimes hebben een even goed testbaar precedence model nodig. - Behandel een structured deny van een PreToolUse hook als elke andere tool observation. De reasoning loop kan dit al afhandelen. Je hebt geen afzonderlijke security workflow nodig.
- Een approvalpercentage van 93% is een signaal om promptkwaliteit en escalationfrequentie te inspecteren. Volg edits, denials en incidents na approval in plaats van een universeel target te kopiëren.
- Audience-bound tokens en per-session vaults beperken credential replay en exposure. Ze vervangen project trust, hook policy of sandboxing niet.
- Supply-chain checks moeten op automation speed draaien. Pin versions en Actions SHAs, scan in CI en review dependency changes in door agents geschreven pull requests.
- Bouw de policy layer zo dat een nieuwe productlaunch deze niet ongeldig maakt. OpenAI Agents SDK, Codex CLI en Claude Code drukken dezelfde primitives verschillend uit. De primitives (permission ladders, hooks, sandboxes, interrupts, audience-bound tokens) zijn waarop je inzet.
De volgende laag is de runtime
Part 5, Long-Running AI Agent Runtime, laat zien waar de sandbox, secret broker, checkpoint en audit trace tijdens een lange run leven. Part 6 gaat vervolgens de harness in, waar deze permission ladder één stage van meerdere is, en onderzoekt hoe acceptance checks, retries en trace-driven evaluation voorkomen dat de loop te vroeg succes declareert. Daar komt ook een vraag bij die dit artikel niet nodig had: of een call die halverwege een timeout kreeg überhaupt veilig opnieuw kan worden verstuurd.
Referenties
De framings
- Bharani Subramaniam en Martin Fowler, Emerging Patterns in Building GenAI Products.
- Simon Willison, The lethal trifecta for AI agents, 16 juni 2025.
- Joel Fokou, Parallax: Why AI Agents That Think Must Never Act, arXiv 2604.12986, 14 april 2026 (niet peer-reviewed).
- Alessandro Pignati, Your AI Agent Has Too Much Power: Understanding and Taming Excessive Agency, januari 2026.
LLM guardrail-producten
- NVIDIA NeMo Guardrails
- Meta Llama Guard 4
- Guardrails AI
- Lakera Guard
- AWS Bedrock Guardrails
- Azure Content Safety: Prompt Shields
- openai-guardrails-python
Incidents
- Itay Ravia (voorheen Aim Labs, nu Cato Networks), Breaking down EchoLeak (CVE-2025-32711).
- AWS, Amazon Q Developer VS Code v1.84.0 advisory (CVE-2025-8217).
- Microsoft, Azure MCP Server CVE record (CVE-2026-32211; vendor reference: Microsoft).
- Check Point Research, RCE and API token exfiltration through Claude Code project files (CVE-2025-59536).
- axios, v1.14.1 / v0.30.4 compromise post-mortem.
- Aqua Security, Trivy Actions tag hijack (GHSA-69fq-xp46-6x23).
Policy surfaces
- OpenAI Agents SDK — MCP tools docs
- Codex CLI managed configuration
- Claude Code permission modes
- Claude Code sandboxing
- Claude Managed Agents
HITL
- LangGraph interrupts docs
- HumanLayer Python quickstart
- Anthropic, Measuring AI agent autonomy in practice, 18 februari 2026.
- Anthropic, Claude Code auto mode, 25 maart 2026.
- Jackson Wells (Galileo), How to Build Human-in-the-Loop Oversight for Production AI Agents, 21 december 2025.
OWASP
- OWASP Agentic Security Initiative, Top 10 for Agentic Applications, 2026, 9 december 2025.
De policy layer van de Market Analyst Agent bevindt zich in de gecombineerde analysis-to-trade graph van de repo, niet in de analysis graph die in Part 1 wordt genoemd. Deze beheert simulated trade state: een deterministische guardian node weigert restricted actions, keurt low-value actions automatisch goed en escaleert de rest naar een compliance-officer node voordat de graph stopt met interrupt_before. De policy layer staat op GitHub. De deny hook, input canary en Stop-hook validator hierboven zijn sketches van dezelfde control points. Ze zijn geschreven om gelezen te worden, niet om zonder aanpassingen in die repo te worden geplaatst.