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: local
contains custom hook definitions.id
is the hook’s identifier, which you can use to run the hook on the commandline.name
is the human-readable name that pre-commit displays whilst running the hook.language: node
selects the Node.js language. pre-commit will download Node.js and use it to set up an isolated environment for the hook.additional_dependencies
is a list ofnpm
package 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
autoupdate
command 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.entry
is the command to run. pre-commit passes it filenames as arguments (unless you setpass_filenames: false
).args
defines extra arguments to pass to the command. In this case, we’re passing two options to the stylelint CLI:--formatter unix
which uses “Unix” style output formatting, which fits in with pre-commit.--fix
to apply automatic fixes where possible.
types_or
selects files matching thecss
“tag”, which means files with a.css
extension. This comes from theidentify
tool, as covered in the pre-commit docs.You can add other tags here to match multiple types. For example, you could add the
scss
tag to match SCSS (Sass) files. These use an extended version of CSS that stylelint also supports.exclude
is 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: system
won’t set up any isolated environment. Therefore the command inentry
must be available on your search path (thePATH
environment 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
jpegoptim
command.entry
is justjpegoptim
since it optimizes images in-place by default.The
jpeg
type tag selects files ending in.jpeg
and.jpg
. You can check this in the identify source.
After adding the hook, you can again optimize all JPEG’s 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.
Learn more about pre-commit, particularly for Python projects, in my DX book.
One summary email a week, no spam, I pinky promise.
Related posts:
Tags: pre-commit