pre-commit: How to create hooks for unsupported tools

The pre-commit framework lists hundreds of hook repositories on its hooks page. You can drop these into your configuration file and get a tool running in seconds. But there are many more tools out there that you might want to use, which you can run with custom configuration.
Custom hooks use the special local repository name, meaning they’re local to your project. Then you need to configure the hook with the fields listed in the docs, including language which defines how to install the tool. Let’s look at the options for language and a couple of examples setting up different tools.
Languages, lawful and chaotic
pre-commit hooks use a “language”, selecting from the supported list.
Most of these represent a programming language with an associated package manager, for which pre-commit will manage an isolated environment. For example, the python language creates a Python virtual environment directory, installing dependencies with pip.
But some are more like “pseudo-languages”, using some other method to run the hook. For running arbitrary tools, there is the system language, which can run any program.
It’s easiest to use the managed programming languages, since pre-commit can handle isolation and version management for you. But if needs must, you can use system, with the drawback that your team will need to install and update the relevant tool by hand.
The following examples cover running a tool in a fully managed lanaguage, node (Node.js), and then a pre-installed tool with system.
node hook: stylelint, a CSS linter
stylelint is a CSS linter that can help you write valid, consistent CSS. At time of writing its repository does not have a pre-commit hooks file. But since it is available on npm, you can configure pre-commit to install and run it with a local node hook.
Here’s an example hook for running stylelint:
repos:
- repo: local
hooks:
- id: stylelint
name: stylelint CSS linter
language: node
additional_dependencies:
- stylelint@14.16.1
- stylelint-config-standard@29.0.0
entry: stylelint
args: [--formatter, unix, --fix]
types_or: [css]
exclude: '.*\.min\.css'
A thorough dissection:
repo: localcontains custom hook definitions.idis the hook’s identifier, which you can use to run the hook on the commandline.nameis the human-readable name that pre-commit displays whilst running the hook.language: nodeselects the Node.js language. pre-commit will download Node.js and use it to set up an isolated environment for the hook.additional_dependenciesis a list ofnpmpackage specifiers to install. As a local hook, the Node.js environment will be empty unless we declare additional dependencies. Here we list stylelint, plus stylelint-config-standard to configure some extra linting rules.You should always specify versions in
additional_dependencies, to ensure that pre-commit uses the same versions everywhere that it runs. Otherwise, new environments can pick up new versions, leading to surprise failures.Beware: pre-commit’s
autoupdatecommand only works with Git repositories, so it does not update versions inadditional_dependencies. If you want to keep such a tool up to date, you’ll need to manually update these version numbers.entryis the command to run. pre-commit passes it filenames as arguments (unless you setpass_filenames: false).argsdefines extra arguments to pass to the command. In this case, we’re passing two options to the stylelint CLI:--formatter unixwhich uses “Unix” style output formatting, which fits in with pre-commit.--fixto apply automatic fixes where possible.
types_orselects files matching thecss“tag”, which means files with a.cssextension. This comes from theidentifytool, as covered in the pre-commit docs.You can add other tags here to match multiple types. For example, you could add the
scsstag to match SCSS (Sass) files. These use an extended version of CSS that stylelint also supports.excludeis a regular expression pattern that deselects some files. In this case the pattern matches files ending.min.css, an extension for minified CSS files. Such files are the outputs of a build process, so inappropriate for linting, since you can’t edit them.
That’s it for configuring pre-commit. stylelint also needs a configuration file. To use the stylelint-config-standard package as-is, drop this JSON into .stylelintrc in the repository root:
{
"extends": "stylelint-config-standard"
}
After adding any new hook, check it against all applicable files:
$ pre-commit run stylelint --all-files
stylelint CSS linter.....................................................Failed
- hook id: stylelint
- exit code: 2
- files were modified by this hook
/.../example.css:3:16: Unexpected invalid hex color "#0" (color-no-invalid-hex) [error]
1 problem (1 error, 0 warnings)
In this example, stylelint found several issues, some of which it auto-fixed, and one which it reported as an error. The auto-fixes standardize the formatting:
@@ -1,4 +1,3 @@
-:root
-{
- --darkest: #0;
+:root {
+ --darkest: #0;
}
The error covers an invalid hex colour code, #0, which needs manually correcting:
@@ -1,3 +1,3 @@
:root {
- --darkest: #0;
+ --darkest: #000;
}
After those fixes, the hook passes:
$ pre-commit run stylelint --all-files
stylelint CSS linter.....................................................Passed
Bri-lint-iant.
When adding a hook, you can commit the hook and initial fixes in one. This way, pre-commit will always pass, no matter which commit you are on.
system hook: jpegoptim, a JPEG optimizer
jpegoptim is a JPEG optimization tool. When passed a JPEG, it optimizes it to shrink file size without affecting quality, an easy way of saving disk space and bandwidth.
Since jpegoptim is written in C, there is no platform-independent package manager to install it with. So, you need to use the system language to run it in pre-commit, and rely on setting it up manually.
Here’s an example hook for running jpegoptim:
repos:
- repo: local
hooks:
- id: jpegoptim
name: Optimize JPEGs
language: system
entry: jpegoptim
types_or: [jpeg]
The fields similar to above, though fewer are required. Some notes:
language: systemwon’t set up any isolated environment. Therefore the command inentrymust be available on your search path (thePATHenvironment variable).This normally means installing the tool globally on your system. For example, with macOS Homebrew you can install the jpegoptim formula to get the
jpegoptimcommand.entryis justjpegoptimsince it optimizes images in-place by default.The
jpegtype tag selects files ending in.jpegand.jpg. You can check this in the identify source.
After adding the hook, you can again optimize all JPEGs in your repository with:
$ pre-commit run jpegoptim --all-files
Let’s see what the hook looks like when it runs during commit. Add a new JPEG and try to commit:
$ git status -s
A donut.jpeg
$ git commit -m "Mmm, donut."
Optimize JPEGs...........................................................Failed
- hook id: jpegoptim
- files were modified by this hook
donut.jpeg 550x541 24bit N Exif IPTC JFIF [OK] 66520 --> 56017 bytes (15.79%), optimized.
jpegoptim found 15% savings, sweet. Because the file changed, pre-commit blocked the commit.
Check it looks fine, and then continue:
$ open donut.jpeg
$ git status -s
Found existing alias for "git status". You should use: "gst"
AM donut.jpeg
$ git add donut.jpeg
Found existing alias for "git add". You should use: "ga"
$ git commit -m "Mmm, donut."
Optimize JPEGs...........................................................Passed
[main cc4e434] Mmm, donut.
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 donut.jpeg
And that’s how to run a system tool within pre-commit.
Push your hook definitions upstream
You can adapt custom configuration for a tool in a supported language into a .pre-commit-hooks.yaml file in the repository. Adding configuration into a tool’s repository makes it more discoverable and allows pre-commit autoupdate to keep you on the latest version. See the documentation and existing repositories for details.
Most projects are more than happy to add the .pre-commit-hooks.yaml file, along with a little documentation. After you’ve done so, add the repository to the all-repos.yaml file for pre-commit.com, to make it show up on the website.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
Tags: pre-commit