Caching pre-commit Environments on a Named Volume

The pre-commit framework builds an isolated environment per hook, and rebuilding those on every container is slow. This page caches pre-commit's environment directory on a named volume, so hook environments persist across rebuilds and commits stay fast.

To see why this matters, it helps to understand what pre-commit actually does under the hood. For each hook you declare — a linter, a formatter, a secrets scanner — the framework creates a fully isolated environment: a dedicated Python virtualenv, a Node install, or a Ruby gem set, whatever the hook needs, so hooks never conflict with each other or with your project's dependencies. That isolation is a feature, but it has a cost: building those environments means creating virtualenvs and installing packages, which on a cold container can take tens of seconds or more for a multi-hook, multi-language config. In a devcontainer that cost is paid on every rebuild, because the environments live in the container's ephemeral storage and vanish when the container is recreated.

The fix is the same caching discipline applied to every other expensive, reproducible artifact in a devcontainer: identify the directory that holds the built environments, put it on a named volume so it survives rebuilds, and warm it once so the first commit after a rebuild is fast rather than slow. Because pre-commit keys each hook environment on the hook's pinned revision, the cache stays valid as long as your .pre-commit-config.yaml is unchanged, and repopulates automatically when you bump a hook. The result is that hook environments are built once per hook version and reused thereafter, so the commit-time check stays fast enough that developers never feel tempted to bypass it with --no-verify.

Prerequisites

You need a pre-commit-based devcontainer — the framework installed and its hooks wired into postCreateCommand after the workspace mount, as the parent guide describes. This caching page assumes that foundation is already in place; it adds the volume that makes the hook environments persist. If your hooks are not yet installing correctly, fix that first, because caching a broken setup only makes the broken state reach faster.

  • The pre-commit framework installed and wired in postCreate.
  • A pinned .pre-commit-config.yaml.
  • A named volume for the pre-commit cache.

The one detail worth confirming before you start is the exact cache path, because it depends on the remoteUser's home directory. pre-commit stores its environments under $HOME/.cache/pre-commit by default, which for a vscode user is /home/vscode/.cache/pre-commit. Pinning the hook revisions in .pre-commit-config.yaml is also a prerequisite for the cache to behave predictably: because the cache is keyed on those revisions, unpinned hooks that float to new versions will repeatedly invalidate the cache and rebuild environments, defeating the point.

Cache prerequisitesYou need the cache dir, a volume, a warm step, and a reuse check.Cache dir~/.cache/pre-commitVolumemount itinstall--install-hookswarm itVerifyreuse

Step-by-Step Implementation

  1. Mount the pre-commit cache on a volume.
{
  "mounts": ["source=devcontainer-precommit,target=/home/vscode/.cache/pre-commit,type=volume"],
  "remoteUser": "vscode"
}

The target must match the real cache directory for your remoteUser exactly — here /home/vscode/.cache/pre-commit for the vscode user. This is the step most likely to silently fail: a volume mounted one directory too high, or at the path for the wrong user, caches nothing while appearing configured, because the volume and the directory pre-commit actually writes to never coincide. If you use a different remoteUser, derive the target from that user's home directory rather than copying this path verbatim.

  1. Install hooks (and their envs) on create.
{ "postCreateCommand": "pip install pre-commit && pre-commit install --install-hooks" }

The --install-hooks flag is what warms the cache. Plain pre-commit install writes the git hook scripts but defers building each hook's environment until the first commit, so without --install-hooks the first commit after a rebuild still pays the full environment-build cost. --install-hooks builds all the hook environments during container creation instead, where the cost overlaps with other setup and the developer is not waiting on a commit. Because this runs on every create and the environments land on the mounted volume, the build happens once and every subsequent rebuild finds them already present.

  1. Confirm the cache is used.
pre-commit run --all-files   # reuses cached hook environments

Running the hooks once against all files both validates that the config is sound and confirms the environments are being reused rather than rebuilt. On a warm cache this completes quickly because no environment-building happens; on a cold one you will see pre-commit report that it is installing each hook's environment. Watching for that "installing environment" output is the simplest way to tell whether the cache is actually working — its absence on a rebuild means the volume is doing its job. Note that --all-files runs the hooks against the entire repository, which is more work than a normal staged-files commit does; use it here as a one-off validation of the config and cache, not as the everyday path, since the commit-time run should stay scoped to changed files for speed.

  1. Verify a rebuild doesn't rebuild hook envs.

The definitive test is to rebuild the container from scratch and time the first pre-commit run. With the volume working, the environments are already present, so the run is fast and reports no environment installation. If the first run after a rebuild is slow and shows pre-commit rebuilding environments, the cache is not being used — almost always because the volume target does not match the real cache path, or the volume is owned by root and the remoteUser cannot read it. Catching this in a deliberate rebuild test is far better than discovering it as vague "commits feel slow" complaints weeks later.

pre-commit cache anatomyCaching the per-hook environment directory on a volume keeps commits fast across rebuilds.Hook envsone per hook, isolatedCache dir~/.cache/pre-commitNamed volumesurvives rebuildsResultfast commits

Common Pitfalls

Slow commits come from rebuilding hook environments each container — and the two most common reasons the cache fails to prevent that are a path mismatch and an ownership problem. Both are easy to overlook because the setup looks correct: the volume is declared, the build succeeds, hooks run. The triage below distinguishes them, but the underlying question to ask whenever commits are slow after a rebuild is "is pre-commit finding its cached environments, or rebuilding them?" — and the answer is always visible in whether pre-commit run reports installing environments.

The ownership pitfall is the sneakier of the two and stems from the non-root user discipline. A named volume may be created root-owned, while the container runs as the unprivileged remoteUser, so pre-commit cannot write (or sometimes read) the cache directory and quietly rebuilds environments in a location it can write instead. The fix is to ensure the cache directory is owned by the remoteUser, typically via a chown in postCreateCommand before the pre-commit install step. This is the same ownership consideration that affects every cache volume under a non-root user, and it is worth handling proactively rather than diagnosing after the fact.

Cache triageA triage path from rebuilding hook envs to a warm, cached pre-commit.Do hook envs rebuild each container?YESCache the dir on a volumeCan the user write the cache?YESHooks pinned + warmedFast, cached hooks

SymptomRoot CauseRemediation
Hook envs rebuild each containerCache dir not on a volumeMount ~/.cache/pre-commit on a volume
Permission denied writing cacheVolume owned by rootchown the cache dir to the user
Cache present but ignoredWrong cache pathTarget the exact pre-commit cache dir
Commits still slowHooks run on all filesRely on staged-file runs

Conclusion

pre-commit's per-hook environments are the slow part; cache them on a named volume and they persist across rebuilds. Warm them in postCreateCommand, and commits stay fast enough that developers never reach for --no-verify.

The value of this caching grows with the number of hooks and languages in your config, which is why polyglot repositories benefit most. A single Python linter builds one environment; a config with Python, JavaScript, and Ruby hooks builds three, each with its own install cost, and rebuilding all of them on every container recreate quickly becomes the slowest part of getting back to work. Caching the environment directory amortizes that across every rebuild, so a heavy multi-language hook set is built once per version and reused thereafter — turning what could be a minute of setup on each rebuild into a near-instant reuse.

Framed more broadly, this is the same principle that governs the base-image layer cache, the language dependency caches, and the extension cache: the expensive, reproducible-from-source artifact goes on a volume, and the pinned identity (here, the hook revisions) keeps the cache valid. pre-commit's environments are simply another such artifact, and treating them the same way keeps the commit-time check — the whole reason the hooks exist — fast enough that it remains a helpful nudge rather than a toll booth developers route around.

Cache and keep fastCaching hook environments plus staged-file runs keeps commits fast.Cache~/.cache/pre-commitOn a named volumeWarmed on createKeep fastStaged-file runsPinned hooksHeavy checks in CI

FAQ

What exactly is pre-commit caching? The pre-commit framework creates an isolated environment per hook (a Python venv, a Node install, etc.) under ~/.cache/pre-commit. Caching that directory on a named volume means those environments persist across container rebuilds instead of being rebuilt each time. Each hook's environment is keyed on the hook's pinned revision, so the cache holds one built environment per hook version, and pre-commit looks it up rather than rebuilding whenever that version is requested again. The cache is purely the built environments — the installed tools each hook needs to run — not the results of running the hooks, which are always recomputed against your current files.

Why do my commits get slow after a rebuild? Because a fresh container has no hook environments, so the first run rebuilds them all. Mount the cache directory on a volume and warm it in postCreateCommand, and rebuilds reuse the environments, keeping commit time low. The slowness is concentrated in that first post-rebuild invocation, which is exactly why warming with --install-hooks during container creation helps so much: it moves the cost off the developer's first commit and onto the create step, where it overlaps with other setup. Without the volume, that cost recurs on every rebuild; with it, the environments survive and the first commit is as fast as any other.

Does caching affect which checks run? No — the cache only holds the hook environments, not results. Your pinned .pre-commit-config.yaml still determines which hooks run and on which files. Caching purely removes the environment-build cost, so behaviour is unchanged. This separation is what makes the cache safe: it accelerates how the hooks run without touching what they check, so you never trade correctness for speed.

What happens when I bump a hook version? The cache repopulates automatically for that hook. Because pre-commit keys each environment on the hook's pinned revision, changing a rev in .pre-commit-config.yaml produces a new key, so pre-commit builds a fresh environment for the new version on the next run while the old one remains cached but unused. This means a version bump is a deliberate, reviewable change that costs one environment rebuild — exactly the behavior you want, since the cache should reflect the versions you currently pin, not preserve an old one after you intended to move.

Should I cache this in CI too? Yes, and it is just as valuable there. CI runs pre-commit against the same pinned config, so caching the environment directory between CI runs (using your CI provider's caching keyed on the config hash) avoids rebuilding hook environments on every pipeline run. Because the local devcontainer and CI use the same .pre-commit-config.yaml, the same caching principle applies in both places — the environments are built once and reused, keeping both the commit-time check and the CI backstop fast.

Can multiple projects share one pre-commit cache? They can, and it is often efficient. pre-commit's cache is content-addressed by hook and version, so two projects that pin the same version of the same hook will share the same built environment within one cache directory. A named volume shared across projects with overlapping hook sets means the second project finds most environments already built. The only caveat is that a shared cache accumulates environments for every hook version any project has used; that is usually harmless, but if you want each project's cache cleanly matched to its own pins, a per-project volume keeps them separate. For teams standardizing on one hook set, sharing the cache maximizes reuse.

Does the cache ever need clearing? Rarely. Because environments are keyed on hook versions, stale versions simply stop being referenced when you bump a pin, and they sit unused rather than causing problems. The pre-commit gc command prunes those unreferenced environments if the cache grows large, and pre-commit clean wipes it entirely to force a full rebuild — useful when diagnosing a suspected cache corruption. In normal use the cache is safe to leave alone across rebuilds, which is precisely what makes putting it on a persistent volume the right call.