Aller au contenu

Command Palette

Search for a command to run...

Claude Code headless et Codex : de l'assistant à l'agent

🇬🇧EN

claude -p, --json-schema et codex exec : lancer un agent IA sans terminal, avec une réponse JSON qu'un script peut lire, puis passer à un workflow IronFlow.

9 min de lecture
claude-codecodexagents-iaheadlessironflowautomatisation
Trois niveaux pour sortir un agent IA du terminal : claude -p, un skill lancé en headless, un orchestrateur IronFlow

Un assistant attend qu’on lui parle. Tant que Claude Code ou Codex tournent dans un terminal ouvert, il faut quelqu’un devant pour taper le prompt, lire la réponse et décider de la suite. Le mode headless de Claude Code et codex exec changent ça : une commande, une réponse JSON validée par un schéma, et un script qui décide à votre place.

Cet article montre les trois niveaux que j’utilise pour sortir un agent du terminal : une ligne de shell, un skill lancé sans interface, puis un orchestrateur. Avec les commandes exactes pour Claude Code et pour Codex, et deux pièges que j’ai rencontrés en écrivant les exemples.

Assistant ou agent : la différence tient dans le déclencheur

Ma première revue de code IA passait par un node n8n qui appelait Claude Code. Un diff de 2 000 lignes pouvait coûter 15 dollars pour des findings bruités, mais personne n’avait besoin d’ouvrir un terminal. C’était déjà un agent, mal réglé.

Ce qui sépare un assistant d’un agent n’est pas le modèle. C’est ce qui l’entoure :

  • Un déclencheur qui n’est pas un humain : un commit, un webhook, un cron.
  • Une sortie contractuelle qu’un programme peut lire : un JSON qui respecte un schéma, pas un paragraphe.
  • Des bornes : un budget en dollars, une liste d’outils, un mode de permission. Personne n’est là pour dire stop.

Claude Code et Codex fournissent les deux derniers points en flags. Le déclencheur, c’est votre CI, votre cron ou votre orchestrateur.

AssistantNiveau 1 et 2 : commandeNiveau 3 : orchestrateurHumain au clavierTerminal interactifTexte à lirel’humain décideJob CI, cron, hookun seul appelclaude -p / codex execskill, effort, budgetJSON + schémale script décideWebhook, cron, APIruns persistésIronFlowshell, agent, approvalÉtapes typéesl’humain valide au besoinLe niveau 3 appelle le niveau 1 : IronFlow lance claude -p avec —json-schema

Niveau 1 : claude -p et —json-schema

Le flag -p lance la boucle d’agent complète, imprime le résultat et rend la main. La documentation officielle décrit trois formats de sortie. Celui qui compte pour un script, c’est json combiné à --json-schema :

claude -p "Classe ce message de commit : 'fix: #9 build the image with kaniko on the Talos runner'" \
  --model haiku \
  --output-format json \
  --max-budget-usd 0.20 \
  --json-schema '{
    "type": "object",
    "properties": {
      "type":    { "type": "string", "enum": ["feat", "fix", "ci", "docs", "chore"] },
      "issue":   { "type": ["integer", "null"] },
      "summary": { "type": "string" }
    },
    "required": ["type", "issue", "summary"],
    "additionalProperties": false
  }' | jq '.structured_output'

La réponse complète contient le texte dans result, et l’objet validé dans structured_output :

{
  "type": "fix",
  "issue": 9,
  "summary": "build the image with kaniko on the Talos runner"
}

C’est cet objet qui fait la différence avec un assistant. Un script peut brancher sur type, refuser un commit sans issue, ou router vers la bonne équipe, sans qu’un humain lise la réponse.

Trois flags transforment l’appel en agent borné :

  • --max-budget-usd coupe la session au-delà d’un montant.
  • --allowedTools "Read,Grep" ou --permission-mode dontAsk limitent ce que l’agent peut faire sans humain pour valider.
  • --bare ignore les hooks, les plugins, les serveurs MCP et le CLAUDE.md de la machine. La documentation le recommande pour la CI, avec une contrepartie : il exige une clé ANTHROPIC_API_KEY et n’utilise pas l’abonnement.

Ce dernier point compte plus qu’on ne le croit. Sans --bare, -p charge tout le contexte d’une session interactive : skills, CLAUDE.md, serveurs MCP. Le même script ne se comporte donc pas pareil sur mon poste et sur le runner CI. C’est le même constat que pour les skills qui dégradent un agent : tout ce qu’on charge influence la réponse.

Le même contrat avec Codex : codex exec

Codex suit exactement le même modèle. codex exec remplace l’interface par un process qui lit un prompt, travaille et sort. Le schéma passe par un fichier, et -o écrit le dernier message (documentation Codex) :

codex exec \
  --sandbox read-only \
  --ephemeral \
  --output-schema commit.schema.json \
  -o result.json \
  "Classe ce message de commit : 'fix: #9 build the image with kaniko on the Talos runner'" \
  </dev/null

Même schéma, même réponse. Le </dev/null à la fin n’est pas décoratif. Mon premier essai depuis un script n’a jamais rendu la main : quand stdin est ouvert, codex exec l’ajoute au prompt et attend qu’il se ferme. Après trois minutes, je l’ai tué. Dans un job CI ou un script, fermer stdin explicitement.

Les flags se correspondent presque un à un :

BesoinClaude CodeCodex
Mode non interactifclaude -p "..."codex exec "..."
Sortie validée par schéma--output-format json --json-schema '<schéma>'--output-schema schema.json -o out.json
Niveau de raisonnement--effort low à max-c model_reasoning_effort="low"
Bornes--max-budget-usd, --allowedTools--sandbox read-only
Revue de codeskill /code-reviewcodex exec review
Événements en flux--output-format stream-json--json

Le principe vaut pour tout harness qui a un mode non interactif. Ce qui change d’un outil à l’autre, c’est le nom du flag, pas l’architecture.

Niveau 2 : lancer un skill sans terminal

Un skill est une procédure écrite et testée une fois. Le lancer en headless, c’est réutiliser cette procédure dans un pipeline sans la recopier dans un prompt. La documentation le confirme : un /nom-du-skill placé dans le prompt de -p est développé avant l’exécution. Avec un skill que j’ai écrit, --json-schema s’applique normalement à la réponse finale.

La commande /code-review de Claude Code prend un niveau d’effort, de low (peu de findings, très sûrs) à max (beaucoup, certains incertains), et une cible : un fichier, une branche, un commit.

claude -p "/code-review low .gitlab-ci.yml" --output-format json --max-budget-usd 2 </dev/null \
  | jq -r '.result'

La revue revient en texte. Le réflexe suivant est d’ajouter --json-schema pour récupérer des findings typés. C’est le deuxième piège : /code-review est une commande intégrée qui fait sa revue hors de la boucle principale. Le JSON de sortie indique num_turns: 0, et structured_output reste à null (constaté avec Claude Code 2.1.291). La revue tourne, mais le schéma n’est jamais appliqué.

La solution est de demander la revue en langage naturel. Le modèle lance alors le skill comme un outil, et sa réponse finale passe par le schéma :

claude -p "Run the code-review skill at level low on .gitlab-ci.yml, then return its findings." \
  --effort medium \
  --output-format json \
  --json-schema "$(cat findings.schema.json)" \
  --max-budget-usd 2 </dev/null | jq '.structured_output.findings'

Cette fois, structured_output contient un tableau de findings avec file, line, severity et summary. Deux niveaux d’effort coexistent ici : --effort règle le raisonnement du modèle pour toute la session, low est l’argument du skill et règle combien de findings il garde.

Côté Codex, la revue est une sous-commande, et l’effort passe par la configuration :

codex exec review --base main -c model_reasoning_effort='"low"' --ephemeral </dev/null

--base main relit les changements de la branche courante, --commit <sha> un seul commit, --uncommitted le travail en cours. Dans les deux outils, la revue se lance en une ligne depuis un job CI.

Niveau 3 : quand une ligne de shell ne suffit plus

Un claude -p dans un job CI couvre beaucoup de cas. Il ne couvre pas ceux-ci :

  • Plusieurs étapes qui dépendent l’une de l’autre : récupérer un diff, le faire relire, poster les commentaires, relancer si l’API répond 502.
  • Une validation humaine au milieu : l’agent propose, quelqu’un approuve, la suite s’exécute. Pendant ce temps, le run doit survivre à un redémarrage.
  • Un déclencheur qui n’est pas la CI : un webhook Sentry, un cron nocturne, un appel d’API.
  • Un coût suivi sur l’ensemble, pas appel par appel.

C’est pour ces cas que j’ai écrit IronFlow, un moteur de workflows où les workflows sont du code Rust. J’ai expliqué pourquoi Rust plutôt que du YAML dans un autre article. Ici, ce qui compte, c’est qu’IronFlow ne réinvente pas le niveau 1. Son provider Claude Code construit la même ligne de commande :

let mut args: Vec<String> = vec![
    "-p".to_string(),
    config.prompt.clone(),
    "--output-format".to_string(),
    output_format.to_string(),
];
// ...
push_opt(&mut args, "--max-budget-usd", &config.max_budget_usd);
// ...
push_opt(&mut args, "--json-schema", &transformed_schema);

Le schéma ne s’écrit plus à la main. Il vient d’un type Rust qui dérive JsonSchema, et la réponse revient typée. Un workflow de revue avec une validation humaine quand l’agent trouve un problème bloquant :

#[derive(Deserialize, JsonSchema)]
struct Review {
    blocking: bool,
    findings: Vec<String>,
}

impl WorkflowHandler for ReviewMr {
    fn name(&self) -> &str {
        "review-mr"
    }

    fn execute<'a>(&'a self, ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
        Box::pin(async move {
            let diff = ctx
                .shell("diff", ShellConfig::new("git diff origin/main...HEAD"))
                .await?;

            let review = ctx
                .agent(
                    "review",
                    AgentStepConfig::new(&format!("Review this diff:\n{}", diff.stdout()))
                        .model("sonnet")
                        .max_turns(5)
                        .max_budget_usd(0.50)
                        .output::<Review>(),
                )
                .await?;

            if review.blocking {
                // Le run se suspend ici jusqu'à une décision humaine
                ctx.approval("merge-gate", ApprovalConfig::new("Findings bloquants. Merger quand même ?"))
                    .await?;
            }
            Ok(())
        })
    }
}

Le if review.blocking est du Rust ordinaire, parce que review est une structure et pas du texte. C’est tout l’intérêt du schéma : il transforme la réponse d’un modèle en valeur sur laquelle le code peut brancher. Le moteur persiste chaque étape. Quand le run reprend après l’approbation, le diff et la revue sont rejoués depuis le cache au lieu d’être recalculés et refacturés.

CodeRift, mon outil de revue de merge requests, est né de cette progression : un node n8n qui appelait Claude Code, puis un workflow IronFlow avec des étapes, des règles et un budget par agent.

Ce que je changerais

Le schéma garantit la forme, pas le fond. Deux revues du même fichier, au même niveau, ne m’ont pas rendu les mêmes findings. Un JSON valide reste une opinion de modèle. Pour une décision qui bloque une merge request, je garde une validation humaine ou je croise deux runs.

Le headless exécute ce que le dépôt contient. Sans --bare, claude -p lance les hooks du .claude/settings.json et les serveurs du .mcp.json du dossier courant, sans fenêtre de confiance. Sur un dépôt qu’on ne contrôle pas, comme une MR externe, c’est une porte d’entrée. J’ai détaillé ce type de risque dans l’article sur les failles des agents de code IA. En CI, --bare et une liste d’outils explicite.

Commencer par une ligne de shell. J’ai écrit IronFlow parce que mes besoins dépassaient un appel isolé. La plupart des automatisations n’en ont pas besoin : un job CI avec claude -p ou codex exec, un schéma et un budget couvrent la revue, le tri d’issues, la classification de commits. L’orchestrateur se justifie au moment où il faut une approbation, une reprise ou plusieurs étapes, pas avant.

Mettre des agents dans les pipelines d’une équipe, avec des budgets, des validations humaines et une revue de code IA qui ne bloque pas tout le monde, c’est le sujet de ma formation IA pour managers techniques.

Questions fréquentes

Comment lancer Claude Code sans terminal interactif ?

Avec le flag -p (ou --print) : claude -p "prompt" lance la boucle d'agent complète, imprime le résultat et rend la main. Avec --output-format json, la sortie devient un objet JSON qu'un script peut lire.

Comment obtenir une réponse JSON validée par un schéma avec Claude Code ?

Combiner --output-format json et --json-schema avec un JSON Schema. L'objet conforme arrive dans le champ structured_output, et le script peut brancher dessus.

Quel est l'équivalent de claude -p pour Codex ?

codex exec. L'option --output-schema prend un fichier JSON Schema et -o écrit le dernier message dans un fichier. Fermer stdin (</dev/null) dans un script, sinon codex exec attend une entrée et ne rend jamais la main.

Peut-on lancer un skill comme /code-review en mode headless ?

Oui : un /nom-du-skill en tête du prompt de claude -p est développé avant l'exécution, et --json-schema s'applique à la réponse. Exception constatée avec Claude Code 2.1.291 : /code-review, commande intégrée, ignore le schéma ; demander la revue en langage naturel rend bien le JSON.

Quand passer de claude -p à un orchestrateur comme IronFlow ?

Quand il faut un déclencheur (webhook, cron), une validation humaine au milieu, des reprises après panne ou un suivi des coûts sur plusieurs étapes. Pour un appel isolé dans un job CI, une ligne de shell suffit.