git-hooks-ext
Name
git-hooks-ext — semantic events for Git reference transactions
Synopsis
git-hooks-ext install [--local|--global|--system]
# or: ghe install [--local|--global|--system]
ghe uninstall [--local|--global|--system]
ghe add [--local|--global|--system] <event> <name> <command> [args...]
ghe list [--local|--global|--system]
ghe show [--local|--global|--system] <name>
ghe remove [--local|--global|--system] <name>
ghe events
ghe doctor
ghe worktree <command> [args...]
ghe reference-transaction <state> [--dry-run]
Description
Git's reference-transaction hook reports old and new
values together with a ref name. git-hooks-ext
translates that input into events such as
branch-created, tag-deleted, and
branch-renamed.
ghe is a shorter, equivalent command installed alongside git-hooks-ext.
Run ghe doctor in a repository for its detected Git compatibility and bridge status.
It also provides a frontend for semantic worktree lifecycle events that Git does not expose as native hooks.
0000… a84f… refs/heads/topic
branch-created topic
Event hooks run after the transaction is committed. They receive
the short ref name, full ref name, old value, and new value.
Symbolic values use Git's ref: representation.
Events
Event names are identical in hook files, Git config, and dry-run output.
Everyday refs
| Ref | Created | Deleted | Updated | Renamed |
|---|---|---|---|---|
| Branch | branch-created | branch-deleted | branch-updated | branch-renamed |
| Remote branch | remote-branch-created | remote-branch-deleted | remote-branch-updated | remote-branch-renamed |
| Tag | tag-created | tag-deleted | tag-updated | tag-renamed |
| Note | note-created | note-deleted | note-updated | note-renamed |
| Stash | stash-created | stash-deleted | stash-updated | — |
Specialized refs
| Ref | Created | Deleted | Updated |
|---|---|---|---|
| Replace | replace-created | replace-deleted | replace-updated |
| Prefetch | prefetch-created | prefetch-deleted | prefetch-updated |
| Bisect ref | bisect-ref-created | bisect-ref-deleted | bisect-ref-updated |
| Rewritten ref | rewritten-ref-created | rewritten-ref-deleted | rewritten-ref-updated |
| Per-worktree ref | worktree-ref-created | worktree-ref-deleted | worktree-ref-updated |
*-created is a creation candidate, not proof that the
ref did not already exist. Git uses an all-zero old value both for
ref creation and for an update that does not require a particular
previous value. This bridge maps every zero → value
record to *-created, which is usually a creation.
Events run only after commit, so the ref already has its new value
and git rev-parse cannot distinguish the two cases.
Fallback refs
| Ref | Created | Deleted | Updated |
|---|---|---|---|
Other refs/* | ref-created | ref-deleted | ref-updated |
| Root ref | root-ref-created | root-ref-deleted | root-ref-updated |
Remote HEAD
| Ref | Created | Deleted | Updated |
|---|---|---|---|
| Remote HEAD | remote-head-created | remote-head-deleted | remote-head-updated |
HEAD
| HEAD | Updated | Attached | Detached | Switched |
|---|---|---|---|---|
| HEAD | head-updated | head-attached | head-detached | head-switched |
The worktree-ref-* events describe updates under
refs/worktree/*. They are separate from the
worktree-* lifecycle events below.
remote-head-* treats the first path component after
refs/remotes/ as the remote name. Names containing
/ are ambiguous with remote branches ending in
/HEAD and are classified as remote branches.
Ref names outside refs/* that reach the ref backend,
including custom names, emit root-ref-*. Git's
main-worktree/ and worktrees/<name>/
aliases use the class of the underlying per-worktree ref while
retaining the full alias-qualified name in hook arguments.
Alias-qualified FETCH_HEAD and
MERGE_HEAD can reach the ref backend and emit
root-ref-*, unlike their direct pseudoref names.
Every changed HEAD value emits
head-updated. Changes between direct and symbolic
values additionally emit head-attached or
head-detached; changes between symbolic targets emit
head-switched. When Git reports an all-zero old or
new value, the previous or next attachment state is unknown, so
only head-updated is emitted. Git can report a zero
old value even for an explicit
git update-ref --no-deref HEAD detach.
Worktree lifecycle
Git has no hook for observing worktree lifecycle operations, so
these events are available only through the
ghe worktree wrapper.
| Wrapper command | Event |
|---|---|
ghe worktree add | worktree-created |
ghe worktree remove | worktree-removed |
ghe worktree move | worktree-moved |
ghe worktree lock | worktree-locked |
ghe worktree unlock | worktree-unlocked |
ghe worktree prune | worktree-pruned |
ghe worktree repair | worktree-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.
Quick start
1. Enable the hook bridge
git-hooks-ext install
# or: ghe install
The command automatically uses config-based hooks with Git 2.54+ or installs the legacy bridge on older Git versions. After upgrading Git, remove the legacy reference-transaction hook and run git-hooks-ext install again (or ghe install).
2. Add an event hook
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
3. Trigger it
git branch topic
created branch: topic
Manage configured hooks
For config-based hooks added with ghe add, inspect and manage the named entries:
ghe list
ghe show announce-branch
ghe remove announce-branch
list prints each hook name, event and command. The commands use local configuration by default and accept --local, --global or --system. The technical bridge is not listed; use install and uninstall to manage it.
Diagnose compatibility
ghe doctor
Run this inside a repository to see the detected Git version and ref backend, the bridge state, and whether each event is supported, requires a newer Git version, or is affected by a known Git bug.
Worktree events
Use the worktree frontend when lifecycle events are needed:
ghe worktree add -b feature ../feature
ghe worktree lock --reason "offline disk" ../feature
ghe worktree move ../feature ../feature-renamed
ghe worktree remove ../feature-renamed
Arguments are forwarded to git worktree. Events are
emitted after Git succeeds and the worktree state changes.
Compatibility
Measured on 2026-09-19. The detailed CI matrix covers 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.
Commands and emitted events
This table lists each tested command, its expected event and the
Git versions that emit it. Direct update-ref commands
show where an event remains reachable when a higher-level command
omits usable transaction data. The worktree rows use the
extension's frontend.
| Hook | Command | Supported Git versions |
|---|---|---|
branch-created | git branch topic | Git ≥ 2.28 |
branch-updated | git commit | Git ≥ 2.28 |
branch-deleted | git branch -D topic | 2.28 ≤ Git ≤ 2.30 ² |
branch-deleted | git update-ref -d refs/heads/topic | Git ≥ 2.28 |
branch-renamed | git branch -m old new | ❌ ¹ |
branch-renamed | git update-ref --stdin (heads) | Git ≥ 2.28 |
remote-branch-created | git fetch origin | Git ≥ 2.28 |
remote-branch-updated | git fetch origin | Git ≥ 2.28 |
remote-branch-deleted | git remote prune origin | ❌ ² |
remote-branch-deleted | git update-ref -d refs/remotes/origin/topic | Git ≥ 2.28 |
remote-branch-renamed | git remote rename origin upstream | Git ≥ 2.55 |
remote-branch-renamed | git update-ref --stdin (remotes) | Git ≥ 2.28 |
tag-created | git tag v1 | Git ≥ 2.28 |
tag-updated | git tag -f v1 | Git ≥ 2.28 |
tag-deleted | git tag -d v1 | 2.28 ≤ Git ≤ 2.30 ² |
tag-deleted | git update-ref -d refs/tags/topic | Git ≥ 2.28 |
tag-renamed | git update-ref --stdin (tags) | Git ≥ 2.28 |
stash-created | first git stash push | Git ≥ 2.28 |
stash-updated | second git stash push | ❌ |
stash-updated | git update-ref refs/stash | Git ≥ 2.28 |
stash-deleted | git stash clear | Git ≥ 2.28 |
note-created | git notes add | Git ≥ 2.28 |
note-updated | git notes append | ❌ |
note-updated | git notes remove | ❌ |
note-updated | git update-ref refs/notes/topic | Git ≥ 2.28 |
note-deleted | git update-ref -d refs/notes/topic | Git ≥ 2.28 |
note-renamed | git update-ref --stdin (notes) | Git ≥ 2.28 |
remote-head-created | git remote set-head origin main | Git ≥ 2.54 |
remote-head-created | git remote set-head origin topic after fetch | Git ≥ 2.54 |
remote-head-updated | git update-ref --stdin (symref-update) | Git ≥ 2.54 |
remote-head-deleted | git remote set-head -d origin | ❌ |
remote-head-deleted | git update-ref --stdin (symref-delete) | Git ≥ 2.54 |
replace-created | git replace <old> <new> | Git ≥ 2.28 |
replace-updated | git replace -f <old> <new> | Git ≥ 2.28 |
replace-deleted | git replace -d <old> | Git ≥ 2.28 |
prefetch-created | git fetch --prefetch origin | Git ≥ 2.32 |
prefetch-updated | second git fetch --prefetch origin | Git ≥ 2.32 |
bisect-ref-created | git bisect start <bad> <good> | Git ≥ 2.28 |
remote-head-* | direct git update-ref | Git ≥ 2.28 |
replace-* | direct git update-ref | Git ≥ 2.28 |
prefetch-* | direct git update-ref | Git ≥ 2.28 |
bisect-ref-* | direct git update-ref | Git ≥ 2.28 |
rewritten-ref-* | direct git update-ref | Git ≥ 2.28 |
worktree-ref-* | direct git update-ref | Git ≥ 2.28 |
ref-* | direct git update-ref | Git ≥ 2.28 |
head-updated | symbolic/ref-backend transaction | Git ≥ 2.54 |
head-attached | symbolic/ref-backend transaction | Git ≥ 2.54 |
head-switched | symbolic/ref-backend transaction | Git ≥ 2.54 |
remote-head-created | symbolic/ref-backend transaction | Git ≥ 2.54 |
root-ref-* | symbolic/ref-backend transaction | Git ≥ 2.54 |
head-detached | git checkout --detach | ❌ |
worktree-created | ghe worktree add | Git ≥ 2.39.3 |
worktree-removed | ghe worktree remove | Git ≥ 2.39.3 |
worktree-moved | ghe worktree move | Git ≥ 2.39.3 |
worktree-locked | ghe worktree lock | Git ≥ 2.39.3 |
worktree-unlocked | ghe worktree unlock | Git ≥ 2.39.3 |
worktree-pruned | ghe worktree prune | Git ≥ 2.39.3 |
worktree-repaired | ghe worktree repair | Git ≥ 2.39.3 |
Git bugs
Some compatibility gaps are caused by Git bugs, rather than
limitations in git-hooks-ext. I filed these reports
upstream with proposed fixes:
- ¹
git branch -momits the destination ref from thereference-transactionhook. - ²
git branch -Dandgit tag -dreport zero OIDs from Git 2.31 onward.
git remote prune removes the tracking ref without a
semantic deletion. In the tested versions,
git notes append, git notes remove, and a
second git stash push emit a created event
instead of the expected updated event.
See the compatibility notes for the exact tested versions, method, and raw transaction results.
Notes
Rename detection is best-effort. A rename is emitted only when a deletion and creation have one unique object-ID match within the same ref namespace.
Git 2.28 introduces the required hook, but not every Git command supplies a usable payload. See the command compatibility matrix for measured event availability and command behavior through Git 2.55.
See Install for native packages and the README for complete hook arguments.