Blog • AI / Agent
Comment mettre en place les permissions d'un agent Claude ? De la Permission Policy et des appels MCP au flux d'approbation JSON
Le contrôle des permissions d'un agent Claude est une décision JSON placée entre l'appel d'outil proposé par le modèle et le code qui s'exécute vraiment. La policy produit allow, deny ou ask. Ask devient un enregistrement d'approbation. La décision est renvoyée sur le même tool_use_id. Si l'identifiant manque ou est faux, la session reste bloquée ou le même appel s'exécute deux fois.
La décision automatique de la plateforme est dans l'article précédent. Là, auto signifie que le serveur lit permission_policy et écrit evaluated_permission. Vous ne changez pas cette évaluation. Cet article porte sur les trois documents JSON que vous branchez : la policy, la file d'approbation et le renvoi du refus. Les outils personnalisés, et les appels visibles seulement dans le callback, passent par ici.
Cet article couvre :
- Où s'arrête l'évaluation officielle, et quand
canUseTools'exécute vraiment - Champs, valeurs par défaut et ordre de correspondance d'une Permission Policy JSON
- Comment un appel d'outil MCP passe le schéma avant de correspondre à une règle
- Quels champs stocke un enregistrement d'approbation, et ce que fait l'expiration
- Comment allow et deny sont renvoyés pour qu'un appel ne s'exécute pas deux fois
À retenir d'abord : dès qu'un outil est une règle allow, la file ne le voit plus. canUseTool ne se déclenche pas pour un appel déjà approuvé. Les sections suivantes séparent le point de branchement, le JSON de policy, la file et le renvoi.
Où vous vous branchez sur l'évaluation officielle
L'Agent SDK et Claude Code évaluent les règles dans un ordre fixe. Voir les permissions de l'Agent SDK et les permissions de Claude Code. Deny est examiné d'abord, puis ask, puis allow. Un allow plus précis n'annule pas un deny déjà correspondant.
-
1
deny arrête l'appel
Un deny correspondant bloque l'appel, y compris en mode
bypassPermissions. Un nom d'outil seul, comme Bash, est retiré du contexte avant cette étape. -
2
ask atteint le callback
Un ask correspondant est transmis à
canUseTool, même si un mode de contournement est actif. DansdontAsk, il n'y a pas d'invite : l'appel correspondant est refusé et le callback n'est pas invoqué. -
3
allow saute la demande
Un allow correspondant exécute l'outil, et
canUseTooln'est pas appelé. Lister seulement le nom de l'outil dans la liste allow fait la même chose, sauf si une règle ask ramène cet appel vers le callback. -
4
Aucune correspondance
Si rien n'a correspondu plus tôt, seul le mode
defaultenvoie l'appel àcanUseTool.dontAskle refuse et n'appelle pas le callback.
La file ne reçoit donc que les appels qui n'ont pas déjà été autorisés. Pour enregistrer chaque appel, utilisez un hook PreToolUse. Il s'exécute avant le reste des contrôles et peut autoriser, refuser ou modifier les arguments, mais il n'annule pas un deny ou un ask déjà correspondant.
Les trois points de branchement partagent un même enregistrement de file. Seule la forme officielle du renvoi change.
| Branchement | JSON que vous stockez | Forme renvoyée au modèle |
|---|---|---|
| Outil hébergé Managed Agents, en pause sur ask | permission_policy |
user.tool_confirmation |
| Callback de l'Agent SDK | Votre policy, plus un enregistrement de file | { behavior, updatedInput / message } |
| Outil personnalisé | Votre policy, plus un enregistrement de file | user.custom_tool_result |
Un outil personnalisé arrive sous agent.custom_tool_use et n'a pas de evaluated_permission. Documentation : politiques de permission Managed Agents. Quand auto refuse un outil hébergé, le client ne peut pas annuler cela par un événement de confirmation. Ne faites pas de la file un second canal qui force allow.
Comment écrire la Permission Policy JSON
Si vous n'exécutez que Claude Code, les chaînes allow / ask / deny du settings.json officiel suffisent. Un agent dans un produit a aussi besoin d'un document que vous pouvez versionner, comparer et vérifier avec un schéma. Le document ci-dessous est votre contrat d'application, pas un corps de requête Anthropic.
{
"version": 1,
"default": "ask",
"rules": [
{
"id": "deny-env",
"effect": "deny",
"tool": "Read",
"input": { "file_path": { "const": ".env" } },
"reason_code": "secret_file"
},
{
"id": "ask-pr-main",
"effect": "ask",
"tool": "mcp__github__create_pull_request",
"input": { "base": { "enum": ["main", "master"] } },
"reason_code": "protected_branch"
},
{
"id": "allow-docs-search",
"effect": "allow",
"tool": "mcp__docs__search"
}
]
}
| Champ | Sens |
|---|---|
version |
Version du format de policy. Copiez-la sur l'enregistrement de file pour ne pas lire un ancien ticket avec de nouvelles règles. |
default |
Effet quand aucune règle ne correspond. Dans un produit, utilisez ask ou deny, pas allow. |
effect |
deny, ask ou allow |
tool |
Nom complet de l'outil. Un outil MCP commence par mcp__, puis le nom du serveur, puis le nom de l'outil. |
input |
Facultatif. S'il est présent, les arguments doivent aussi correspondre. Omettez-le pour ne regarder que le nom de l'outil. |
reason_code |
Code machine stable. Placez-le sur le renvoi et la ligne d'audit. N'utilisez pas une phrase comme clé primaire. |
L'ordre suit l'officiel : tous les deny, puis ask, puis allow. Dans un même effect, la règle la plus tôt dans le fichier l'emporte.
Quand default vaut ask, un nouvel outil MCP qu'aucune règle ne nomme va dans la file au lieu de s'exécuter. C'est la même idée qu'un toolset MCP hébergé dont la valeur par défaut est always_ask, sauf que le document et la file sont les vôtres.
Comment un appel MCP correspond à une règle
Un appel du modèle porte au moins un type, un id, un nom d'outil et input. Les noms de champs suivent le bloc tool_use de l'API Messages. Un nom d'outil MCP joint le nom du serveur et le nom de l'outil.
{
"type": "tool_use",
"id": "toolu_01AbC",
"name": "mcp__github__create_pull_request",
"input": {
"base": "main",
"title": "Retry failed checks",
"head": "fix/retry"
}
}
-
1
Valider d'abord la forme
Validez input avec le JSON Schema propre à cet outil. En cas d'échec : ne pas enfiler, ne pas appeler MCP, deny immédiat, reason_code
schema_invalid. -
2
Puis faire correspondre deny, ask, allow
L'appel ci-dessus touche
ask-pr-mainparce quebaseest dans l'enum. L'ordre du fichier ne compte qu'à l'intérieur d'un même effect. -
3
Ne pas autoriser sur le seul nom d'outil
Une règle allow sans contrainte d'input laisse aussi passer base égal à main. Les contraintes d'arguments font partie de la permission, ce n'est pas une copie du schéma.
function decide(policy, call) {
const order = ["deny", "ask", "allow"];
for (const effect of order) {
const hit = policy.rules.find(
(rule) => rule.effect === effect && matchRule(rule, call)
);
if (hit) {
return {
effect,
rule_id: hit.id,
reason_code: hit.reason_code || null
};
}
}
return { effect: policy.default, rule_id: null, reason_code: "default" };
}
function matchRule(rule, call) {
if (rule.tool !== call.name) return false;
if (!rule.input) return true;
return Object.entries(rule.input).every(([key, cond]) => {
const value = call.input?.[key];
if (Object.prototype.hasOwnProperty.call(cond, "const")) return value === cond.const;
if (Array.isArray(cond.enum)) return cond.enum.includes(value);
return false;
});
}
Les préfixes de chemin demandent un contrôle séparé qui compare des chemins normalisés. Si la fonction ci-dessus voit une condition qu'elle ne comprend pas, elle renvoie false : la règle manque, elle n'autorise pas.
| JSON Schema | Permission Policy | |
|---|---|---|
| Question | La forme des arguments est-elle valide ? | Cet appel peut-il s'exécuter ? |
| En cas d'échec | schema_invalid, et ne pas enfiler |
deny / ask |
| Exemple | base doit être une chaîne |
base vaut main |
Ce que stocke la file d'approbation
Quand effect vaut ask, enregistrez avant d'attendre. Ne gardez pas l'attente seulement en mémoire. Si le processus redémarre, l'appel du modèle n'a pas de résultat.
{
"id": "apr_01HZXK",
"session_id": "sess_01",
"tool_use_id": "toolu_01AbC",
"tool_name": "mcp__github__create_pull_request",
"input": {
"base": "main",
"title": "Retry failed checks",
"head": "fix/retry"
},
"matched_rule": "ask-pr-main",
"policy_version": 1,
"status": "pending",
"created_at": "2026-09-23T09:00:00Z",
"expires_at": "2026-09-23T09:15:00Z"
}
tool_use_idest unique. Si le même id revient, renvoyez l'enregistrement existant et n'appelez pas MCP une seconde fois.- status ne peut passer que de
pendingàapproved,deniedouexpired. Un état terminal ne change pas. - Après
expires_at, marquez-leexpiredet renvoyezdenyavec le reason_codeapproval_expired. Ne pas autoriser par défaut. - Conservez input tel qu'il a été soumis pour qu'une personne puisse le lire. Ne chargez pas la policy depuis input. Le modèle ne peut pas changer les règles par les arguments.
Stockez la décision humaine dans son propre document. Ne l'écrasez pas sur l'enregistrement de file.
{
"approval_id": "apr_01HZXK",
"tool_use_id": "toolu_01AbC",
"decision": "deny",
"actor": "user:alex",
"reason_code": "protected_branch",
"decided_at": "2026-09-23T09:04:12Z"
}
actor vient de votre session de connexion. Ne faites pas confiance à un champ utilisateur dans input.
Comment deny et allow sont renvoyés
Dès que la décision est terminale, renvoyez-la sur le même tool_use_id. Si vous l'omettez, la session reste en attente. Le modèle ne peut pas continuer, et le tour suivant peut reproposer l'appel sous un nouvel id.
| Votre décision | Callback de l'Agent SDK | Outil hébergé en pause sur ask | Outil personnalisé |
|---|---|---|---|
allow |
{ behavior: "allow", updatedInput } |
result: "allow" |
Après une exécution réussie, renvoyer un résultat d'outil personnalisé |
deny |
{ behavior: "deny", message } |
result: "deny" + deny_message |
Un résultat d'outil personnalisé marqué en erreur, avec reason_code |
Voir la documentation officielle sur la saisie utilisateur. Un allow en TypeScript inclut updatedInput, et en Python updated_input. Avant Claude Code v2.1.207, omettre ce champ échouait à la validation et refusait l'appel. Renvoyez toujours l'input d'origine.
{
"behavior": "deny",
"message": "Denied by rule ask-pr-main (protected_branch). Do not retry this call."
}
message et deny_message sont montrés au modèle. Incluez l'id de règle et le reason_code. N'incluez pas de noms d'hôtes internes, de jetons ni de notes privées du réviseur.
{
"tool_use_id": "toolu_01AbC",
"effect": "deny",
"rule_id": "ask-pr-main",
"reason_code": "protected_branch",
"wrote_back": true
}
N'appelez MCP qu'après allow. deny et expired ne l'appellent pas. Le moteur de policy n'exécute pas l'outil d'abord pour demander à une personne ensuite.
Différence avec settings et auto
Les trois jeux de règles ne répondent pas à la même question. Ne tenez pas trois listes en espérant qu'elles restent alignées.
| Réglages Claude Code | Managed Agents | Votre policy et votre file | |
|---|---|---|---|
| Forme de la règle | Une chaîne outil plus spécificateur | always_allow / always_ask / auto |
effect, plus le nom d'outil et une contrainte d'input facultative |
| Qui évalue | Claude Code, dans l'ordre deny, ask, allow | La plateforme. auto évalue cet appel précis | Votre code, dans le même ordre que le flux officiel |
| Où une personne approuve | Une invite interactive, ou canUseTool | user.tool_confirmation |
Votre file, puis un mapping vers les deux renvois de gauche |
| Convient à | Le codage local | Un agent hébergé | Un produit qui doit garder des tickets et un audit |
{
"permissions": {
"allow": ["Bash(npm test *)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}
Les outils qu'une personne doit voir restent à l'effect ask. Quand auto juge un appel sûr, il s'exécute avant que quiconque le voie. Les détails sont dans l'article précédent. Dans un produit, votre policy est la source. Exporter une chaîne deny Claude Code n'est qu'une projection. L'état d'approbation reste dans la file.
Vérifier les trois documents JSON dans JSONNote
Avant de livrer, faites passer la policy, l'enregistrement de file et le renvoi dans l'espace de travail. Les trois peuvent être formatés, validés et comparés dans le navigateur, sans agent en production.
-
1
Formater la policy
Ouvrez la policy dans Formater le JSON. Les settings de Claude Code sont aussi du JSON strict. Un commentaire ou une virgule finale est signalé comme erreur au prochain démarrage.
-
2
Valider l'enregistrement de file
Collez le schéma ci-dessous dans l'outil JSON Schema, puis un enregistrement d'approbation. Un tool_use_id manquant, ou un status hors de l'enum, échoue.
-
3
Comparer les règles modifiées
Avant et après un changement de règle, utilisez JSON Diff pour voir si
askest devenuallow. Ce changement compte plus que le texte. -
4
Valider le renvoi
Utilisez Valider le JSON pour confirmer que le renvoi est encore du JSON légal et que
tool_use_idest toujours présent.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"session_id",
"tool_use_id",
"tool_name",
"input",
"status",
"created_at",
"expires_at"
],
"properties": {
"id": { "type": "string", "minLength": 1 },
"session_id": { "type": "string", "minLength": 1 },
"tool_use_id": { "type": "string", "minLength": 1 },
"tool_name": { "type": "string", "minLength": 1 },
"input": { "type": "object" },
"matched_rule": { "type": ["string", "null"] },
"policy_version": { "type": "integer", "minimum": 1 },
"status": { "enum": ["pending", "approved", "denied", "expired"] },
"created_at": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" }
}
}
Questions fréquentes
Pourquoi canUseTool n'est-il pas appelé ?
Une règle allow antérieure, ou un mode comme acceptEdits ou bypassPermissions, a déjà approuvé l'appel. Le callback ne voit que les appels pas encore approuvés. Pour inspecter chaque appel, utilisez PreToolUse. Il ne peut toujours pas annuler un deny.
Valider le JSON Schema suffit-il ?
Non. Le schéma ne prouve que la forme, par exemple que base est une chaîne. Savoir si base égal à main exige une personne relève de la policy. Une forme valide peut rester un deny ou un ask.
Après un refus, le modèle doit-il réessayer les mêmes arguments ?
Non. Le retour doit dire de ne pas réessayer les mêmes arguments, et il ne faut pas effacer le résultat pour faire comme si l'appel n'avait pas eu lieu. Une nouvelle tentative exige un nouveau tool_use_id et un nouveau passage dans la file.
Une entrée expirée est-elle autorisée par défaut ?
Non. Marquez l'enregistrement expired et renvoyez un deny avec le reason_code approval_expired. L'expiration n'est pas une approbation.
Faut-il encore cette file si Managed Agents utilise auto ?
Oui, mais seulement pour always_ask et les outils personnalisés. Un appel que auto a marqué deny ne peut pas être annulé par un événement de confirmation. Ne faites pas de la file un contournement qui force allow.
Conclusion
Mettre en place le contrôle des permissions, c'est trois documents JSON : la policy que vous écrivez, l'enregistrement de file créé pour ask, et la décision renvoyée quand le statut est terminal. L'ordre suit l'officiel, deny d'abord. Le schéma ne vérifie que la forme. Si une personne n'a pas décidé, ou si le ticket a expiré, n'appelez pas MCP.
Conclusion directe : la valeur par défaut est ask ou deny. Un allow doit nommer l'outil et, quand cela compte, les arguments. Chaque tool_use_id s'exécute au plus une fois, et reçoit toujours un renvoi.
Qui déclare un outil MCP et qui l'exécute est traité dans la sécurité MCP. Pourquoi le JSON se place devant l'exécution est traité dans la frontière de sécurité.
Ensuite : validez la forme de l'enregistrement d'approbation, puis remettez la policy à l'agent.