Configuring pre-commit in a multi-language repo
Introduction
Deploy deterministic, cross-language pre-commit hooks inside DevContainers without host dependency drift. This guide provides a configuration blueprint for isolating runtimes, sharing caches, and enforcing parity across Python, Node.js, and Go toolchains. For foundational architecture patterns, consult Customization & Developer Toolchain Integration.
Sections
1. Deterministic Runtime Mapping
Map each language’s linter and formatter to isolated container runtimes. This prevents dependency collisions across heterogeneous stacks. Avoid system-wide package installations. Rely exclusively on container-managed binaries to guarantee reproducible environments across all developer machines.
Implementation requires explicit language declarations in .pre-commit-config.yaml. Pin exact hook repository revisions and use additional_dependencies for per-hook package versions. Set fail_fast: false to enable full-scan reporting across all tracked files so a single failure does not mask downstream issues in other languages.
Implementation Steps:
- Define hooks with explicit
language: python,language: node, andlanguage: systemdeclarations. - Pin exact hook revisions using the
revfield. Never use floating tags. - Pin package versions via
additional_dependenciesper hook definition. - Set
fail_fast: falseto allow comprehensive reporting across heterogeneous stacks.
2. Shared Cache & Volume Mount Strategy
Prevent cache thrashing and redundant package installations by mounting a persistent volume to $PRE_COMMIT_HOME. This strategy ensures deterministic execution speeds across container rebuilds. Align this configuration with Pre-commit Hook Configuration for Containerized Workflows for optimized mount paths and CI parity.
Implementation Steps:
- Set
PRE_COMMIT_HOMEto a stable path such as/home/vscode/.cache/pre-commit. - Map this path to a named Docker volume in
devcontainer.jsonso it survives rebuilds. - Export
PRE_COMMIT_HOMEviaremoteEnvso all lifecycle scripts and interactive terminals see the same value.
3. Hook Execution Isolation
Use types and files regex filters to route files to their correct hooks. This eliminates cross-contamination in large monorepos and prevents the Go linter from running against Python files.
For Go hooks using language: system, the binary (golangci-lint) must be present in the container’s PATH. Install it in the Dockerfile or via a devcontainer.json feature rather than relying on pre-commit to compile it.
Implementation Steps:
- Set strict
types: [python]andtypes: [javascript]filters for targeted execution. - Set
stages: [pre-commit, pre-push]for granular control across the Git lifecycle. - For
language: systemhooks, verify the binary is onPATHbefore runningpre-commit install.
Code
.pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.56.0
hooks:
- id: eslint
types: [javascript, jsx, ts, tsx]
additional_dependencies:
- eslint@8.56.0
- eslint-config-prettier@9.1.0
- repo: https://github.com/psf/black
rev: 24.3.0
hooks:
- id: black
language_version: python3.11
types: [python]
- repo: local
hooks:
- id: golangci-lint
name: golangci-lint
entry: golangci-lint run
language: system
types: [go]
pass_filenames: false
fail_fast: false
devcontainer.json
{
"name": "Multi-Language DevContainer",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"mounts": [
{
"source": "pre-commit-cache",
"target": "/home/vscode/.cache/pre-commit",
"type": "volume"
}
],
"remoteEnv": {
"PRE_COMMIT_HOME": "/home/vscode/.cache/pre-commit"
},
"postCreateCommand": "pip install pre-commit && pre-commit install --install-hooks"
}
Common Pitfalls
| Issue | Resolution |
|---|---|
| Cache invalidation on container rebuild | Persist PRE_COMMIT_HOME via a named Docker volume to bypass redundant pip/npm installs. |
| Cross-language dependency conflicts | Use additional_dependencies per hook instead of global container installs. Isolate runtimes via language directives. |
language: system hook not found | Install the binary (golangci-lint, shellcheck, etc.) in the Dockerfile before calling pre-commit install. |
| Hooks run against unrelated file types | Add types or files filters to restrict each hook to its target language. |
Conclusion
The combination of explicit rev pinning, additional_dependencies for per-hook packages, and a persistent PRE_COMMIT_HOME volume delivers sub-second hook initialization after the first run. The language: system pattern for Go hooks is the most common source of failures — always verify the binary is present on PATH before committing.
FAQ
How do I prevent pre-commit from reinstalling dependencies on every container start?
Mount a persistent volume to PRE_COMMIT_HOME and pin exact versions in additional_dependencies. This bypasses redundant pip/npm installs and ensures fast hook initialization.
Can I run pre-commit hooks in parallel across different language runtimes?
Pre-commit runs hooks sequentially by default within each repository. Use multiple repo entries to separate hook groups. For parallel execution in CI, use pre-commit run --all-files across matrix jobs rather than relying on pre-commit’s internal concurrency.
What is the fastest way to validate hook parity between DevContainer and CI?
Export PRE_COMMIT_HOME to a shared cache path and run pre-commit run --all-files --show-diff-on-failure in both environments. Identical outputs confirm deterministic execution.