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 :

À 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. 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. 2
    ask atteint le callback

    Un ask correspondant est transmis à canUseTool, même si un mode de contournement est actif. Dans dontAsk, il n'y a pas d'invite : l'appel correspondant est refusé et le callback n'est pas invoqué.

  3. 3
    allow saute la demande

    Un allow correspondant exécute l'outil, et canUseTool n'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. 4
    Aucune correspondance

    Si rien n'a correspondu plus tôt, seul le mode default envoie l'appel à canUseTool. dontAsk le 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.

Policy d'application : default vaut ask, et deny l'emporte sur un allow plus loin
{
  "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.

Un appel qui ouvre une pull request contre une branche protégée
{
  "type": "tool_use",
  "id": "toolu_01AbC",
  "name": "mcp__github__create_pull_request",
  "input": {
    "base": "main",
    "title": "Retry failed checks",
    "head": "fix/retry"
  }
}
  1. 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. 2
    Puis faire correspondre deny, ask, allow

    L'appel ci-dessus touche ask-pr-main parce que base est dans l'enum. L'ordre du fichier ne compte qu'à l'intérieur d'un même effect.

  3. 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.

Un appariement limité à l'égalité et à l'enum. Ne concaténez pas les chaînes de règles dans un shell.
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.

Une approbation en attente. tool_use_id est unique.
{
  "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"
}

Stockez la décision humaine dans son propre document. Ne l'écrasez pas sur l'enregistrement de file.

La décision d'approbation. actor vient de la session de connexion.
{
  "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.

Un deny renvoyé au modèle. Incluez l'id de règle et le reason_code, et dites de ne pas réessayer les mêmes arguments.
{
  "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.

Stockez la ligne d'audit à part du renvoi. Ne mettez wrote_back à true qu'après la réussite du renvoi.
{
  "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
Règles de permission Claude Code. C'est la forme officielle des settings, pas la policy d'application ci-dessus.
{
  "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. 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. 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. 3
    Comparer les règles modifiées

    Avant et après un changement de règle, utilisez JSON Diff pour voir si ask est devenu allow. Ce changement compte plus que le texte.

  4. 4
    Valider le renvoi

    Utilisez Valider le JSON pour confirmer que le renvoi est encore du JSON légal et que tool_use_id est toujours présent.

JSON Schema d'un enregistrement d'approbation. À coller tel quel dans l'outil Schema.
{
  "$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.

Retour au blog