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, and language: system declarations.
  • Pin exact hook revisions using the rev field. Never use floating tags.
  • Pin package versions via additional_dependencies per hook definition.
  • Set fail_fast: false to 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_HOME to a stable path such as /home/vscode/.cache/pre-commit.
  • Map this path to a named Docker volume in devcontainer.json so it survives rebuilds.
  • Export PRE_COMMIT_HOME via remoteEnv so 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] and types: [javascript] filters for targeted execution.
  • Set stages: [pre-commit, pre-push] for granular control across the Git lifecycle.
  • For language: system hooks, verify the binary is on PATH before running pre-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

IssueResolution
Cache invalidation on container rebuildPersist PRE_COMMIT_HOME via a named Docker volume to bypass redundant pip/npm installs.
Cross-language dependency conflictsUse additional_dependencies per hook instead of global container installs. Isolate runtimes via language directives.
language: system hook not foundInstall the binary (golangci-lint, shellcheck, etc.) in the Dockerfile before calling pre-commit install.
Hooks run against unrelated file typesAdd 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.