Skip to content

Repository files navigation

Release License Lint Tests Coverage

git-hooks-ext

About · Quick Start · Install · Worktrees · Configuration · Hook Arguments · Compatibility · Development · Website ↗

About

Git's reference-transaction hook reports raw old and new values together with ref names. It does not tell a hook that a branch was created, a tag was deleted or a ref was renamed.

git-hooks-ext turns those low-level updates into semantic events such as branch-created, tag-deleted, remote-head-updated and ref-created. It also adds the worktree lifecycle events that Git does not provide.

Supported ref events are grouped by purpose:

Everyday refs

RefCreatedDeletedUpdatedRenamed
Branchbranch-createdbranch-deletedbranch-updatedbranch-renamed
Remote branchremote-branch-createdremote-branch-deletedremote-branch-updatedremote-branch-renamed
Tagtag-createdtag-deletedtag-updatedtag-renamed
Notenote-creatednote-deletednote-updatednote-renamed
Stashstash-createdstash-deletedstash-updated—

Specialized refs

RefCreatedDeletedUpdated
Replacereplace-createdreplace-deletedreplace-updated
Prefetchprefetch-createdprefetch-deletedprefetch-updated
Bisect refbisect-ref-createdbisect-ref-deletedbisect-ref-updated
Rewritten refrewritten-ref-createdrewritten-ref-deletedrewritten-ref-updated
Per-worktree refworktree-ref-createdworktree-ref-deletedworktree-ref-updated

Fallback refs

RefCreatedDeletedUpdated
Other refs/*ref-createdref-deletedref-updated
Root refroot-ref-createdroot-ref-deletedroot-ref-updated

Remote HEAD

RefCreatedDeletedUpdated
Remote HEADremote-head-createdremote-head-deletedremote-head-updated

HEAD

HEADUpdatedAttachedDetachedSwitched
HEADhead-updatedhead-attachedhead-detachedhead-switched

*-created is a creation candidate, not an independent proof that the ref did not exist. Git uses an all-zero old value both when it creates a ref and when an update does not require a particular previous value. This bridge maps every zero -> value record to *-created, which is usually a creation. It runs only at the committed state, however, so the ref already has its new value and cannot be checked with git rev-parse to distinguish those cases. Hooks that need that distinction must treat *-created as a candidate.

The worktree-ref-* events describe updates under refs/worktree/*. They are separate from the lifecycle events below.

Worktree lifecycle

Git has no hook for observing worktree lifecycle operations, so worktree-* events are available only when the command is run through the ghe worktree wrapper.

Wrapper commandEvent
ghe worktree addworktree-created
ghe worktree removeworktree-removed
ghe worktree moveworktree-moved
ghe worktree lockworktree-locked
ghe worktree unlockworktree-unlocked
ghe worktree pruneworktree-pruned
ghe worktree repairworktree-repaired

The wrapper forwards all arguments to git worktree, compares the worktree state before and after a successful mutating command, and emits the observed lifecycle events. Calling git worktree directly bypasses it and cannot emit these events.

Event names are identical in Git config, classic hook filenames and dry-run output. The command compatibility matrix shows which commands produced each event in end-to-end tests.

Quick Start

Enable the extension in a Git repository and add a configured hook:

git-hooks-ext install
# or: ghe install

mkdir -p scripts
cat > scripts/announce-branch <<'SH'
#!/bin/sh
echo "created branch: $1"
SH
chmod +x scripts/announce-branch
ghe add branch-created announce-branch ./scripts/announce-branch

ghe is a shorter, equivalent command installed alongside git-hooks-ext.

Run ghe doctor inside a repository to show the detected Git and reference backend, installed bridge, and compatibility status for every event.

Now create a branch:

git branch topic

The hook prints:

created branch: topic

Run ghe events to list every supported event. If you use core.hooksPath, put the event hook in that directory instead.

Install

Homebrew

brew tap ciembor/git-hooks-ext
# Homebrew 7+: trust this formula (omit on older versions).
brew trust --formula ciembor/git-hooks-ext/git-hooks-ext
brew install ciembor/git-hooks-ext/git-hooks-ext

The public tap builds from the checksummed source archive in the GitHub release; it does not depend on local files or paths.

Debian 12 (AMD64 / ARM64)

Download the package and checksums from the v0.4.0 release:

arch=$(dpkg --print-architecture)
curl -fLO "https://github.com/ciembor/git-hooks-ext/releases/download/v0.4.0/git-hooks-ext_0.4.0-1_${arch}.deb"
curl -fLO https://github.com/ciembor/git-hooks-ext/releases/download/v0.4.0/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
sudo apt install "./git-hooks-ext_0.4.0-1_${arch}.deb"

Packages are available for AMD64 (Intel/AMD 64-bit) and ARM64 (AArch64). Other architectures are not currently published. This is a downloadable package installed with APT, not an APT repository: automatic upgrades via apt upgrade are not yet available. The release also contains the corresponding GPL-2.0-only source archive.

Container Image

A multi-platform image is published to GitHub Container Registry:

docker run --rm ghcr.io/ciembor/git-hooks-ext:latest --version

Native packages are recommended when installing hooks in a repository. The container image is useful for inspecting the CLI and processing input from a mounted or piped-in repository environment.

After installing the package, enable it in each repository:

git-hooks-ext install
# or: ghe install

The command detects the installed Git version. With Git 2.54 or later it uses config-based hooks; with Git 2.53 or older it installs a legacy reference-transaction hook and prints migration instructions. After upgrading Git, remove that legacy bridge and run git-hooks-ext install again (or ghe install).

Remove the bridge with:

ghe uninstall

This removes the config-based bridge and a legacy bridge installed by a current version of git-hooks-ext, while leaving any other reference-transaction hook untouched.

Fedora, Arch Linux and Alpine packages are also available. See Distribution and Packaging for package details, build recipes and installation tests.

Worktree Lifecycle

Git has no native hooks for removing, moving, locking, pruning or repairing a worktree. Run worktree commands through git-hooks-ext to add those events:

ghe worktree add -b feature ../feature
ghe worktree lock --reason "offline disk" ../feature
ghe worktree move ../feature ../feature-renamed
ghe worktree remove ../feature-renamed

All arguments are forwarded to git worktree. The command snapshots git worktree list --porcelain -z before and after a successful mutation and emits events only for observed lifecycle changes. Read-only commands are also forwarded, so ghe worktree list behaves like git worktree list. Commands run directly as git worktree ... bypass this frontend and do not emit lifecycle events.

For example, a classic hook can react to a newly created worktree:

cat >.git/hooks/worktree-created <<'SH'
#!/bin/sh
printf 'worktree %s created at %s\n' "$3" "$1"
SH
chmod +x .git/hooks/worktree-created
ghe worktree add -b feature ../feature

Advanced Configuration

With Git 2.54+ config-based hooks, you can also configure hooks through Git config:

git-hooks-ext install
# or: ghe install
ghe add branch-created create-branch-env ./scripts/create-branch-env
ghe list
ghe show announce-branch
ghe remove announce-branch

list prints the configured hook name, event and command. show and remove operate on one named event hook. These commands accept the same optional scope as add; they use the local repository configuration by default.

Hook Arguments

Reference create, update and delete hooks receive positional arguments:

<short-name> <full-ref> <old-value> <new-value>

Rename hooks receive:

<old-short-name> <new-short-name> <old-ref> <new-ref> <object-value>

Rename events are detected for branches, remote branches, tags and notes. The head-attached, head-detached and head-switched hooks use the same four arguments as head-updated. Symbolic values use Git's ref:refs/heads/<name> representation.

Worktree creation, removal, pruning and repair hooks receive:

<path> <head-value> <branch-ref>

The branch ref is empty for a detached worktree. Move hooks receive:

<old-path> <new-path> <head-value> <branch-ref>

Lock and unlock hooks receive the path and lock reason. The reason is empty when none was supplied:

<path> <reason>

By default, events are emitted only for the committed transaction state. This keeps user hooks post-factum and avoids aborting Git ref transactions.

Notes

Rename detection is best-effort. Git's reference-transaction hook reports ref updates, not user intent, so a delete and create of refs pointing at the same object can look like a rename. A rename is emitted only when both the deletion and creation have a unique match within the same ref namespace.

Events depend on Git providing a usable reference-transaction payload. The hook is available from Git 2.28, but the tested versions do not report both sides of git branch -m. From Git 2.31 through 2.55, ordinary git branch -D and git tag -d report zero -> zero, so they cannot produce semantic delete events through this bridge. Creation and explicit git update-ref -d remain usable. See the Git compatibility matrix for tested versions, ref backends and the reproducible probe.

References not covered by a named namespace still produce ref-created, ref-updated or ref-deleted when their name is below refs/. Ref names outside refs/* that pass through the ref backend, including custom names such as CUSTOM and misc/path, produce root-ref-*. Direct FETCH_HEAD and MERGE_HEAD are pseudorefs and are not classified as root refs. Git's main-worktree/ and worktrees/<name>/ aliases for per-worktree refs are classified by their underlying ref name; hook arguments retain the full alias-qualified name. Git can pass alias-qualified FETCH_HEAD and MERGE_HEAD through the ref backend, and these emit root-ref-*.

Every changed HEAD value produces head-updated. A known direct value changing to a symbolic value also produces head-attached; a symbolic value changing to a known direct value produces head-detached; and a change between different symbolic targets produces head-switched. Git can report an all-zero old value for ordinary git symbolic-ref, checkout and even explicit git update-ref --no-deref HEAD operations when HEAD already exists. In that case the extension emits only head-updated: it cannot safely infer the previous attachment state from a zero value.

remote-head-* uses the first path component after refs/remotes/ as the remote name. Remote names containing / are ambiguous with branch names ending in /HEAD when only the ref name is available; these are treated as remote branches by this classifier.

Other ref commands can provide incomplete information too. In the tested Git versions, git notes append, git notes remove and a second git stash push report a zero old value even though those refs already exist; the bridge therefore emits another note-created or stash-created creation candidate instead of an update. git remote prune removes its tracking branch without a semantic deletion. The command matrix below separates these limitations from events emitted when Git supplies complete transactions.

Compatibility

Commands and emitted events

Measured on 2026-09-19. The detailed CI matrix tests Git 2.27–2.55, Apple Git 2.39.3 and the files and reftable backends. Git versions < 2.28 do not provide the required reference-transaction hook.

This table lists each tested command, its expected event and the Git versions that emit it. The direct update-ref rows show which events remain reachable when a higher-level command omits usable transaction data. Rows for ghe worktree uses the extension's frontend; plain git worktree does not run these hooks.

HookCommandSupported Git versions
branch-createdgit branch topicGit ≥ 2.28
branch-updatedgit commitGit ≥ 2.28
branch-deletedgit branch -D topic2.28 ≤ Git ≤ 2.30 ²
branch-deletedgit update-ref -d refs/heads/topicGit ≥ 2.28
branch-renamedgit branch -m old new❌ ¹
branch-renamedgit update-ref --stdin (heads)Git ≥ 2.28
remote-branch-createdgit fetch originGit ≥ 2.28
remote-branch-updatedgit fetch originGit ≥ 2.28
remote-branch-deletedgit remote prune origin❌ ²
remote-branch-deletedgit update-ref -d refs/remotes/origin/topicGit ≥ 2.28
remote-branch-renamedgit remote rename origin upstreamGit ≥ 2.55
remote-branch-renamedgit update-ref --stdin (remotes)Git ≥ 2.28
tag-createdgit tag v1Git ≥ 2.28
tag-updatedgit tag -f v1Git ≥ 2.28
tag-deletedgit tag -d v12.28 ≤ Git ≤ 2.30 ²
tag-deletedgit update-ref -d refs/tags/topicGit ≥ 2.28
tag-renamedgit update-ref --stdin (tags)Git ≥ 2.28
stash-createdfirst git stash pushGit ≥ 2.28
stash-updatedsecond git stash push❌
stash-updatedgit update-ref refs/stashGit ≥ 2.28
stash-deletedgit stash clearGit ≥ 2.28
note-createdgit notes addGit ≥ 2.28
note-updatedgit notes append❌
note-updatedgit notes remove❌
note-updatedgit update-ref refs/notes/topicGit ≥ 2.28
note-deletedgit update-ref -d refs/notes/topicGit ≥ 2.28
note-renamedgit update-ref --stdin (notes)Git ≥ 2.28
remote-head-createdgit remote set-head origin mainGit ≥ 2.54
remote-head-createdgit remote set-head origin topic after fetchGit ≥ 2.54
remote-head-updatedgit update-ref --stdin (symref-update)Git ≥ 2.54
remote-head-deletedgit remote set-head -d origin❌
remote-head-deletedgit update-ref --stdin (symref-delete)Git ≥ 2.54
replace-createdgit replace <old> <new>Git ≥ 2.28
replace-updatedgit replace -f <old> <new>Git ≥ 2.28
replace-deletedgit replace -d <old>Git ≥ 2.28
prefetch-createdgit fetch --prefetch originGit ≥ 2.32
prefetch-updatedsecond git fetch --prefetch originGit ≥ 2.32
bisect-ref-createdgit bisect start <bad> <good>Git ≥ 2.28
remote-head-*direct git update-refGit ≥ 2.28
replace-*direct git update-refGit ≥ 2.28
prefetch-*direct git update-refGit ≥ 2.28
bisect-ref-*direct git update-refGit ≥ 2.28
rewritten-ref-*direct git update-refGit ≥ 2.28
worktree-ref-*direct git update-refGit ≥ 2.28
ref-*direct git update-refGit ≥ 2.28
head-updatedsymbolic/ref-backend transactionGit ≥ 2.54
head-attachedsymbolic/ref-backend transactionGit ≥ 2.54
head-switchedsymbolic/ref-backend transactionGit ≥ 2.54
remote-head-createdsymbolic/ref-backend transactionGit ≥ 2.54
root-ref-*symbolic/ref-backend transactionGit ≥ 2.54
head-detachedgit checkout --detach❌
worktree-createdghe worktree addGit ≥ 2.39.3
worktree-removedghe worktree removeGit ≥ 2.39.3
worktree-movedghe worktree moveGit ≥ 2.39.3
worktree-lockedghe worktree lockGit ≥ 2.39.3
worktree-unlockedghe worktree unlockGit ≥ 2.39.3
worktree-prunedghe worktree pruneGit ≥ 2.39.3
worktree-repairedghe worktree repairGit ≥ 2.39.3

Git bugs

Some compatibility gaps are caused by Git bugs, rather than limitations in git-hooks-ext. I filed the following reports upstream with proposed fixes for the transaction payloads that prevent the corresponding semantic events:

  1. ¹ git branch -m omits the destination ref from the reference-transaction hook.
  2. ² git branch -D and git tag -d report zero OIDs from Git 2.31 onward.

The compatibility notes explain the test method and Git's raw transaction behavior.

Development

Build instructions, source layout, test documentation and packaging notes are in DEVELOPMENT.md.

About

`git-hooks-ext` turns git's low-level `reference-transaction` hook input into semantic ref events.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages