Aller au contenu
Outils medium

pre-commit : bloquer les erreurs avant qu'elles n'atteignent le dépôt

20 min de lecture

pre-commit intercepte vos commits et vérifie automatiquement le code avant qu'il n'atteigne le dépôt. Espaces en fin de ligne, fichiers YAML mal formés, secrets exposés : tout est détecté et souvent corrigé automatiquement. Ce guide couvre l'installation, la configuration et les hooks les plus utiles pour un workflow DevOps.

  • Comprendre le fonctionnement des hooks Git et de pre-commit.
  • Installer pre-commit et l'activer dans un dépôt.
  • Configurer les hooks essentiels (formatage, YAML, secrets).
  • Intégrer pre-commit dans un pipeline CI/CD.
  • Éviter le risque de sécurité des hooks locaux (LOTP).

Sans validation automatique, les problèmes de qualité s'accumulent :

  • Espaces en fin de ligne qui polluent les diffs
  • Fichiers YAML ou JSON mal formés qui cassent les pipelines
  • Secrets (clés API, mots de passe) commités par erreur
  • Code non formaté qui crée des conflits de merge

La détection manuelle est impossible à maintenir. Les revues de code perdent du temps sur des détails triviaux au lieu de se concentrer sur la logique.

pre-commit automatise ces vérifications :

Sans pre-commitAvec pre-commit
Secrets détectés en productionBloqués au commit
YAML invalide → pipeline casséValidé avant push
Formatage incohérent entre devsUniformisé automatiquement
Revue de code sur des détailsFocus sur la logique métier

pre-commit utilise les hooks Git, des scripts exécutés automatiquement par Git à certains moments (pré-commit, pré-push, etc.).

Workflow pre-commit : git add déclenche git commit qui exécute les hooks pre-commit (linter, formatteur, scanner de secrets). Tous doivent passer pour que le commit soit accepté.

Chaque hook est un outil indépendant (linter, formatteur, scanner) configuré dans un fichier .pre-commit-config.yaml.

Version actuelle : 4.6.0 (février 2026)

pre-commit est écrit en Python. L'installation recommandée utilise pipx pour isoler les dépendances.

  1. Installer pipx (si pas déjà fait)

    Fenêtre de terminal
    # Ubuntu/Debian
    sudo apt install pipx
    pipx ensurepath
    # Ou avec pip
    pip install --user pipx
    pipx ensurepath
  2. Installer pre-commit

    Fenêtre de terminal
    pipx install pre-commit

    Sortie attendue :

    done! ✨ 🌟 ✨
    installed package pre-commit 4.6.0, installed using Python 3.12.3
    These apps are now globally available
    - pre-commit
  3. Vérifier l'installation

    Fenêtre de terminal
    pre-commit --version
    # pre-commit 4.6.0

À la racine de votre projet Git, créez .pre-commit-config.yaml :

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=500']
- id: check-merge-conflict
- id: detect-private-key

Chaque entrée définit :

  • repo : URL du dépôt contenant les hooks
  • rev : version (tag Git) à utiliser
  • hooks : liste des hooks à activer, avec options facultatives

Cette commande écrit un script dans .git/hooks/pre-commit. Elle est donc locale à votre clone : chaque personne qui clone le dépôt doit la relancer, le fichier de configuration versionné ne suffit pas.

Fenêtre de terminal
pre-commit install

Sortie attendue :

pre-commit installed at .git/hooks/pre-commit

À partir de maintenant, chaque git commit déclenche les vérifications.

Pour exécuter les hooks sans commiter :

Fenêtre de terminal
# Sur les fichiers modifiés (staging area)
pre-commit run
# Sur tous les fichiers du dépôt
pre-commit run --all-files
# Un hook spécifique
pre-commit run trailing-whitespace --all-files

Les blocs qui suivent s'ajoutent à la liste repos: du même fichier : on n'écrit pas une configuration par famille de hooks. Prenez-les par ordre de rentabilité, en commençant par la validation des fichiers de configuration et la détection de secrets, qui protègent contre les erreurs les plus coûteuses. Chaque dépôt ajouté allonge le premier lancement, le temps que pre-commit construise les environnements correspondants.

Ces hooks se contentent de vérifier la syntaxe : ils ne modifient aucun fichier et signalent un YAML ou un JSON invalide avant qu'il ne casse un pipeline.

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
args: ['--unsafe'] # Permet les tags YAML personnalisés (Ansible, K8s)
- id: check-json
- id: check-toml
- id: check-xml

Les deux hooks sont complémentaires : detect-private-key cherche des en-têtes de clés privées, tandis que gitleaks applique un jeu de règles bien plus large (jetons cloud, clés d'API, chaînes de connexion). Un secret bloqué ici n'aura jamais à être révoqué.

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks

Exemple de détection :

Fenêtre de terminal
# Créer un fichier avec une clé privée
echo "-----BEGIN RSA PRIVATE KEY-----" > secret.pem
git add secret.pem
git commit -m "Add config"

Sortie :

detect private key...................................................Failed
- hook id: detect-private-key
- exit code: 1
Private key found: secret.pem

Le commit est bloqué, le secret ne rejoint jamais le dépôt.

Ces deux hooks réécrivent vos fichiers. Quand ils interviennent, le commit échoue volontairement : les corrections sont appliquées dans votre copie de travail, à vous de les ajouter avec git add puis de relancer le commit.

repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
args: ['--fix']

Black formate le code automatiquement. Ruff détecte et corrige les erreurs de style (remplace flake8, isort, et plus).

Là où check-yaml valide seulement la syntaxe, yamllint contrôle le style : longueur de ligne, indentation, valeurs booléennes ambiguës. Ses règles par défaut sont strictes, d'où le fichier de configuration dédié.

repos:
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: ['-c', '.yamllint.yaml']

Créez .yamllint.yaml pour personnaliser les règles :

extends: default
rules:
line-length:
max: 120
truthy:
check-keys: false

Les hooks Terraform appellent les binaires installés sur la machine (terraform, tflint) et échouent si ceux-ci sont absents du poste ou du runner. Le hook ansible-lint, lui, installe sa propre dépendance Python dans un environnement isolé : rien à préparer, mais un premier lancement plus long.

repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.88.4
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- repo: https://github.com/ansible/ansible-lint
rev: v24.2.1
hooks:
- id: ansible-lint

hadolint analyse les Dockerfile et signale les constructions qui produisent des images volumineuses ou peu sûres, comme une image de base non versionnée ou un apt-get sans nettoyage du cache.

repos:
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint

Voici une configuration DevOps complète, à copier telle quelle dans un dépôt d'infrastructure ou de scripts. Elle couvre l'hygiène de fichiers, la sécurité, le YAML, le Markdown et le shell, sans dépendre d'un langage applicatif particulier. Les numéros de version indiqués dans rev: sont ceux validés à la rédaction : lancez pre-commit autoupdate après l'avoir copiée pour les rafraîchir.

.pre-commit-config.yaml
repos:
# Vérifications de base
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: ['--unsafe']
- id: check-json
- id: check-added-large-files
args: ['--maxkb=500']
- id: check-merge-conflict
- id: detect-private-key
- id: check-case-conflict
- id: mixed-line-ending
args: ['--fix=lf']
# Sécurité
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
# YAML
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
# Markdown
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.39.0
hooks:
- id: markdownlint
args: ['--fix']
# Shell
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck

Les hooks utilisent des versions fixes (rev:). Pour les mettre à jour :

Fenêtre de terminal
pre-commit autoupdate

Sortie exemple :

[https://github.com/pre-commit/pre-commit-hooks] updating v5.0.0 -> v6.0.0
[https://github.com/gitleaks/gitleaks] updating v8.18.2 -> v8.30.0

Commitez ensuite le fichier .pre-commit-config.yaml mis à jour.

Exécutez pre-commit dans votre pipeline pour détecter les commits qui auraient contourné les hooks locaux. Un git commit --no-verify sur un poste de développement passe inaperçu autrement, tout comme un dépôt cloné par quelqu'un qui n'a jamais lancé pre-commit install. La CI devient donc le filet de sécurité qui rend la règle réellement opposable, et non plus une convention d'équipe.

Les actions sont épinglées par empreinte de commit avec le tag en commentaire : un tag Git peut être redéplacé vers un autre code, pas une empreinte. Le workflow ne demande aucun droit par défaut, chaque job réclamant le strict nécessaire.

.github/workflows/pre-commit.yml
name: pre-commit
on:
push:
pull_request:
permissions: {}
jobs:
pre-commit:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1

La version de pre-commit est fixée explicitement : sans cela, une nouvelle version publiée entre deux exécutions peut faire échouer un pipeline qui passait la veille, sans aucun changement de code.

.gitlab-ci.yml
pre-commit:
image: python:3.12@sha256:7ad6d21a25a94b2c00e685e82c2fd298de814353d9ee0e3f7f2cd4fca063df60
stage: lint
before_script:
- pip install pre-commit==4.6.0
script:
- pre-commit run --all-files --show-diff-on-failure

En CI, on ne travaille pas sur une zone de staging Git : --all-files est donc indispensable, sans quoi aucun fichier n'est analysé. Les deux autres options servent surtout au confort de diagnostic dans les journaux du pipeline.

OptionDescription
--all-filesVérifie tous les fichiers, pas seulement les modifiés
--show-diff-on-failureAffiche le diff si un hook modifie des fichiers
--fail-fastArrête dès le premier échec

Sécurité : pre-commit dans le référentiel LOTP

Section intitulée « Sécurité : pre-commit dans le référentiel LOTP »

pre-commit est un outil de qualité de code, mais son mécanisme d'exécution en fait aussi un outil référencé dans LOTP (Living Off The Pipeline), la base de connaissances qui catalogue les fonctionnalités légitimes d'outils de pipeline exploitables à des fins malveillantes.

Le risque repose sur les hooks locaux (repo: local) avec language: system. Ce type de hook exécute directement une commande shell sur la machine, sans aucun sandboxing, sans isolation, avec les droits de l'utilisateur courant (ou du runner CI/CD).

Ce mécanisme fonctionne parce que pre-commit run charge et exécute automatiquement les hooks définis dans .pre-commit-config.yaml. Quand le hook utilise language: system, pre-commit passe directement le champ entry au shell système, c'est exactement le même mécanisme qu'un Makefile ou un package.json avec des scripts lifecycle.

L'attaque la plus courante est la Poisoned Pipeline Execution (PPE) :

  1. Un attaquant ouvre une PR qui modifie .pre-commit-config.yaml
  2. Le pipeline CI exécute pre-commit run --all-files après checkout du code de la PR
  3. Le hook malveillant s'exécute et exfiltre les secrets (GITHUB_TOKEN, clés API, credentials cloud)

Variante plus subtile : l'attaquant ajoute un hook légitime et un hook malveillant dans la même PR. Le nom (name: lint check) paraît inoffensif mais le champ entry contient du code malveillant.

Aucune de ces mesures ne suffit seule : la première protège le fichier, la deuxième protège le pipeline, les deux dernières réduisent la surface d'exécution. Appliquez au minimum les deux premières sur tout dépôt public.

  1. Protégez .pre-commit-config.yaml avec CODEOWNERS pour qu'une revue soit obligatoire avant toute modification :

    # .github/CODEOWNERS
    .pre-commit-config.yaml @security-team
  2. Ne lancez pas pre-commit run automatiquement sur les PRs de forks. Vérifiez que .pre-commit-config.yaml n'a pas été modifié avant d'exécuter les hooks en CI :

    Fenêtre de terminal
    # Vérifier si le fichier a changé dans la PR
    if git diff --name-only origin/main...HEAD | grep -q '.pre-commit-config.yaml'; then
    echo "⚠️ .pre-commit-config.yaml modifié, revue manuelle requise"
    exit 1
    fi
  3. Évitez language: system dans vos propres hooks sauf nécessité documentée. Préférez language: python, language: node ou language: docker qui offrent une isolation minimale.

  4. Auditez régulièrement les hooks locaux et les dépôts référencés. Vérifiez que les rev: pointent vers des tags signés de dépôts de confiance.

Au quotidien, trois commandes suffisent : install une fois par clone, run --all-files après avoir ajouté un hook, et autoupdate de temps en temps. Les autres servent surtout au dépannage, notamment clean quand un environnement de hook s'est corrompu et que le message d'erreur devient incompréhensible.

CommandeDescription
pre-commit installActive les hooks dans le dépôt
pre-commit runExécute sur les fichiers stagés
pre-commit run --all-filesExécute sur tout le dépôt
pre-commit run <hook-id>Exécute un hook spécifique
pre-commit autoupdateMet à jour les versions des hooks
pre-commit cleanSupprime le cache des environnements
pre-commit gcNettoie les repos non utilisés
pre-commit uninstallDésactive les hooks
pre-commit validate-configValide la syntaxe du fichier config
pre-commit try-repo <url>Teste un hook sans l'installer

Dans de rares cas, vous pouvez avoir besoin de passer outre les hooks :

Fenêtre de terminal
# Ignorer tous les hooks
git commit --no-verify -m "WIP: work in progress"
# Ignorer un hook spécifique (via variable d'environnement)
SKIP=trailing-whitespace git commit -m "Keep trailing spaces"

Par défaut, les hooks s'exécutent au pre-commit. Vous pouvez configurer d'autres moments :

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
stages: [pre-commit, pre-push]
StageMomentCas d'usage
pre-commitAvant le commitFormatage, linting rapide
pre-pushAvant le pushTests longs, scans de sécurité
commit-msgAprès saisie du messageValidation format commit
post-checkoutAprès checkoutMise à jour dépendances

Pour activer plusieurs stages :

Fenêtre de terminal
pre-commit install --hook-type pre-commit --hook-type pre-push

La plupart des blocages tiennent à l'environnement plutôt qu'à la configuration : pre-commit construit un environnement isolé par dépôt de hooks, dans un cache situé sous ~/.cache/pre-commit. C'est ce qui explique la lenteur du premier lancement, et pourquoi une différence de version entre poste et CI produit des résultats divergents.

SymptômeCauseSolution
"command not found: pre-commit"pipx pas dans le PATHpipx ensurepath puis relancer le shell
Hook très lent au premier runTéléchargement des environnementsNormal, le cache accélère les runs suivants
Erreur "not a git repository"Pas de dépôt Gitgit init d'abord
Hook échoue en CI mais pas en localVersions différentesUtiliser pre-commit autoupdate
"No .pre-commit-config.yaml"Fichier absentCréer le fichier à la racine

Les questions ci-dessous reviennent régulièrement lors de la mise en place de pre-commit dans une équipe, en particulier sur l'adoption par tous les développeurs et sur l'articulation avec la CI.

  1. pre-commit = garde-fou automatique, Vérifie le code avant qu'il n'atteigne le dépôt
  2. Installation : pipx install pre-commit puis pre-commit install dans chaque projet
  3. Configuration : fichier .pre-commit-config.yaml à la racine
  4. Mise à jour : pre-commit autoupdate pour utiliser les dernières versions
  5. CI/CD : ajouter pre-commit run --all-files dans le pipeline
  6. Hooks essentiels : trailing-whitespace, check-yaml, detect-private-key, gitleaks
  7. Contournement : git commit --no-verify (à éviter)
  8. Les hooks locaux language: system sont référencés dans le référentiel LOTP : protégez .pre-commit-config.yaml via CODEOWNERS et ne lancez pas pre-commit run automatiquement sur les PRs de forks.

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