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 verbose [--local|--global|--system] <on|off>
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 |
Git may report an all-zero old value even for an existing ref.
The bridge reads its previous value during prepared
and restores it at committed, distinguishing creation
from updates. Without a usable snapshot, *-created
remains a creation candidate. Dry-run output uses only its input.
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. A snapshot preserves the previous
symbolic target or direct OID when Git supplies a zero old value,
including for git update-ref --no-deref HEAD.
Without a snapshot, an unknown attachment state produces only
head-updated.
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.
Log hook dispatches
ghe verbose on
# disable it again
ghe verbose off
verbose is disabled by default. When enabled, it writes each hook dispatch to the same standard-error stream as Git's messages:
[git-hooks-ext] branch-created ➠ create-branch-env
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
Raw Git results were measured on 2026-09-19. Snapshot recovery was
validated on 2026-10-01 with Apple Git 2.39.3 and Git 2.55 using
files and reftable. The CI matrix covers Git 2.27–2.55. 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 | Git ≥ 2.28 |
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 | Git ≥ 2.28 |
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 | Git ≥ 2.28 |
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 | Git ≥ 2.28 |
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 | Git ≥ 2.28 |
note-updated | git notes remove | Git ≥ 2.28 |
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 | Git ≥ 2.54 |
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 | Git ≥ 2.28 |
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.
Snapshots recover branch and tag deletions, tracking ref deletions
from git remote prune, updates from
git notes append, git notes remove and
subsequent git stash push commands. On Git 2.54+,
they also recover git remote set-head -d and symbolic
HEAD transitions. Re-run ghe install after upgrading
the extension to update a config-based bridge.
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.