Aller au contenu
English
Développement medium

Claude Code : mode headless et intégration CI pour automatiser sans session interactive

40 min de lecture

Logo Claude Code - mode headless et CI

Le mode headless (claude -p) exécute Claude Code sans session interactive : un prompt en argument, une réponse sur stdout, un code de sortie exploitable par un script. Cette page pose les bases CLI, montre comment obtenir une sortie typée plutôt qu'un texte à deviner, et déroule une intégration GitHub Actions qui relit chaque PR ouverte sur lab-claude. Elle insiste surtout sur un point que la plupart des tutoriels passent sous silence : sans l'option --bare, un claude -p charge les hooks, les serveurs MCP et le CLAUDE.md du dépôt courant, sans jamais vous demander votre confiance. C'est la première chose à régler avant de brancher Claude sur une chaîne d'intégration.

  • Lancer Claude en non-interactif avec claude -p "prompt"
  • Piper du contexte via stdin (diff, log, fichier)
  • Récupérer une sortie texte, JSON ou typée par un schéma
  • Isoler l'exécution avec --bare pour un résultat identique partout
  • Gérer les permissions dans un environnement sans confirmation possible
  • Déclencher un job GitHub Actions qui fait relire chaque PR par Claude
  • Choisir entre l'intégration officielle et un appel claude -p maison
  • Comprendre les limites : coûts, idempotence, secrets
  • Automatiser une revue légère de diff à chaque PR ou pré-commit
  • Générer un changelog ou une synthèse depuis un historique git
  • Valider un artefact (doc, config) dans un pipeline
  • Piloter Claude depuis un outil maison (script Python, job Airflow, webhook)

Ce que ce n'est pas : un remplacement à vos tests. Claude produit une analyse probabiliste, pas une vérité déterministe. Gardez ruff + pytest comme filet de sécurité.

  • claude --version fonctionnel dans le terminal
  • lab-claude opérationnel, ruff et pytest verts
  • Compréhension des permissions settings.json
  • Un compte GitHub si vous souhaitez dérouler la partie Actions

La forme minimale lance Claude avec un prompt et imprime la réponse sur stdout.

Fenêtre de terminal
claude -p "Résume le dépôt courant en 5 points. Ne modifie aucun fichier."

Quelques variantes immédiatement utiles :

Fenêtre de terminal
# Piper un fichier comme contexte
cat README.md | claude -p "Relève les 3 incohérences majeures avec le code actuel."
# Piper un diff git
git diff main...HEAD | claude -p "Relis ce diff. Liste les remarques bloquantes en 3 puces max."
# Sortie JSON pour un script
claude -p "Donne la liste des endpoints de app/main.py" --output-format json
OptionSortieBon pour
rien (défaut)Texte brutLecture humaine, affichage dans un terminal
--output-format jsonJSON structuré : result porte le texte, avec session_id, l'usage et total_cost_usdScript qui parse la réponse avec jq
--output-format stream-jsonLignes JSON au fur et à mesurePipeline qui consomme la sortie en flux

Exemple de parsing JSON. Le texte de la réponse se trouve dans .result, et non dans .content : c'est l'erreur la plus fréquente sur ce format, et elle donne un null silencieux qui casse le script plus loin.

Fenêtre de terminal
claude -p "Liste les endpoints de app/main.py" \
--output-format json |
jq -r '.result'

La charge JSON porte aussi session_id, l'usage et total_cost_usd, ce qui permet de suivre le coût par invocation sans consulter de tableau de bord.

Demander une sortie typée plutôt que de la deviner

Section intitulée « Demander une sortie typée plutôt que de la deviner »

En intégration continue, ne demandez pas à jq de deviner la structure d'une réponse en langage naturel. L'option --json-schema impose un schéma JSON à la réponse, qui arrive alors dans le champ structured_output. C'est la différence entre un script qui casse au premier changement de formulation et un script qui contractualise ce qu'il reçoit.

Fenêtre de terminal
claude -p "Extrais les endpoints de app/main.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"endpoints":{"type":"array","items":{"type":"string"}}},"required":["endpoints"]}' \
| jq '.structured_output'

Si le schéma est invalide, claude s'arrête sur Error: --json-schema is not a valid JSON Schema suivi du diagnostic du validateur. Le mot-clé format, par exemple "format": "email", est accepté mais traité comme une annotation, donc non contraignant : ne comptez pas dessus pour valider une donnée.

Pourquoi --bare est indispensable en intégration continue

Section intitulée « Pourquoi --bare est indispensable en intégration continue »

Sans --bare, un claude -p charge le même contexte qu'une session interactive. C'est le point de sécurité le plus important de ce guide, et il surprend presque tout le monde. Concrètement, -p découvre et charge automatiquement les hooks, les skills, les commandes personnalisées, les subagents, les plugins, les serveurs MCP, la mémoire automatique et le CLAUDE.md trouvés dans le répertoire de travail ou dans ~/.claude.

Et il le fait sans rien vous demander. La documentation est explicite : un -p exécute les hooks du .claude/settings.json d'un projet et se connecte aux serveurs de son .mcp.json même dans un dossier auquel vous n'avez jamais accordé votre confiance, parce qu'une session -p n'affiche ni le dialogue de confiance de l'espace de travail, ni la demande d'approbation par serveur.

Posez le raisonnement sur une chaîne d'intégration qui construit une pull request venue de l'extérieur : le dépôt cloné apporte ses propres hooks et sa propre configuration MCP, et votre claude -p les exécute. C'est une frontière de confiance franchie sans intention.

Fenêtre de terminal
claude --bare -p "Résume README.md" --allowedTools "Read"

En mode bare, Claude Code ne lit ni les identifiants OAuth, ni le trousseau du système. Votre abonnement ne s'applique donc pas : pour l'API Anthropic, il faut exporter ANTHROPIC_API_KEY, ou fournir un apiKeyHelper dans le JSON passé à --settings. Amazon Bedrock, Google Cloud et Microsoft Foundry continuent de lire leurs propres identifiants.

Le mode bare ne vous prive de rien : il vous oblige à déclarer ce que vous chargez, ce qui est exactement ce qu'on attend d'une chaîne reproductible.

Pour chargerPasser
des ajouts au prompt système--append-system-prompt, --append-system-prompt-file
des réglages--settings <fichier-ou-json>
des serveurs MCP--mcp-config <fichier-ou-json>
des agents personnalisés--agents <json>
un plugin--plugin-dir <chemin>, --plugin-url <url>

En headless, personne n'est là pour approuver une demande de permission. Le mode de démarrage d'un -p est Manual sur tous les plans : si vous ne choisissez rien, la première demande bloque le job. Quatre stratégies, de la plus stricte à la plus permissive :

StratégieRéglageQuand l'utiliser
Refuser tout ce qui n'est pas listé--permission-mode dontAskexécution verrouillée : c'est le mode que la documentation recommande pour la CI
Lister explicitementsettings.json avec un allow précis, ou --allowedToolspipeline dont le périmètre d'outils est connu
Explorer sans modifier les sources--permission-mode planrevue de diff, analyse. Attention, plan exécute des commandes en lecture seule : ce n'est pas une isolation
Pré-accepter les écritures--permission-mode acceptEditsscript qui doit écrire, avec un impact borné

Quand personne ne peut répondre, ajoutez --permission-prompts none : tout ce qui déclencherait une demande est refusé, Claude en est informé et n'insiste pas, et le job continue au lieu d'attendre. L'option demande Claude Code 2.1.259 ou plus récent.

Règle générale : en CI, préférez un périmètre listé explicitement à un mode global permissif.

En session interactive, Claude Code utilise votre connexion OAuth. En headless, vous avez deux chemins :

ContexteMécanisme
Machine locale déjà connectée, sans --barerien à faire, la session stockée est utilisée
Avec --bareANTHROPIC_API_KEY obligatoire : le mode bare ne lit ni les identifiants OAuth ni le trousseau du système
CI, conteneur, machine neuveANTHROPIC_API_KEY, clé créée dans la console Anthropic

Le piège tient en une phrase : le jour où vous ajoutez --bare à un script qui marchait sur votre poste, il cesse de fonctionner faute de clé. Ce n'est pas une régression, c'est la contrepartie de l'isolation. Amazon Bedrock, Google Cloud et Microsoft Foundry continuent, eux, de lire leurs propres identifiants.

En CI, la clé API est toujours un secret du job, jamais en clair dans le workflow ou dans un fichier commité.

Objectif : un script shell que vous lancez avant git push et qui fait relire votre diff par Claude.

  1. Placez-vous à la racine

    Fenêtre de terminal
    cd ~/Projets/lab-claude
  2. Créez scripts/review.sh

    mkdir -p scripts
    cat > scripts/review.sh <<'EOF'
    #!/usr/bin/env bash
    set -euo pipefail
    DIFF=$(git diff main...HEAD)
    if [ -z "$DIFF" ]; then
    echo "Aucun diff par rapport à main."
    exit 0
    fi
    echo "$DIFF" | claude --bare -p \
    --permission-mode dontAsk \
    "Relis ce diff. Liste uniquement les remarques bloquantes \
    (bug probable, test manquant critique, faille). \
    3 puces max. Ne retourne rien si tout est propre."
    EOF
    chmod +x scripts/review.sh
  3. Lancez-le

    Fenêtre de terminal
    ./scripts/review.sh
  4. Intégrez-le à votre routine

    Ajoutez un alias local, ou appelez-le depuis un hook pre-push git. Pas besoin d'un .husky ou d'une dépendance npm, un simple .git/hooks/pre-push suffit.

Application CI : GitHub Actions qui relit chaque PR

Section intitulée « Application CI : GitHub Actions qui relit chaque PR »

Objectif : à chaque PR ouverte sur lab-claude, Claude relit le diff et poste un commentaire récapitulatif.

  1. Ajoutez la clé API en secret GitHub

    Dans le repo GitHub : SettingsSecrets and variablesActionsNew repository secret → nom ANTHROPIC_API_KEY, valeur depuis votre console Anthropic.

  2. Posez un settings.json CI minimal

    Créez .claude/settings.json côté dépôt (s'il n'existe pas déjà) :

    {
    "$schema": "https://json.schemastore.org/claude-code-settings.json",
    "permissions": {
    "allow": [
    "Bash(git diff *)",
    "Bash(git log *)",
    "Bash(git status *)",
    "Read(./*)",
    "Read(./**)"
    ],
    "deny": [
    "Write(./**)",
    "Edit(./**)"
    ]
    }
    }

    Ce qui protège réellement ici, c'est la liste allow, pas la liste deny. Le deny sur Write et Edit porte sur des noms d'outils : il les retire, et c'est solide. En revanche, une version antérieure de cette page ajoutait Bash(curl *) et Bash(wget *) au deny, ce qui donne une fausse assurance : une règle Bash s'applique au texte de la commande, et /usr/bin/curl ou sh -c 'curl …' y échappent. C'est le contre-exemple que donne la documentation elle-même.

    La protection vient donc du fait que seules les commandes git diff, git log et git status sont autorisées : tout le reste, curl compris, n'est jamais approuvé. Pour une contrainte réseau qui ne dépende pas de l'écriture de la commande, il faut un sandbox ou un hook PreToolUse, traités dans les leçons de sécurité du parcours.

  3. Ajoutez le workflow .github/workflows/claude-review.yml

    name: Claude review on PR
    on:
    pull_request:
    types: [opened, synchronize]
    permissions:
    contents: read
    pull-requests: write
    jobs:
    review:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
    fetch-depth: 0
    - name: Install Claude Code CLI
    run: |
    curl -fsSL https://claude.ai/install.sh -o claude-install.sh
    bash claude-install.sh
    - name: Run Claude review
    env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    run: |
    DIFF=$(git diff origin/${{ github.event.pull_request.base.ref }}...HEAD)
    echo "$DIFF" | claude --bare -p \
    --permission-mode dontAsk \
    --permission-prompts none \
    --output-format json \
    "Relis ce diff. Liste les remarques bloquantes, à discuter, mineures. \
    Sois bref (max 8 lignes au total). Si rien à dire, réponds exactement: OK." \
    | jq -r '.result' > review.md
    - name: Post comment
    uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
    with:
    script: |
    const fs = require('fs');
    const body = fs.readFileSync('review.md', 'utf8').trim();
    if (body && body !== 'OK') {
    github.rest.issues.createComment({
    owner: context.repo.owner,
    repo: context.repo.repo,
    issue_number: context.payload.pull_request.number,
    body: '### Revue Claude\n\n' + body
    });
    }
  4. Poussez une branche et ouvrez une PR

    Le job se déclenche, Claude lit le diff, et le commentaire apparaît dans la PR. Si le diff est jugé propre, Claude répond OK et le job ne poste rien.

Vous venez de construire l'intégration à la main, et c'était l'objectif : comprendre ce qui se passe. En production, la question se pose autrement, car Anthropic publie des intégrations officielles qui font ce travail pour vous.

L'échelle de décision tient en trois lignes :

Votre situationCe qu'il faut employer
Dépôt GitHub, besoin courant : répondre à une mention, relire une PR, transformer une issue en PRl'action officielle anthropics/claude-code-action
Dépôt GitLabl'intégration GitLab CI/CD officielle
Workflow spécifique : votre propre orchestration, un cron maison, un outil interneclaude --bare -p, ou l'Agent SDK en Python ou TypeScript

Ne reconstruisez pas ce qui existe. Le cas le plus fréquent, faire relire les pull requests, est couvert par un produit maintenu, et le maintenir vous-même vous fait porter des correctifs que quelqu'un d'autre écrit déjà.

Depuis une session Claude Code ouverte dans le dépôt :

/install-github-app

La commande installe l'application GitHub, crée le secret d'authentification et pousse une branche avec les fichiers de workflow, prête à ouvrir en pull request. Elle exige un accès administrateur au dépôt et le CLI GitHub authentifié par gh auth login.

Le workflow minimal, et ce qui n'y est pas du décor

Section intitulée « Le workflow minimal, et ce qui n'y est pas du décor »
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@19dda84776b3518d98b8798e591daee763049ed3 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

Quatre lignes méritent une explication, parce qu'on les supprime facilement en croyant simplifier :

  • id-token: write est requis pour l'authentification par défaut via l'application GitHub. Sans elle, rien ne démarre.
  • actions: read permet à Claude de lire les résultats de CI sur les pull requests, ce qui change beaucoup la qualité de ses réponses.
  • actions/checkout donne une copie locale du dépôt.
  • Le if: évite de démarrer un runner sur chaque commentaire. L'action vérifie de son côté la phrase de déclenchement, mais autant ne pas payer le démarrage.

L'action déduit son mode de votre configuration, sans réglage explicite.

ModeDéclenchementOù arrive le résultat
Interactifpas d'entrée prompt : Claude attend la phrase @claude dans un commentaire, une revue, ou le corps d'une issueen commentaire sur l'issue ou la PR
Automatisationune entrée prompt est fournie : Claude s'exécute sans attendre de mentiondans le journal du job, sauf si le prompt lui demande de publier

Deux vérifications précèdent chaque exécution, et c'est une bonne nouvelle pour votre modèle de menace. L'auteur du déclenchement doit avoir l'accès en écriture au dépôt, sur les événements d'issue et de pull request. Et un acteur robot est refusé, sauf s'il figure dans allowed_bots, ce qui empêche deux automatisations de se déclencher mutuellement en boucle.

Cette seconde vérification s'applique aussi aux exécutions planifiées, que GitHub attribue à un utilisateur du dépôt, en général la dernière personne à avoir modifié le cron.

Deux secrets possibles, et le choix n'est pas indifférent.

SecretOrigineQuand le préférer
ANTHROPIC_API_KEYconsole Anthropicà l'échelle d'une organisation
CLAUDE_CODE_OAUTH_TOKENclaude setup-token en localusage individuel, sur un abonnement Pro, Max, Team ou Enterprise

Pour un secret partagé entre dépôts, prenez la clé API. Un jeton OAuth est lié à l'abonnement de la personne qui a lancé claude setup-token : le jour où elle quitte l'équipe, toute l'organisation s'arrête.

Pour ne stocker aucun secret durable, la fédération d'identité échange le jeton OIDC du workflow contre un accès à l'API, via un compte de service. Elle demande la permission id-token: write et trois entrées : anthropic_federation_rule_id, anthropic_organization_id et anthropic_service_account_id.

FlagEffet
--baren'auto-découvre ni hooks, ni skills, ni commandes, ni subagents, ni plugins, ni MCP, ni mémoire, ni CLAUDE.md. À mettre partout en CI
--permission-mode <mode>default, acceptEdits, plan, auto, dontAsk, bypassPermissions. En CI, dontAsk
--permission-prompts nonerefuse au lieu d'attendre quand personne ne peut répondre
--json-schema <schema>impose un schéma à la réponse, récupérée dans structured_output
--output-format <format>text (défaut), json, stream-json
--continueReprend la dernière session (moins utile en CI, utile en script local)
--model <alias>force un modèle par son alias (sonnet, opus, haiku, fable). L'alias suit le modèle recommandé, ce qui évite d'épingler un identifiant qui vieillit
--max-turns <N>Limite le nombre de tours, protection anti-boucle

Un appel headless consomme des tokens à chaque invocation. Bons réflexes :

  • Limiter la taille du contexte : ne pipez pas tout le dépôt, seulement le diff ou le fichier pertinent
  • Imposer un format court dans le prompt (« 3 puces max », « max 8 lignes »)
  • Choisir un modèle proportionné : haiku pour une revue légère, sonnet pour une analyse sérieuse
  • Caper avec --max-turns si Claude peut enchaîner des outils

Un job CI qui tourne sur chaque push, avec un prompt non borné, peut devenir cher très vite. Mesurez en local avant de laisser tourner.

SymptômeCause probableCorrection
claude: command not found en CICLI non installé dans l'imageAjouter l'étape curl ... install.sh ou utiliser une image qui l'inclut
Le job CI reste bloquéune demande de permission attend une réponse que personne ne donnera--permission-mode dontAsk et --permission-prompts none, plus un allow qui couvre le périmètre réel
Un hook ou un serveur MCP inattendu s'exécute en CI-p charge le contexte du dépôt cloné sans dialogue de confianceajouter --bare, puis déclarer explicitement ce dont le job a besoin
jq renvoie null sur la sortie JSONmauvais champ : le texte est dans .resultutiliser jq -r '.result', ou --json-schema puis jq '.structured_output'
Claude écrit un fichier en CIWrite/Edit laissés implicitement autorisésAjouter Write(./**) et Edit(./**) dans deny
Coût qui dérapeContexte trop gros ou prompt trop ouvertPiper moins, borner le format, basculer sur haiku
ANTHROPIC_API_KEY n'est pas vueSecret mal câblé dans le workflowVérifier la section env: du step et le nom du secret
  • claude -p "..." fonctionne sur ma machine locale
  • J'ai un script scripts/review.sh ou équivalent pour mon usage quotidien
  • Mon settings.json CI a un deny sur Write et Edit
  • Mon job CI utilise ANTHROPIC_API_KEY comme secret et pas en clair
  • Tous mes appels scriptés passent --bare, et je sais ce que je recharge explicitement
  • Je parse .result, ou structured_output quand j'ai imposé un schéma
  • Je sais qu'un deny sur une commande Bash ne remplace pas un sandbox
  • Je sais reconnaître les cas où l'intégration officielle vaut mieux que mon propre workflow
  • J'ai borné mon prompt (format court) pour contrôler les coûts
  • Je sais que Claude ne remplace pas ruff + pytest

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • claude -p transforme Claude en commande shell comme une autre
  • En CI, préférez un settings.json restrictif à --permission-mode bypassPermissions
  • --output-format json rend la sortie exploitable par jq et autres scripts
  • Claude en CI produit de l'analyse, pas de la vérité : il complète vos tests, ne les remplace pas
  • Coûts et périmètre se contrôlent par le prompt, pas seulement par les flags

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn