Many development teams rely on CI/CD pipelines to validate code quality, security, and compliance. However, it can be frustrating when a pipeline fails because of simple issues that could have been detected locally in seconds, such as formatting errors or invalid configuration files.
Pre-commit helps shift these checks closer to development by allowing developers to run automated validations before committing code. By reusing the same pre-commit configuration in both local environments and CI pipelines, teams can reduce feedback cycles, prevent avoidable pipeline failures, and enforce consistent quality standards across repositories.
This article explains how to use pre-commit effectively in both local development and CI pipelines, including how stages can be used to balance fast developer feedback with more extensive pipeline validations.
What is pre-commit?
Pre-commit is an open-source framework that helps teams automate code quality, formatting, validation, and security checks as part of their development workflow. Despite its name, pre-commit is not limited to the Git pre-commit stage. It provides a flexible framework for managing and executing checks in various Git hook stages as well as through manually triggered executions.
Because pre-commit is tightly integrated with Git, it can efficiently identify changed files and focus validations on relevant repository changes. This makes it particularly suitable for both local development and CI/CD environments.
Pre-commit configurations are typically stored within individual Git repositories. However, organizations often centralize and version common configurations through shared repositories or templates to ensure consistent quality standards across multiple projects.
At the core of pre-commit are so-called hooks. A hook represents an individual check or action that is executed during a specific stage. Hundreds of ready-to-use hooks are available for common use cases such as:
- Validating YAML, JSON, and Markdown files
- Enforcing formatting standards
- Running linters and static code analysis
- Detecting secrets and sensitive information
- Validating infrastructure-as-code configurations
In addition to these community-maintained hooks, teams can create their own custom hooks. Since a hook can execute virtually any command or script, pre-commit offers a high degree of flexibility and can be adapted to the specific requirements of a project or organization.
Why Consistency Matters
One of the biggest advantages of pre-commit is the ability to define checks once and execute them consistently both locally and within CI/CD pipelines. This reduces feedback cycles by catching issues early, often before code is pushed or a pipeline starts.
Using the same validations in both environments helps prevent “works on my machine” situations, reduces avoidable pipeline failures, and ensures that identical quality standards are enforced throughout the development lifecycle. Instead of maintaining separate validation mechanisms, teams benefit from a single source of truth for code quality requirements.
The result is a smoother developer experience, faster feedback, and more predictable pipeline outcomes.
Using Pre-Commit Locally
To use pre-commit locally, a repository must provide a pre-commit configuration, typically through a .pre-commit-config.yaml file. After installing the pre-commit framework, developers can activate the configured hooks for a repository using pre-commit install. Once installed, the configured hooks are executed automatically whenever the corresponding Git hook stage is triggered.
It is important to note that pre-commit only becomes effective after the hooks have been installed locally. Simply having a pre-commit configuration in the repository does not guarantee enforcement. If a developer has not installed the hooks, no local validations will be executed when creating commits.
Using Pre-Commit in CI Pipelines
Pre-commit is not limited to Git hook execution and can be invoked directly from CI/CD pipelines. This allows organizations to reuse the same hook definitions that developers execute locally and integrate them into automated validation workflows.
The most basic execution pattern is to run all hooks configured in the repository: pre-commit run --all-files. For scenarios such as scheduled scans, onboarding existing repositories, or validating newly introduced rules, a full repository scan can be performed.
In most pipeline scenarios, however, it is preferable to validate only changed files as part of the current branch or pull request. Pre-commit supports this through Git references: pre-commit run --from-ref origin/main --to-ref HEAD. This command evaluates only files that differ between the specified revisions, significantly reducing execution time for large repositories.
Using Stages to Differentiate Between Local and Pipeline Validations
While it is beneficial to maintain a single source of truth for validation logic, not every check is equally suitable for local execution. Some validations are resource-intensive, require external tooling, or simply take too long to run on every commit.
Pre-commit addresses this challenge through the concept of stages. Hooks can be assigned to different execution stages, allowing teams to decide which validations should run locally and which should be reserved for CI pipelines.
A common approach is to keep local commit stages lightweight and focused on providing fast feedback. Typical examples include formatting checks, file validation, linting, and basic quality controls. More time-consuming validations can then be assigned to a dedicated stage that is executed only within the CI pipeline.
For local development, developers simply install the configured hooks and continue using Git as usual. The appropriate validations are then executed automatically whenever a commit is created. Within CI pipelines, dedicated stages can be triggered explicitly.
Consider the following example:
pre-commit run \
--hook-stage manual \
--from-ref origin/main \
--to-ref ${SOURCE_COMMITID}
With a hook configuration such as:
- id: checkov
name: Checkov (changed files)
entry: scripts/run-checkov.sh
language: script
verbose: true
files: \.tf$
stages: [pre-commit]
- id: checkov-full
name: Checkov (full scan)
entry: scripts/run-checkov.sh
args: [--all]
language: script
verbose: true
pass_filenames: false
files: \.tf$
stages: [manual]
This configuration causes Checkov to evaluate only modified Terraform files during local commits while performing a full repository scan when executed through the manual stage in a CI pipeline. This approach helps identify findings that may not be visible when validating individual files only, such as issues related to dependencies between Terraform resources and modules.
Limitations
While pre-commit is a powerful framework for standardizing validations across local development and CI/CD pipelines, it is important to understand its limitations and use it appropriately.
One consideration is execution performance. Pre-commit primarily executes hooks sequentially, which can become a bottleneck when numerous or resource-intensive validations are configured. Although some tools perform their own internal parallelization, large-scale validation workloads may be better distributed across dedicated CI pipeline jobs that can run independently and in parallel.
Another challenge is developer adoption. For local validations to be effective, developers must install and activate pre-commit in their local repositories. If the hooks are not installed, the configured checks will not run before commits are created. As a result, organizations should not rely exclusively on local execution and should continue enforcing critical validations within CI pipelines.
Dependencies can also add complexity. Some hooks rely on external tools such as Checkov, Trivy, TFLint, or terraform-docs. Missing or incompatible tool versions may prevent hooks from running successfully, creating additional setup effort for developers. To minimize friction, teams should keep local setup requirements simple, provide clear installation instructions, and consider implementing prerequisite checks for required tools and versions.
A further consideration is configuration governance. Pre-commit configurations are typically stored directly within the repository being validated. As a result, contributors can modify the configuration itself, potentially changing or weakening the validations executed both locally and within CI pipelines. Organizations should therefore ensure that changes to pre-commit configurations are properly reviewed and governed, and that critical quality or security controls are not solely dependent on repository-managed configurations.
Despite these limitations, the benefits of fast feedback, reusable validation logic, and consistent quality standards typically outweigh the additional setup and maintenance effort, making pre-commit a valuable component of modern development workflows.
Conclusion
Pre-commit provides a practical way to establish consistent validations across local development environments and CI/CD pipelines. By maintaining a single source of truth for formatting, quality, security, and compliance checks, teams can reduce feedback cycles, improve developer productivity, and prevent avoidable pipeline failures.
By combining lightweight local validations with stage-specific pipeline checks, organizations can strike a balance between fast developer feedback and comprehensive automated verification. When implemented effectively, pre-commit helps teams improve quality standards while reducing the operational overhead of maintaining separate validation mechanisms for local development and CI/CD environments.