diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 81f2a87e88..48f8446943 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -404,6 +404,57 @@ Both standard output and standard error output are forwarded to
 `git send-pack` on the other end, so you can simply `echo` messages
 for the user.
 
+ref-transaction-prepared
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by any Git command that performs reference
+updates. It executes as soon as all reference updates were queued to
+the transaction and locked on disk. This hook executes for every
+reference transaction that is being prepared and may thus get called
+multiple times.
+
+It takes no arguments, but for each ref to be updated it receives on
+standard input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+If the hook exits with a non-zero status, the transaction is aborted
+and the command exits immediately. The
+<<ref-transaction-aborted,'ref-transaction-aborted'>> hook is not
+executed in that case.
+
+[[ref-transaction-aborted]]
+ref-transaction-aborted
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by any Git command that performs reference
+updates. It executes as soon as a reference transaction is aborted and
+after all reference locks were released and any changes made to
+references were rolled back. The hook may get called multiple times or
+never in case no transaction was aborted.
+
+The hook takes no arguments, but for each ref to be updated it
+receives on standard input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+The hook's exit code is discarded by Git.
+
+ref-transaction-committed
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by any Git command that performs reference
+updates. It executes as soon as a reference transaction is committed,
+persisting all changes to disk and releasing any locks. The hook may
+get called multiple times or never in case no transaction was aborted.
+
+The hook takes no arguments, but for each ref to be updated it
+receives on standard input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+The hook's exit code is discarded by Git.
+
 push-to-checkout
 ~~~~~~~~~~~~~~~~
 
diff --git a/refs.c b/refs.c
index 224ff66c7b..e41fa7ea55 100644
--- a/refs.c
+++ b/refs.c
@@ -9,6 +9,7 @@
 #include "iterator.h"
 #include "refs.h"
 #include "refs/refs-internal.h"
+#include "run-command.h"
 #include "object-store.h"
 #include "object.h"
 #include "tag.h"
@@ -16,6 +17,7 @@
 #include "worktree.h"
 #include "argv-array.h"
 #include "repository.h"
+#include "sigchain.h"
 
 /*
  * List of all available backends
@@ -1986,10 +1988,56 @@ int ref_update_reject_duplicates(struct string_list *refnames,
 	return 0;
 }
 
+static int run_transaction_hook(struct ref_transaction *transaction,
+				const char *hook_name)
+{
+	struct child_process proc = CHILD_PROCESS_INIT;
+	struct strbuf buf = STRBUF_INIT;
+	const char *argv[2];
+	int code, i;
+
+	argv[0] = find_hook(hook_name);
+	if (!argv[0])
+		return 0;
+
+	argv[1] = NULL;
+
+	proc.argv = argv;
+	proc.in = -1;
+	proc.stdout_to_stderr = 1;
+	proc.trace2_hook_name = hook_name;
+
+	code = start_command(&proc);
+	if (code)
+		return code;
+
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	for (i = 0; i < transaction->nr; i++) {
+		struct ref_update *update = transaction->updates[i];
+
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "%s %s %s\n",
+			    oid_to_hex(&update->old_oid),
+			    oid_to_hex(&update->new_oid),
+			    update->refname);
+
+		if (write_in_full(proc.in, buf.buf, buf.len) < 0)
+			break;
+	}
+
+	close(proc.in);
+	sigchain_pop(SIGPIPE);
+
+	strbuf_release(&buf);
+	return finish_command(&proc);
+}
+
 int ref_transaction_prepare(struct ref_transaction *transaction,
 			    struct strbuf *err)
 {
 	struct ref_store *refs = transaction->ref_store;
+	int ret;
 
 	switch (transaction->state) {
 	case REF_TRANSACTION_OPEN:
@@ -2012,7 +2060,17 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
 		return -1;
 	}
 
-	return refs->be->transaction_prepare(refs, transaction, err);
+	ret = refs->be->transaction_prepare(refs, transaction, err);
+	if (ret)
+		return ret;
+
+	ret = run_transaction_hook(transaction, "ref-transaction-prepared");
+	if (ret) {
+		ref_transaction_abort(transaction, err);
+		die(_("ref updates aborted by hook"));
+	}
+
+	return 0;
 }
 
 int ref_transaction_abort(struct ref_transaction *transaction,
@@ -2036,6 +2094,8 @@ int ref_transaction_abort(struct ref_transaction *transaction,
 		break;
 	}
 
+	run_transaction_hook(transaction, "ref-transaction-aborted");
+
 	ref_transaction_free(transaction);
 	return ret;
 }
@@ -2064,7 +2124,10 @@ int ref_transaction_commit(struct ref_transaction *transaction,
 		break;
 	}
 
-	return refs->be->transaction_finish(refs, transaction, err);
+	ret = refs->be->transaction_finish(refs, transaction, err);
+	if (!ret)
+		run_transaction_hook(transaction, "ref-transaction-committed");
+	return ret;
 }
 
 int refs_verify_refname_available(struct ref_store *refs,
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
new file mode 100755
index 0000000000..b6df5fc883
--- /dev/null
+++ b/t/t1416-ref-transaction-hooks.sh
@@ -0,0 +1,88 @@
+#!/bin/sh
+
+test_description='reference transaction hooks'
+
+. ./test-lib.sh
+
+create_commit ()
+{
+	test_tick &&
+	T=$(git write-tree) &&
+	sha1=$(echo message | git commit-tree $T) &&
+	echo $sha1
+}
+
+test_expect_success setup '
+	mkdir -p .git/hooks
+'
+
+test_expect_success 'prepared hook allows updating ref' '
+	test_when_finished "rm .git/hooks/ref-transaction-prepared" &&
+	write_script .git/hooks/ref-transaction-prepared <<-\EOF &&
+		exit 0
+	EOF
+	C=$(create_commit) &&
+	git update-ref HEAD $C
+'
+
+test_expect_success 'prepared hook aborts updating ref' '
+	test_when_finished "rm .git/hooks/ref-transaction-prepared" &&
+	write_script .git/hooks/ref-transaction-prepared <<-\EOF &&
+		exit 1
+	EOF
+	C=$(create_commit) &&
+	test_must_fail git update-ref HEAD $C 2>err &&
+	grep "ref updates aborted by hook" err
+'
+
+test_expect_success 'prepared hook gets all queued updates' '
+	test_when_finished "rm .git/hooks/ref-transaction-prepared actual" &&
+	write_script .git/hooks/ref-transaction-prepared <<-\EOF &&
+		while read -r line; do printf "%s\n" "$line"; done >actual
+	EOF
+	C=$(create_commit) &&
+	cat >expect <<-EOF &&
+		$ZERO_OID $C HEAD
+		$ZERO_OID $C refs/heads/master
+	EOF
+	git update-ref HEAD $C <<-EOF &&
+		update HEAD $ZERO_OID $C
+		update refs/heads/master $ZERO_OID $C
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'committed hook gets all queued updates' '
+	test_when_finished "rm .git/hooks/ref-transaction-committed actual" &&
+	write_script .git/hooks/ref-transaction-committed <<-\EOF &&
+		while read -r line; do printf "%s\n" "$line"; done >actual
+	EOF
+	C=$(create_commit) &&
+	cat >expect <<-EOF &&
+		$ZERO_OID $C HEAD
+		$ZERO_OID $C refs/heads/master
+	EOF
+	git update-ref HEAD $C &&
+	test_cmp expect actual
+'
+
+test_expect_success 'aborted hook gets all queued updates' '
+	test_when_finished "rm .git/hooks/ref-transaction-aborted actual" &&
+	write_script .git/hooks/ref-transaction-aborted <<-\EOF &&
+		while read -r line; do printf "%s\n" "$line"; done >actual
+	EOF
+	C=$(create_commit) &&
+	cat >expect <<-EOF &&
+		$ZERO_OID $C HEAD
+		$ZERO_OID $C refs/heads/master
+	EOF
+	git update-ref --stdin <<-EOF &&
+		start
+		update HEAD $C $ZERO_OID
+		update refs/heads/master $C $ZERO_OID
+		abort
+	EOF
+	test_cmp expect actual
+'
+
+test_done
