Anthropic heeft gemeten wat er gebeurt als Claude Code om toestemming vraagt: gebruikers keuren 93% van de prompts goed. Bij dat percentage is de prompt behang. Je leest hem niet meer, je vinger kent de sneltoets, en de ene prompt die ertoe doet, krijgt dezelfde reflex-ja als de veertig die er niet toe doen.
De populaire oplossing maakt het erger. --dangerously-skip-permissions is dezelfde beslissing, maar dan zonder de knop: je zei al overal ja op, nu geef je het gewoon toe. De incidenten laten zich raden: gewiste home-directories, verdwenen databases, een gestage stroom mensen die er door schade en schande achter komen dat een agent met jouw shell precies zo voorzichtig is als zijn slechtste sample.
De echte oplossing is snappen dat het permissiesysteem van Claude Code uit vier lagen bestaat, en dat elke laag een andere vraag beantwoordt. Zodra je dat ziet, kost de configuratie twintig minuten en zijn de prompts die overblijven de prompts die het lezen waard zijn.
De vier lagen
| Laag | De vraag die hij beantwoordt |
|---|---|
| Mode | Hoe vaak vraagt hij om toestemming? |
| Regels | Wat mag hij aanroepen? |
| Hooks | Wat wordt er deterministisch afgedwongen? |
| Sandbox | Waar kan het proces fysiek bij? |
Modes en regels sturen het gedrag van het model. Hooks en de sandbox dwingen af wat jij wilt. De meeste gidsen stoppen na de eerste twee lagen, en zo zitten mensen met een allowlist waarvan ze denken dat het een beveiligingsgrens is. Het is een slot op een deur die de agent keurig gebruikt.
Laag 1: de mode bepaalt hoe vaak hij vraagt
De mode is een schakelaar per sessie, in te stellen met Shift+Tab, --permission-mode, of defaultMode in je settings:
- default: lezen mag altijd, al het andere geeft een prompt. Hier begin je.
- plan: alleen lezen en verkennen. De agent stelt voor, jij beslist.
- acceptEdits: bestandswijzigingen gaan automatisch, shell-commando's blijven vragen. Mijn standaardmodus voor repo's met nette git-hygiëne, want
git checkout .draait een edit terug en niets draait een shell-commando terug. - auto: een veiligheidsclassifier keurt alles goed wat hij onschuldig vindt. Anthropic rapporteert 0,4% vals-positief en 17% vals-negatief. Dat één op de zes riskante aanroepen erdoorheen glipt is nog altijd een betere deal dan de 93% reflex-ja, maar weet wat je koopt.
- bypassPermissions: alles mag. Prima in een wegwerpcontainer, roekeloos op de machine met je SSH-sleutels.
Een mode die je als vlag meegeeft, blijft niet per project hangen. Mensen draaien één keer --dangerously-skip-permissions, hervatten de sessie een dag later, en snappen niet waarom de prompts terug zijn. Wil je hem permanent, zet dan defaultMode in je settings.
Laag 2: regels bepalen wat hij mag aanroepen
Het permissions-blok in settings.json kent drie lijsten, en de volgorde ligt vast: eerst deny, dan ask, dan allow. De eerste match wint, en hoe specifiek een regel is doet er niet toe:
{
"permissions": {
"allow": ["Bash(aws s3 ls)"],
"deny": ["Bash(aws *)"]
}
}Die allow-regel is dode code. De deny matcht eerst, dus elk aws-commando wordt geblokkeerd, ook het commando dat je net had toegestaan. Deny wint van ask, ask wint van allow, altijd.
De syntax heeft tanden, en je wilt weten waar hij bijt:
- De spatie in de wildcard telt.
Bash(ls *)matchtls -laen dwingt een woordgrens af.Bash(ls*)matcht ooklsof. Eén teken verschil, een compleet andere blast radius. - Samengestelde commando's matchen per onderdeel.
Bash(git *)keurtgit add . && rm -rf /tmp/xniet goed. Elk subcommando achter&&,;of een pipe heeft zijn eigen matchende regel nodig. Daarom blijft je allowlist prompts geven: het model rijgt dolgraag commando's aan elkaar. - Pad-ankers luisteren nauw. In Read- en Edit-regels is
//etc/passwdeen absoluut pad, ankert/src/**aan het project van het settings-bestand, en is./de werkmap. Eén slash vooraan betekent iets anders dan je shell-reflex zegt. - Een deny op
Readgeldt alleen voor de Read-tool."deny": ["Read(.env)"]houdt tegen dat de agent het bestand rechtstreeks opent. Het doet niets tegen een script dat de agent draait en dat zelf.envuitleest. Regels begrenzen tool-aanroepen, nooit de code die door zo'n aanroep wordt uitgevoerd.
Dat laatste punt is precies waarom de volgende twee lagen bestaan.
Laag 3: hooks zijn de deterministische laag
Een PreToolUse-hook is een shell-commando dat elke tool-aanroep ziet vóór hij draait en hem onvoorwaardelijk kan tegenhouden, zonder dat er een model aan te pas komt. Exitcode 2 blokkeert de aanroep. Met een JSON-antwoord dwing je allow, ask of deny af:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE_PATH" == *".env"* ]]; then
echo "Blocked: secrets stay out of context" >&2
exit 2
fi
exit 0Regels zijn patronen die het permissiesysteem naast de commandostring legt. Een hook is jouw code: die krijgt de volledige tool-input in handen en past elke logica toe die jij wilt. Waar de allowlist-regex je in de steek laat (en met de valkuil van samengestelde commando's hierboven gebeurt dat gegarandeerd een keer), vuurt de hook gewoon. Ik schreef eerder een volledige gids over Claude Code hooks voor de hele levenscyclus. Voor permissions is PreToolUse de hook die telt. Twijfel je of een regel, een hook of iets anders bij je situatie past, dan is er ook een keuzegids voor skills, subagents, hooks en slash commands.
Laag 4: de sandbox is de muur
Alles hierboven leeft in het proces van Claude Code en onderhandelt met het model. De sandbox zit op OS-niveau: bestandssysteem- en netwerkisolatie om de shell-commando's zelf heen. Anthropics eigen beeld klopt hier gewoon: permissions zijn het slot op de deur, de sandbox is de muur.
{
"sandbox": {
"enabled": true,
"network": { "allowedDomains": ["registry.npmjs.org", "api.github.com"] },
"filesystem": { "denyRead": ["~/.ssh", "~/.aws"] }
}
}Met de sandbox aan strandt een script dat ~/.ssh probeert te lezen op OS-niveau, wat het model ook van plan was, wat de regels ook zeiden. Het standaardgedrag is een prettige ruil: gesandboxte commando's draaien zonder prompt, en alles wat de sandbox uit wil, valt terug op de gewone permission-flow. Minder prompts en een hardere grens tegelijk.
De settings.json die ik draai
Op projectniveau, ingecheckt in de repo, per project bijgesteld. Met toelichting:
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run test:*)",
"Bash(npm run build)",
"Bash(npx vitest run:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"ask": ["Bash(git push:*)"],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(~/.ssh/**)",
"Bash(curl:*)",
"Bash(wget:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./.claude/hooks/protect-files.sh" }]
}
]
}
}De logica: een edit terugdraaien kost niets, dus acceptEdits. De test- en buildloop mag vrijuit draaien, want een prompt op elke npm run test is precies waarmee je die 93%-reflex kweekt. git push vraagt altijd, want dat is het moment waarop werk de machine verlaat. Secrets worden op de regellaag geweigerd en door de hook nog eens tegengehouden, en curl/wget staan op deny omdat de agent daarvoor WebFetch met een domein-allowlist heeft. Dat twee lagen hetzelfde zeggen is precies de bedoeling: de regel stuurt wat het model probeert, de hook vangt wat erdoorheen glipt.
Hetzelfde principe trek je door naar je data. De agent databasetoegang geven met SELECT-only credentials en field redaction is exact deze stack, één laag dieper. Hoe dat eruitziet staat uitgewerkt in Claude veilig toegang geven tot je SQL-database.
Twintig minuten configureren, en het aantal prompts zakt van veertig per sessie naar een handvol. Lees die. Dat zijn de prompts die je aandacht verdienen.