Claude Code headless : refuser les permissions automatiquement et nettoyer ses Skills

Passer Claude Code du terminal à un pipeline CI casse au premier prompt de permission. Voici comment cadrer les autorisations, nettoyer les Skills accumulés avec skill-doctor, et un workflow GitHub Actions copiable pour reviewer tes PR.

Tu as installé Claude Code, tu l'utilises tous les jours en interactif, et maintenant tu veux le brancher sur ton CI ou une tâche cron. Le premier essai finit toujours pareil : le process attend une confirmation qui ne viendra jamais, le runner timeout au bout de 6 heures, et tu paies pour rien. Cet article couvre les trois choses qui manquent quand on passe du terminal à l'automatisation : gérer les permissions sans humain devant l'écran, standardiser la config par projet, et garder un environnement de Skills propre. Si tu débutes sur l'outil, commence plutôt par la documentation d'apprentissage de Claude ou par la page installation et configuration de Claude Code, cet article suppose que tu as déjà tapé quelques commandes à la main.

Ce que veut dire headless dans Claude Code

Le mode headless s'active avec le flag -p (alias --print). Claude lit son prompt, exécute ce qu'il a à exécuter, écrit sa réponse sur stdout, et quitte. Pas de REPL, pas de TTY, pas de prompt clignotant. C'est ce que tu veux dans un job GitHub Actions, un hook pre-commit, un container Docker qui tourne à 3h du matin, ou un script bash lancé par cron.

claude -p "résume les changements de ce diff en 5 puces" < diff.txt

Cette commande retourne du texte sur stdout et s'arrête. Tu peux la piper, la rediriger, la capturer dans une variable shell. Tout le reste marche comme d'habitude : les fichiers .claude/settings.json, les Skills installés, les serveurs MCP configurés. Ce qui change, c'est le comportement quand Claude veut faire quelque chose qui demande une confirmation.

Le problème des permission prompts en automatisation

En interactif, Claude te demande avant d'écrire un fichier, de lancer une commande bash, d'appeler un outil MCP qui contacte un serveur externe. Tu vois la question, tu tapes y ou n, la session continue. En headless, personne ne tape rien. Le process reste bloqué à attendre stdin, et ton runner CI finit par le tuer.

Le symptôme typique : un job qui devait durer 90 secondes tourne pendant l'intégralité du timeout du workflow. Les logs montrent une dernière ligne du genre Do you want to allow Write to src/api/handler.ts? (y/n), puis plus rien. Ce n'est pas un bug, c'est le comportement par défaut : Claude préfère hang plutôt que d'agir sans validation.

Quatre modes de permission existent pour piloter ce comportement. default demande à chaque action sensible. acceptEdits auto-approuve les éditions de fichiers (Write, Edit) mais garde la confirmation sur Bash et les outils réseau. plan interdit toute écriture : Claude peut lire, analyser, réfléchir, mais pas modifier l'état du système. bypassPermissions désactive tous les prompts, y compris les commandes bash destructrices. C'est le mode qui te sauve la vie en CI et qui casse ta machine si tu l'utilises hors sandbox.

Refuser ou accepter automatiquement : les flags qui comptent

Le choix du mode dépend de ce que tu veux que Claude fasse dans le job. Pour une review de PR qui produit des commentaires mais ne touche pas au code :

claude -p "review ce diff et liste les 3 risques principaux" \
  --permission-mode plan \
  < pr.diff

Aucun risque d'écriture accidentelle. Si Claude essaie d'appeler Write, l'appel est refusé, il continue avec l'information manquante.

Pour un job qui génère de la documentation à partir du code et l'écrit dans docs/ :

claude -p "génère la doc API à partir de src/routes/" \
  --permission-mode acceptEdits \
  --settings .claude/ci-settings.json

Les écritures passent sans prompt, mais si Claude décide qu'il a besoin de lancer npm install pour comprendre une dépendance, la confirmation bash bloquera. C'est voulu : en CI, tu ne veux pas qu'un agent installe des paquets arbitrairement.

Reste le cas --dangerously-skip-permissions (équivalent à --permission-mode bypassPermissions). À réserver aux containers jetables. Sur un runner GitHub Actions qui s'auto-détruit après le job, dans un Docker isolé du réseau, ça a du sens. Sur un serveur partagé avec des clés SSH montées, jamais. La règle : si le container qui exécute Claude survit à la fin du job, n'utilise pas bypass.

Depuis la version 2.1.261 de Claude Code (septembre 2026), un flag complémentaire existe : --permission-prompts none. Il ne donne aucune permission supplémentaire, il refuse automatiquement tout ce qui aurait demandé une confirmation. Combiné avec --permission-mode acceptEdits, tu obtiens un profil précis : les éditions passent, tout le reste est refusé silencieusement au lieu de bloquer. C'est le réglage que tu veux pour une session non surveillée où tu préfères qu'une action douteuse échoue plutôt qu'elle attende un humain.

Configurer les permissions par outil dans settings.json

Les flags CLI marchent, mais les répéter dans chaque script devient vite pénible. Le fichier .claude/settings.json commité au repo permet de définir un comportement par défaut, valable pour toute l'équipe et tous les jobs CI.

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Write(src/**)",
      "Write(docs/**)",
      "Bash(npm test:*)",
      "Bash(git log:*)"
    ],
    "deny": [
      "Bash(rm:*)",
      "Bash(curl:*)",
      "Write(.env*)",
      "Write(package.json)"
    ],
    "ask": [
      "Bash(git push:*)",
      "WebFetch(*)"
    ]
  }
}

Trois catégories, trois comportements. allow passe sans prompt. deny refuse toujours, même en bypass. ask demande à l'utilisateur, ce qui en headless veut dire refuse (couplé avec --permission-prompts none) ou bloque (sans ce flag).

La précédence va du plus local au plus global : les règles dans .claude/settings.local.json (ignoré par git, spécifique au dev) l'emportent sur celles de .claude/settings.json (partagé), qui l'emportent sur la config utilisateur globale dans ~/.claude/settings.json. Pratique pour qu'un dev active temporairement Bash(docker:*) chez lui sans polluer le repo.

Un piège fréquent : les patterns sont matchés strictement. Bash(npm test) autorise uniquement la commande exacte. Pour autoriser toutes les variantes (npm test, npm test -- --watch, etc.), il faut Bash(npm test:*). Idem pour les chemins : Write(src) ne matche pas src/api/handler.ts, il faut Write(src/**). Si tu veux voir cette logique appliquée à un vrai projet en cours, la création d'une application complète avec Claude Code détaille un settings.json de référence.

Nettoyer ses Skills avec skill-doctor

Après quelques semaines, ton dossier .claude/skills/ ressemble à un tiroir de câbles. Des Skills expérimentaux jamais finis, des doublons entre le repo et le user global, des descriptions trop vagues pour que Claude les charge un jour. Chaque Skill installé consomme du contexte à chaque appel, même s'il ne sert pas dans la session.

La commande /skill-doctor (ajoutée en version 2.1.261 de Claude Code, septembre 2026) audite la session en cours. Elle liste les Skills chargés mais non utilisés, leur coût en tokens, et te donne de quoi décider quoi supprimer.

/skill-doctor

Skills chargés cette session : 12
Utilisés : 3
Inutilisés : 9

Top coût contexte (inutilisés) :
  - pdf-extractor        4 200 tokens
  - stripe-webhooks      3 800 tokens
  - notion-sync          3 100 tokens
  - old-migration-helper 2 700 tokens

Deux réflexes à prendre. D'abord, un Skill dont la description est trop vague ne sera jamais chargé par Claude (le routeur ne trouvera pas de match), donc autant le supprimer plutôt que le garder "au cas où". Ensuite, les Skills spécifiques à un contexte de production (comme stripe-webhooks) n'ont rien à faire en CI si le job n'implique pas Stripe. Un settings.json par environnement, ou un flag --skill explicite qui n'active que les Skills nommés, évite d'alourdir le contexte pour rien.

Un script CI complet pour review de PR

Voilà un workflow GitHub Actions qui checkout la PR, lance Claude en mode plan pour analyser le diff, et poste le résultat en commentaire. Rien à adapter à part le prompt.

name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Generate diff
        run: |
          git diff origin/${{ github.base_ref }}...HEAD \
            > /tmp/pr.diff

      - name: Run Claude review
        id: review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p "$(cat <<'EOF'
          Review ce diff. Format de sortie :
          - 3 risques principaux (sécurité, perf, logique)
          - 2 suggestions d'amélioration
          - Verdict : LGTM / CHANGES_REQUESTED
          Sois direct, pas de préambule.
          EOF
          )" \
            --permission-mode plan \
            --permission-prompts none \
            < /tmp/pr.diff \
            > /tmp/review.md 2> /tmp/review.err
          
          if [ $? -ne 0 ]; then
            echo "Claude a échoué, voir logs"
            cat /tmp/review.err
            exit 1
          fi

      - name: Post comment
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('/tmp/review.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Claude review\n\n${body}`
            });

Le mode plan garantit que Claude ne peut rien modifier dans le repo checkout. --permission-prompts none évite qu'un appel d'outil imprévu bloque le job. La capture séparée de stderr permet de debug si le prompt était mal formé ou si l'API a rate-limited. Pour aller plus loin sur ce genre d'orchestration, la page configurer Claude pour un usage quotidien couvre les patterns de settings réutilisables entre projets.

Les pièges à éviter

Sept choses qui cassent en pratique, avec leur signature.

PiègeSymptômeFix
MCP indisponible en CIClaude tente d'appeler un connecteur (Notion, Slack) qui n'est pas configuré sur le runner, échec avec message cryptiqueDésactiver les serveurs MCP dans le settings.json du repo, ou utiliser allowedMcpServers pour lister seulement ceux qui existent
bypassPermissions sur runner partagéUn job compromis peut lire les secrets ou modifier des fichiers hors du workspaceContainer jetable obligatoire, ou --permission-mode plan si le job n'a pas besoin d'écrire
stderr non capturéJob qui échoue silencieusement, logs vides côté GitHubToujours rediriger 2> error.log et cat le fichier en cas de non-zero exit
Skills qui appellent une API absenteTimeout au premier appel du Skill, session gelée/skill-doctor en local pour identifier, désinstaller ce qui ne marche pas hors dev
Prompt trop long piped depuis stdinErreur de context window ou troncature silencieusePré-filtrer le diff (garder les fichiers modifiés significatifs), splitter si nécessaire
ANTHROPIC_API_KEY manquantMessage d'auth au démarrage, exit code 1Vérifier le nom exact du secret, tester en local avec la même variable d'environnement avant de push
Absence de timeoutJob qui tourne 6 heures et facture pour rien si Claude entre en boucleToujours timeout-minutes sur le job ET un timeout shell sur la commande claude

Le fil rouge : en headless, tout ce qui aurait déclenché une intervention humaine devient un mode d'échec silencieux. Le bon réflexe est de tester le pipeline complet en local avec les mêmes flags que la CI, pas juste la commande claude isolée.

Et maintenant

Une fois que tu as un script CI qui tourne, la question suivante devient : qu'est-ce que tu construis avec ces briques ? Une revue automatique de PR est utile, mais c'est le premier étage. Les workflows intéressants combinent Claude Code en headless avec un backend, une interface utilisateur, un déploiement continu. Si tu veux passer de scripts isolés à un vrai produit livré, construire votre produit IA en 5 semaines couvre le cycle complet : architecture, développement avec Claude Code, déploiement, mise en production. Le mode headless que tu viens de câbler devient alors une brique parmi d'autres dans un pipeline qui tient debout.

Pilier 7 · Mastery

Créer une application avec Claude Code (sans être développeur)

De l'idée au MVP fonctionnel en quelques jours grâce à Claude Code. Le sujet le plus puissant pour les futurs Builders.

Découvrir le pilier complet →Formation Claude Builder