Native hooks vs pre-commit-managed hooks
What you're going to do
This module is short and depends directly on the previous one: you'll need the my-pre-commit-project repo created in module 5, with pre-commit install already run inside it. You'll open the script that command generated, understand that it doesn't come from you, then briefly compare it to a hand-written native git hook.
Opening the generated hook
Inside the my-pre-commit-project folder, open the file generated in step 4 of the previous module:
cat .git/hooks/pre-commit
You should see a script close to this one (the exact content may vary slightly depending on the pre-commit version installed):
#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03
# start templated
INSTALL_PYTHON=/path/to/bin/python
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated
HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")
if [ -x "$INSTALL_PYTHON" ]; then
exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
exec pre-commit "${ARGS[@]}"
else
echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2
exit 1
fi
The comment at the top of the file is explicit: File generated by pre-commit. You didn't write a single line of this script. What it does, concretely: it locates a Python interpreter (or, failing that, the pre-commit executable available in your PATH), then hands off all the real work to it by passing hook-impl and the path to .pre-commit-config.yaml. It's that delegated call which then reads your configuration, resolves the trailing-whitespace hook, and runs it — exactly what you observed in module 5.
In other words: .git/hooks/pre-commit is not the hook that checks for trailing whitespace. It's a simple dispatcher — a switchboard — that hands control over to pre-commit, which in turn reads your versioned configuration to know what to actually run.
What Git's official documentation says about native hooks
According to Git's official documentation, the pre-commit hook is a script that git automatically invokes right before finalizing a commit; if that script exits with a non-zero status, git aborts the commit. That's exactly the mechanism you triggered in module 5 — except that instead of a hand-written script, it's the dispatcher generated by pre-commit occupying that spot.
Git looks for this script in the repo's .git/hooks folder by default. This folder is not versioned: it's never pushed or cloned along with the rest of the repo, which is why everyone needs to rerun pre-commit install after cloning a project that uses pre-commit — without that, .git/hooks/pre-commit simply doesn't exist for them.
Git also offers the core.hooksPath configuration variable, which lets you point to a different hooks folder — potentially shared and versioned, outside of .git/hooks. This is an alternative way to manage native hooks across a team, but it's not the mechanism pre-commit uses: pre-commit install writes into the repo's default hooks folder (.git/hooks). If core.hooksPath already points elsewhere, pre-commit doesn't guess and doesn't silently write there instead — it refuses to install, with an explicit error (Cowardly refusing to install hooks with 'core.hooksPath' set.). A rare situation on a first install, but good to know if you ever run into that message.
A hand-written native hook, for contrast
If you hadn't used pre-commit, you could have written a script yourself directly inside .git/hooks/pre-commit — for example, a few lines of shell that grep the staged content for the word TODO and refuse the commit if it finds one. That would work, in the sense that git would invoke it exactly the same way. But that hook would stay specific to your machine, unversioned, unshared with the team, and with no update mechanism — every improvement would have to be manually copied to each teammate.
That's precisely what pre-commit adds on top of git's native mechanism: a versioned configuration (.pre-commit-config.yaml, committed with the code), reusable hooks maintained by others (like trailing-whitespace), and a single command (pre-commit install) so that everyone who clones the project activates the same behavior.
What you just did
You opened the script generated by pre-commit install and confirmed, text in hand, that it does nothing more than delegate to pre-commit — it's neither a hook you wrote nor magic: it's a perfectly ordinary native git hook from git's point of view, one that happens to point to an external tool rather than containing the verification logic directly. You now know how to place the two layers: git's hook mechanism, native and documented since forever, and pre-commit, which builds on top of it to make checks versionable and shareable across teammates.
This module closes out the track: you've gone from zero exposure to Dev Containers and pre-commit to a practical understanding of both — able to open and read a Dev Container, and to install, configure, and trigger pre-commit with full knowledge of what's happening underneath.
Check your understanding
The .git/hooks/pre-commit file generated in the previous module — what exactly is it?
According to Git's official documentation, what determines where git looks for the pre-commit hook to run before a commit?
Do a fully hand-written native git hook (without pre-commit) and the hook generated by pre-commit install share the same entry point as far as git is concerned?
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.