Wiring pre-commit into the devcontainer lifecycle
This module builds on 2 and 3, not 4
Important reminder: the pre-commit configuration you're wiring in here is the working one from module 3 — not the deliberately broken fixture from module 4, which was only a diagnosis exercise. The project for this module is my-python-script, with the .pre-commit-config.yaml fixed and fully green at the end of module 3 (check-yaml, detect-private-key, black, flake8).
The goal
Until now, activating pre-commit on a project meant typing pre-commit install yourself after cloning the repo — one more manual step, easy to forget, invisible in a README nobody reads. The goal of this module: make pre-commit install run automatically when the container is created, so anyone opening this Dev Container gets active hooks without doing anything extra.
Check for an official Feature first
Same instinct as in module 2: before writing anything by hand, check whether an official Dev Container Feature already covers this need. A search of the official registry (containers.dev/features) for "pre-commit" surfaces several Features, but none under the official ghcr.io/devcontainers/features/ namespace — only third-party implementations maintained by individual contributors, under their own namespaces (e.g. ghcr.io/prulloac/devcontainer-features/pre-commit or ghcr.io/hspaans/devcontainer-features/pre-commit).
These third-party Features may well work fine, but this module doesn't use them: they add a dependency on an external maintainer unaffiliated with Microsoft or the Dev Container Spec project, for a marginal gain over two shell commands in postCreateCommand. Module 2's rule cuts both ways: don't fabricate a Feature reference you haven't verified, but also don't reach for one just because it exists if a simpler, more transparent alternative covers the same need.
The devcontainer.json for my-python-script
This project didn't have a .devcontainer/devcontainer.json yet — you write one now, reusing the skills from module 1 directly:
{
"name": "My Python script",
"image": "mcr.microsoft.com/devcontainers/python:3.12",
"postCreateCommand": "pip install pre-commit && pre-commit install"
}
image
mcr.microsoft.com/devcontainers/python:3.12: an official image with Python 3.12 preinstalled, matching the project's language (unlike module 2, there's no need to add Python via a Feature here — it's native to this base image).
postCreateCommand
Two commands chained with &&:
pip install pre-commit— installs the pre-commit tool itself inside the container (it's not a project dependency, sopre-commitdoesn't need arequirements.txt).pre-commit install— writes the.git/hooks/pre-commitGit hook into the repo. This second command is what actually activates commit interception.
The && guarantees pre-commit install only runs if installing pre-commit succeeded — no silent run in a half-configured state.
pre-commit install needs an already-initialized Git repository (.git/) to know where to write the hook — that's already the case here, since git init was run back in module 3.
No-cache rebuild
To prove the automation actually works — not just because you already ran pre-commit install by hand in an earlier session — start from a completely fresh state. From the command palette: Dev Containers: Rebuild Without Cache and Reopen in Container.
Unlike Rebuild Container, this command forces Docker to rebuild every image layer without reusing anything from the existing cache, and reruns postCreateCommand from a container that has strictly nothing installed beforehand — the closest thing to a brand-new contributor's machine.
The proof: a rule-violating commit, blocked with nothing extra typed
Once the rebuild finishes, open an integrated terminal — without running pre-commit install yourself, the whole point is to check it's no longer necessary. Deliberately reintroduce a defect in app.py:
import os
def greet(name):
x = 1
print( "hello, "+name )
greet('world')
Then attempt the commit:
git add app.py
git commit -m "test violating commit"
If git asks for an identity (Please tell me who you are) instead of running the hooks, that's unrelated to pre-commit: set it once with git config --global user.email "you@example.com" and git config --global user.name "Your Name", then retry the commit.
Real output obtained:
check yaml...............................................................Passed
detect private key.......................................................Passed
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted app.py
All done! ✨ 🍰 ✨
1 file reformatted.
flake8...................................................................Failed
- hook id: flake8
- exit code: 1
app.py:1:1: F401 'os' imported but unused
app.py:5:5: F841 local variable 'x' is assigned to but never used
The commit is rejected (non-zero exit code, no new commit created): the hooks fired automatically the moment git commit ran, without any pre-commit install command typed by hand after the rebuild. That's exactly the behavior you were after — fix app.py as in module 3, retry the commit, and it goes through.
What you just did
You closed the loop between the two threads of this track: the devcontainer mechanism (postCreateCommand, mastered in module 1) now activates the pre-commit configuration (written in module 3) automatically for anyone who opens this project — zero manual steps, zero README to read, and proof from the terminal's real behavior rather than trust. It's the same philosophy as the very first module of the Beginner track: never assume an environment works, prove it with a command.
Check your understanding
Which previous modules does this module build on?
Why does this module use postCreateCommand rather than a Dev Container Feature to install pre-commit?
What's the proof that the integration actually works, with no manual step forgotten?
Want to hear about the next modules?
The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.