MAIA demande une confirmation avant d'utiliser un outil sensible (Bash, Write, Edit, WebFetch, Agent…). Les **permissions** permettent d'autoriser, de demander ou d'interdire à l'avance des usages précis, pour ne plus répondre « o/n » à chaque `ls` (à la question, `s` = « stop » : l'action n'est pas exécutée, le travail s'interrompt sans rien annuler pour vous laisser donner d'autres informations) — et pour bloquer définitivement ce qui ne doit jamais s'exécuter.
Les exemples de ce guide ont été vérifiés avec le moteur de permissions de MAIA (`packages/agent/src/permissions.ts`).
## 1. Où les définir
| Fichier | Portée |
| --- | --- |
| `~/.maia/settings.json` | toutes vos sessions (dossier de l'utilisateur) |
| `./.maia/settings.json` | le projet (dossier de lancement de MAIA) |
Les deux fichiers s'**additionnent** (les listes sont fusionnées). Le fichier du dossier parent n'est pas lu. `~/.maia/settings.json` contient déjà la configuration LiteLLM : ajoutez simplement la propriété `permissions`.
```json
{
"LiteLLM_URL": "http://localhost:4000",
"OpenAI_EndPoint": "/v1/chat/completions",
"LiteLLM_Models": "/v1/models",
"permissions": {
"allow": ["Bash(ls *)", "Read"],
"ask": ["Bash(git push *)"],
"deny": ["Bash(rm *)"]
}
}
```
Les trois listes sont facultatives. La commande `/permissions` affiche les règles chargées, par fichier (badges `ALLOW`, `ASK`, `DENY`). Une règle invalide est ignorée avec un avertissement au démarrage. Les règles sont lues au démarrage, à `/clear` et à `/new` : relancez ou tapez `/clear` après une modification.
## 2. Les trois familles et leur priorité
| Liste | Effet |
| --- | --- |
| `deny` | l'appel est **refusé** sans rien demander ; le modèle est prévenu de ne pas réessayer tel quel |
| `ask` | l'appel demande **toujours** une confirmation |
Ordre d'évaluation : **`deny` > `ask` > `allow`**. La première famille qui correspond décide, quelle que soit la précision des règles : un `allow` très précis ne peut jamais lever un `deny` ou un `ask` plus large. Si **aucune** règle ne s'applique, MAIA demande une confirmation (c'est le comportement par défaut, pour tous les outils).
## 3. Syntaxe d'une règle
```
Outil
Outil(motif)
```
-`Outil` seul (ex. `"Read"`) : **tous** les usages de l'outil.
-`Outil(motif)` : seulement les usages qui correspondent au motif. Le nom de l'outil respecte la casse (`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebFetch`, `Skill`, `Agent`, outils personnalisés…).
- Chaque règle est une chaîne JSON : les guillemets doubles du motif doivent être échappés (`\"`).
Le contenu du motif dépend de l'outil :
| Outil | Le motif est… | Exemples |
| --- | --- | --- |
| `Bash` | une commande (voir § 4) | `Bash(ls *)`, `Bash(npm run test:*)` |
| `Read`, `Write`, `Edit`, `Glob`, `Grep` | un motif de **chemin** relatif au dossier de lancement (glob `*`, `**`) ; un dossier couvre son contenu | `Read(src/**)`, `Edit(docs)`, `Write(*.md)`, `Read(.env)` |
MAIA découpe la commande aux séparateurs `&&`, `||`, `;`, `|` et aux retours à la ligne, **sans tenir compte des guillemets**. Chaque morceau est une « sous-commande ».
- Pour **`allow`** : **tous** les morceaux doivent être couverts par une règle. Un seul morceau non couvert, et MAIA redemande confirmation.
- Pour **`deny` et `ask`** : **un seul** morceau qui correspond suffit. Le découpage est même plus fin : il coupe aussi aux `$(…)`, aux backticks, aux parenthèses et aux redirections, pour retrouver une commande cachée dans `echo $(rm -rf x)`.
- Une commande qui contient une substitution (`$(…)`, backticks, `<(…)`, `>(…)`) n'est **jamais** autorisée automatiquement par un `allow` : MAIA demande toujours.
- Une commande qui contient une **redirection de sortie** (`>`, `>>`, `2>`, `&>`…) n'est autorisée que si le fichier visé l'est aussi par une règle `Redirect` (§ 8). Sans règle `Redirect`, MAIA demande toujours.
Règle pratique : `commande *` autorise la commande avec ou sans arguments ; la commande seule la limite à elle-même. Les espaces comptent. Un `*` au milieu remplace n'importe quelle suite de caractères. `"Bash"` seul autorise **toute** commande : à éviter sauf dans un environnement jetable.
## 5. Exemple simple : autoriser `ls`
Pour ne plus confirmer `ls`, avec ou sans options :
```json
{
"permissions": {
"allow": ["Bash(ls *)"]
}
}
```
Résultats :
| Commande demandée par le modèle | Décision |
| --- | --- |
| `ls` | autorisée |
| `ls -la src` | autorisée |
| `lsof` | confirmation (autre commande) |
| `ls && rm -rf x` | confirmation (le morceau `rm -rf x` n'est pas couvert) |
Un tube entre commandes toutes autorisées passe : `cat a.txt | grep foo` est autorisé avec `Bash(cat *)` et `Bash(grep *)`.
## 6. Exemple complexe : `podman run … sh -c "… | grep …"`
Commande à autoriser :
```
podman run --rm alpine sh -c "echo 'hello world' | grep 'll'"
```
**Piège** : le `|` à l'intérieur des guillemets coupe quand même la commande en deux morceaux :
1.`podman run --rm alpine sh -c "echo 'hello world'`
2.`grep 'll'"`
Une règle qui reproduit la commande entière n'est donc **jamais** reconnue, et une règle `podman … sh -c *` seule ne couvre que le premier morceau : MAIA redemande confirmation. (Les deux cas ont été vérifiés.) Chaque morceau doit avoir sa règle :
```json
{
"permissions": {
"allow": [
"Bash(podman run --rm alpine sh -c *)",
"Bash(grep *)"
]
}
}
```
Avec ces deux règles, la commande ci-dessus est autorisée sans confirmation. Remarques :
-`Bash(podman run --rm alpine sh -c *)` limite l'image (`alpine`) et l'option `--rm` : `podman run --privileged …` ou une autre image redemandent confirmation.
-`Bash(grep *)` autorise `grep` partout, pas seulement dans ce conteneur. C'est le prix du découpage : si c'est trop large, ne l'autorisez pas et acceptez la confirmation pour cette commande.
- Dans un fichier JSON, écrivez la règle telle quelle ; il n'y a pas de guillemets à échapper tant que le motif n'en contient pas. Si le motif en contient : `"Bash(echo \"hello\")"`.
Les morceaux non couverts restent bloqués : avec les mêmes règles, `podman run --rm alpine sh -c "echo x | rm -rf /tmp/y"` demande confirmation (le morceau `rm -rf /tmp/y"` n'est pas autorisé).
## 7. Interdire une commande, même cachée dans un tube
Pour interdire `rm` quoi qu'il arrive :
```json
{
"permissions": {
"deny": ["Bash(rm *)", "Bash(* rm *)"]
}
}
```
Les deux règles sont complémentaires :
-`Bash(rm *)` : un morceau qui **commence** par `rm`.
-`Bash(* rm *)` : `rm` apparaît comme un mot **après** autre chose (`xargs rm`, `sudo rm`, `time rm`…).
Vérifications (avec ces deux règles) :
| Commande | Décision |
| --- | --- |
| `rm -rf build` | refusée |
| `ls && rm -rf build` | refusée |
| `echo ok > f; rm f` | refusée |
| `cat f \| (rm -f g)` | refusée |
| `echo $(rm -rf x)` | refusée |
| `ls \| xargs rm` | refusée (grâce à `* rm *`) |
| `podman run --rm alpine sh -c "echo x \| rm -rf /tmp/y"` | refusée : le découpage de `deny` entre aussi dans les guillemets |
Avec `Bash(rm *)`**seul**, `ls | xargs rm` n'est **pas** refusé (le morceau commence par `xargs`) : ajoutez `Bash(* rm *)`.
Un `deny` l'emporte toujours sur un `allow` : même avec `Bash(podman run --rm alpine sh -c *)` et `Bash(grep *)` autorisés, la commande contenant `| rm -rf …` est refusée si `Bash(rm *)` est dans `deny`.
Une redirection de sortie écrit dans un fichier **sans passer par l'outil `Write`**. Elle est donc contrôlée à part, avec des règles **`Redirect(chemin)`** (ou `Redirect` seul pour « toutes les redirections »). Le chemin suit la même syntaxe que `Read`/`Write` : relatif au dossier de lancement, `*` et `**` acceptés, un dossier couvre son contenu.
### 8.1 Comportement par défaut : on demande
Tant qu'aucune règle `Redirect` ne les autorise, les redirections vers un fichier **repassent en confirmation**, même si la commande elle-même est autorisée :
| `ls 2>&1` · `ls >&2` | autorisées : ce sont des **duplications de flux**, elles n'écrivent dans aucun fichier |
| `echo "a > b"` (avec `Bash(echo *)`) | autorisée : un `>` entre guillemets n'est pas une redirection |
C'est la configuration la plus sûre : rien à faire pour **ne pas** autoriser les redirections.
### 8.2 Autoriser certaines redirections
Ajoutez des règles `Redirect(...)` dans `allow`. Le fichier visé doit être couvert **et** la commande doit être autorisée par ailleurs (`Redirect` seul n'autorise aucune commande) :
| `echo x >> ~/.bashrc` | refusée (le `~` est comparé tel qu'il est écrit : le motif `~/**` le reconnaît) |
| `echo y 2> .env` (placé dans une suite `a; b`) | refusée |
| `echo x > docs/a.md` | confirmation |
| `echo x > notes.txt` | autorisée |
### 8.4 À savoir
-`Redirect` ne concerne que les redirections de **sortie**. Une redirection d'entrée (`<fichier`)litunfichier:ellen'estpascontrôlée(utilisez`Bash(cat .env)`ou`Read(.env)`en`deny`pourprotégerunelecture).
- Une redirection sans commande (`> fichier`, qui vide un fichier), un guillemet non fermé ou une cible absente : confirmation.
- Les substitutions (`> $(…)`, `>(…)`) sont toujours en confirmation.
- D'autres commandes écrivent sans `>` : `tee`, `cp`, `mv`, `dd of=…`, `sed -i`. Autorisez-les (ou interdisez-les) explicitement avec des règles `Bash(...)`, par exemple `deny: ["Bash(tee *)", "Bash(* tee *)"]`.
- Les règles `Redirect` vont dans les mêmes fichiers `settings.json` que les autres et s'additionnent comme elles. Un sous-agent peut en ajouter dans la propriété `permissions` de son `AGENT.md`.
Les règles analysent le **texte** de la commande. Elles ne peuvent pas tout voir :
-`find . -delete`, `git clean -fd`, `truncate`, `mv x /dev/null`, `python -c "import os; os.remove('f')"` ou `perl -e …` suppriment des fichiers **sans** écrire `rm`. Ajoutez les règles correspondantes si elles vous importent (`Bash(find * -delete*)`, `Bash(git clean *)`), ou n'autorisez que les commandes dont vous avez besoin (liste `allow` courte) et laissez le reste en confirmation.
- Une commande lancée depuis un script (`bash script.sh`) n'est pas analysée : le contenu du script est invisible.
- Les `deny` sur les outils de fichiers (`Read(.env)`, `Write(.git/**)`) ne concernent que ces outils : `Bash(cat .env)` ou une redirection `> .env` sont d'autres règles (`Bash(...)`, `Redirect(...)`), à ajouter si besoin.
Approche conseillée : peu de `allow` (lecture seule, tests, build), quelques `ask` (publication, écriture hors projet), des `deny` pour ce qui ne doit jamais arriver (`rm`, `sudo`, `curl … | sh`), et **laisser le défaut** (confirmation) pour tout le reste.
Un sous-agent hérite des permissions de l'agent principal. Sa définition (`AGENT.md`) peut en **ajouter** :
```yaml
permissions:
allow: [Read, Grep]
deny: [Bash]
```
Les règles s'additionnent : un `deny` ou un `ask` hérité n'est jamais levé par un sous-agent. Quand un sous-agent a besoin d'une confirmation, la question s'affiche dans l'interface principale, préfixée `[sous-agent nom]`.
Les serveurs MCP sont déclarés une fois pour toutes dans `~/.maia/mcp.json` (voir le README) ; **aucun n'est actif tant qu'une règle `allow` ne l'autorise**. Syntaxe : `Mcp(clé du serveur)` ou `Mcp(clé du serveur,outil)`.
| Règle | Effet |
| --- | --- |
| `allow: ["Mcp(github)"]` | active `github` ; tous ses outils sont autorisés sans confirmation |
| `deny: ["Mcp(github,delete_repository)"]` | l'outil n'est jamais proposé au modèle (`*` accepté : `Mcp(github,delete_*)`) |
| `ask: ["Mcp(github,merge_pull_request)"]` | cet outil demande confirmation malgré le `allow` du serveur |
| `allow: ["Mcp(github)"]` + `deny: ["Mcp(github,*)"]` | serveur **désactivé** : MAIA ne tente pas de s'y connecter |
| `allow: ["Mcp(*)"]` (= `Mcp(*,*)`) | active **tous** les serveurs déclarés dans `mcp.json`, tous leurs outils autorisés |
| `deny: ["Mcp(*,delete_repository)"]` | retire cet outil (motif accepté) sur tous les serveurs ; en `ask` : confirmation partout |
| `allow: ["Mcp(github,list_issues)"]` | active `github` ; seul cet outil est autorisé, les autres demandent confirmation |
Ces règles sont lues **avant** toute connexion ; deny > ask > allow, comme pour les autres outils. Mettre `allow` dans `~/.maia/settings.json` active le serveur pour tous les projets.
Sans interface, personne ne peut répondre « o/n » sur le moment. Les règles restent les mêmes (`allow` passe, `deny` refuse) ; seule une demande `ask` (ou un outil sans règle) est traitée selon `permissionMode` (en-tête de l'agent parent ou `--permission-mode`) :
| Mode | Demande `ask` |
| --- | --- |
| `wait` (défaut) | enregistrée ; l'agent passe « en attente » jusqu'à `maia approve <id> <demande>` (ou `--deny`) — rien n'est perdu |
| `deny` | refusée aussitôt ; le modèle en est informé |
Pour un workflow qui tourne la nuit sans surveillance, préférez des règles `allow` précises dans le `settings.json` du projet (ce que le workflow doit pouvoir faire) et laissez `wait` pour le reste : seules les actions imprévues attendront une réponse. `maia workflow <id>` liste les demandes en attente et la commande pour y répondre.