We're hiring!
*

Git hooks, upgraded: What's new in Git 2.54 and coming in 2.55

Adrian Ratiu avatar

Adrian Ratiu
April 27, 2026

Share this post:

Reading time:

Git is certainly one of the most influential software projects in the world, to which we all owe a debt of gratitude. We might exaggerate slightly, but software development can be split into multiple eras, two of which are before and after Git.

It's an honor to contribute back features and bug-fixes to the Git project, so we encourage everyone to contribute, as they can, to their favorite Git implementations, porcelains, web interfaces, and so on.

This post covers new features from Git 2.54 (released last week) and upcoming features planned for 2.55 or later.

Hooks specified via git configuration

The GitHub Blog has a very nice introduction to this feature, so we do not repeat everything it covers. Their section titled Config-based hooks is definitely worth a read, so please do read it. It also contains links to the source code.

We slightly modify the GitHub example, to use pre-push instead of pre-commit, to better illustrate the feature in the next section:

[hook "linter"]
   event = pre-push
   command = ~/bin/linter --cpp20

[hook "no-leaks"]
   event = pre-push
   command = ~/bin/leak-detector

This configures two hooks to be run for the pre-push event: a linter and a memleak detector.

In Git 2.54 they run sequentially in the order they appear in configs (more on this later).

Before this feature, the two commands had to be added to .git/hooks/pre-push as a script or binary program in each individual repository (hooks cannot be committed and auto-run due to well-known security risks).

Git configurations live at multiple levels and are parsed (centralized) when git runs:

  • System scope (git config --system) applies to every user and all repos on a system and typically lives at paths like /etc/gitconfig or C:\Program Files\Git\etc\gitconfig.
  • Global scope (git config --global) applies to a specific user and is stored in the user's $HOME dir at paths like $HOME/.gitconfig or $HOME/.config/git/config.
  • Local scope (git config --local) applies to a single repository and is stored inside each repository at .git/config.

Now users have the ability to define or enable/disable hooks at all these levels.

Users and developers typically have no idea what hooks run on their systems and often the system level hooks are managed by an organization's sysadmins, so to make things easier we also extended the git hook command, for e.g. to list the scope where each hook is defined.

This feature was added in 2.54.

Running hooks in parallel

Running the above two (or more) hooks in parallel requires intricate scripting or fork/spawn/signal processing for each OS, including collecting and de-interlacing (separating, demuxing) each hook's process output, to print them nicely to stdout.

Git did not support this: all hooks run sequentially up to 2.54 inclusive, regardless of where they are defined.

This is solved by another patch series we landed in the git next branch, hopefully to be released as part of 2.55. With this series, the earlier example can run the linter and the memleak checker in parallel:

[hook "linter"]
   event = pre-push
   command = ~/bin/linter --cpp20
   parallel = true

[hook "no-leaks"]
   event = pre-push
   command = ~/bin/leak-detector
   parallel = true

[hook]
   jobs = 2

Parallelism is opt-in to avoid any breakage risks, because existing hooks might run the same program or write to the same file. We do not know what users have in their hooks.

The hook.jobs knob sets a global parallel job count. It behaves like fetch.parallel and can be overridden as well, for example, certain hook events might need different parallelism and set a hook..jobs config. A value of -1 uses the number of available CPU cores.

There is also a CLI override, so users can run/test their hooks with simple commands, without touching their git configuration:

git hook run -j N (or --jobs=N)

It's worth mentioning that some hooks can't be run in parallel for safety reasons and cannot be overridden, like pre-commit, prepare-commit-msg, and others, typically because of shared resource usage like the commit message buffer.

Big thank you to Jeff King (Peff) for helping us come up with a super cool design to cleanly and efficiently do all the above (and more!) and helping debug signal issues on rather esoteric platforms supported by Git, like NonStop UNIX.

For more details, feel free to examine the ar/parallel-hooks merge commit. We hope this will be part of the 2.55 release.

Fixing submodule path collisions

Prior to 2.54, Git always errors out if one tries to add two submodule names that collide at the gitdir level, for example a submodule named foo and another named foo/bar:

  $ git submodule add --name foo ../child path-a
  $ git submodule add --name foo/bar ../child path-b
  error: submodule git dir '.../.git/modules/foo/bar' is inside git dir '.../.git/modules/foo'
  fatal: refusing to create/use '.../.git/modules/foo/bar' in another submodule's git dir

Another variant of this problem appears on case-insensitive filesystemd, where submodules named Foo and foo collide, even without nesting directories.

In addition to the filesystem path conflict, this also has security implications because one can override another repository's hooks dir with a specially crafted submodule name.

To cut the story short, we added an extension config (extensions.submodulePathConfig) which solves this by transparently encoding the submodule gitdirs and introduced a submodule..gitdir as a single source of truth for submodule paths, which can be used to change the submodule encoding if necessary.

This is present in 2.54.

Follow-up work

There is plenty more to be done and we encourage everyone to start using these features, report bugs, send patches, extend commands like git hook with features you like, add support in alternative Git implementations like JGit, libgit2, or gitoxide, and so on.

As always, Collabora is available to help if needed.

Particularly inside the main Git project, we plan to convert all remaining hooks to the new hook.h API abstraction, because it now has the above "killer" features. Then we can also remove the old hook APIs, which will be unused.

Big thank you to the Git community

We would like to extend a big thank you to the entire Git community and especially to those who contributed directly and helped shape these features: Emily Shaffer, Patrick Steinhardt, Junio Hamano, Jeff King, Ævar Arnfjörð Bjarmason, Phillip Wood, Josh Steadmon, Brandon Williams, Eric Sunshine, Szeder Gabor, and many others to whom we apologize for not naming directly here, but who were crucial to making these happen, including non-code behind-the-scenes contributions.

All of your work is deeply valued and appreciated.

 

Search the newsroom

Latest News & Events

Git hooks, upgraded: What's new in Git 2.54 and coming in 2.55

27/04/2026

Collabora's contributions to Git 2.54 and the upcoming 2.55 add powerful config-based hooks with better visibility, opt-in parallel hook…

Accelerating OpenXR at XR Expo 2026

20/04/2026

Collabora is headed to Stuttgart for XR Expo 2026! Visit us in Hall 2, booth 2C22, to experience the latest around Monado and ElectricMaple,…

YouTube Device Partner Summit 2026

14/04/2026

This week, Collabora is at the YouTube Device Partner Summit in Tokyo showcasing our ongoing work with YouTube, notably on their TV app…

Open Since 2005 logo

Our website only uses a strictly necessary session cookie provided by our CMS system. To find out more please follow this link.

Collabora Limited © 2005-2026. All rights reserved. Privacy Notice. Sitemap.