Shift Left with the Pre-Commit framework

C Armstrong
KPMG UK Engineering
5 min readFeb 6, 2024

I enjoy using git for version control and setting up automated builds and deployments using CI/CD tools such as Jenkins, GitHub and Azure DevOps. The times it gets most frustrating for me are when I forget to run a code scan, linter or some other basic check before pushing my code resulting in basic but frustrating failures on the Git remote and a messy commit history. I’ve used git hooks in the past, but never really embedded them into my workflow properly as I found the process of writing them a little cumbersome.

I recently stumbled on a framework called pre-commit, however which changed that for me…

Git Hooks

For those who don’t know (if you do feel free to jump to the next section), Git hooks are scripts that exist within your .git directory that are designed to allow executing checks at various points in the git workflow. You can easily see the available hooks by running something like the following:

> ls -l .git/hooks

total 128
-rwxr-xr-x 1 user group 478 6 Jun 2023 applypatch-msg.sample
-rwxr-xr-x 1 user group 896 6 Jun 2023 commit-msg.sample
-rwxr-xr-x 1 user group 4726 6 Jun 2023 fsmonitor-watchman.sample
-rwxr-xr-x 1 user group 189 6 Jun 2023 post-update.sample
-rwxr-xr-x 1 user group 424 6 Jun 2023 pre-applypatch.sample
-rwxr-xr-x 1 user group 1643 6 Jun 2023 pre-commit.sample
-rwxr-xr-x 1 user group 416 6 Jun 2023 pre-merge-commit.sample
-rwxr-xr-x 1 user group 1374 6 Jun 2023 pre-push.sample
-rwxr-xr-x 1 user group 4898 6 Jun 2023 pre-rebase.sample
-rwxr-xr-x 1 user group 544 6 Jun 2023 pre-receive.sample
-rwxr-xr-x 1 user group 1492 6 Jun 2023 prepare-commit-msg.sample
-rwxr-xr-x 1 user group 2783 6 Jun 2023 push-to-checkout.sample
-rwxr-xr-x 1 user group 3650 6 Jun 2023 update.sample

Each of these scripts is named after the hook it implements, and all are initially samples, denoted by the .sample suffix — meaning they won’t run.

In order to make a basic example, let me change the pre-commit.sample file to echo a simple “Hello From Git Hook” message, to demonstrate the functionality.

> cat .git/hooks/pre-commit
echo "Hello From Git Hook"

> git commit -am "Test Commit"
Hello From Git Hook
[feature/somefeat abcde01] Test Commit
1 file changed, 1 insertion(+), 2 deletions(-)

As you can see my simple script executed and showed some output. This would allow me to check various things and show statuses, but what about preventing the commit altogether? Lets change the script a little:

> cat .git/hooks/pre-commit
echo "Hello From Git Hook"
exit 1

> git commit -am "Test Fail Commit"
Hello From Git Hook

>

This time I added a non-zero exit code which resulted in the hook failing, and the commit not being performed at all! Using these hooks you can prevent commits from making it into the git database altogether if they don’t pass the necessary checks.

I’ll not go into all of the workflows, but as you can see hooks provide a great way to make sure you enforce some basic controls on your code and find issues before you even push your code! For a full list of client and server side hooks and more detailed descriptions, see https://git-scm.com/docs/githooks#_hooks

The Pre-Commit Framework

So now that we know about git hooks, you could go ahead and write all sorts of wonderful scripts to perform checks for you. The problem is they’ll be local to you unless you devise a mechanism to distribute them and all the fun that comes with it. Fortunately some clever people have done this work and the pre-commit framework does exactly that, as well as promote lots of re-use of code for commonly performed tasks, great!

Step 1: Install it using pip (remember to use a virtual environment to keep things clean):

> pip install pre-commit
> pre-commit --version

pre-commit 3.6.0

Step 2: Create a pre-commit configuration file

> cat .pre-commit-config.yaml

repos:
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
args:
- "src"

This is a very basic file just to show the syntax. I want in this case to run flake8 on the src directory. There are a full list of pre-created hooks to choose from or you can write your own!

Remember to keep .pre-commit-config.yaml committed into git, thereby allowing all of the contributors to use the same set of hooks. If you use the same ones again in your CI workflow then this ensures a seamless transition from local to remote checks and really streamlines your delivery process.

Step 3: Install the hook scripts

> pre-commit install

pre-commit installed at .git/hooks/pre-commit

This will insert the pre-commit script to the pre-commit hook location ready to execute on commit

Step 4: Test it out by doing a manual run, and try a commit

> pre-commit run --all-files

flake8...................................................................Failed
- hook id: flake8
- exit code: 1

path/to/my/python.py:80:19: F541 f-string is missing placeholders

Oh no, I can immediately see my flake8 run shows an issue. I’ll quickly fix that and then

> pre-commit run --all-files

flake8...................................................................Passed

I can also see it execute on commit:

> git commit -am "Add files"

flake8...................................................................Passed
[feature/myfeature abcde01] Add files
1 file changed, 1 insertions(+), 1 deletions(-)

All our checks will now run either on demand with the pre-commit run command, or whenever we try to commit files!

Use cases

There are pre-built hooks available for lots of popular scanning and linting frameworks. You could execute tests or a subset of tests, run static code analysis or prevent credentials from accidentally making it into your commit history.

Remember that infrastructure as code has pushed infrastructure into the discipline of software engineering and checks exist for terraform formatting and security scanning as well as executing tools like infracost to detect changes in the cost of applying your terraform code. You can also create your own hooks.

My only word of warning is to keep pre-commit config lightweight — you don’t want to be waiting for a full build and test suite before every commit…. (or do you!)

--

--