diff --git a/builtin/clone.c b/builtin/clone.c
index 2a2a03bf76..c2c8596aa9 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -1393,6 +1393,18 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 			   branch_top.buf, reflog_msg.buf, transport,
 			   !is_local);
 
+	if (hook_should_prompt_suggestions()) {
+		for (ref = mapped_refs; ref; ref = ref->next) {
+			if (ref->peer_ref &&
+			    !strcmp(ref->peer_ref->name,
+				    "refs/remotes/origin/suggested-hooks")) {
+				fprintf(stderr, _("The remote has suggested hooks in refs/remotes/origin/suggested-hooks.\n"
+						  "Run `git hook install all` to install them.\n"));
+				break;
+			}
+		}
+	}
+
 	update_head(our_head_points_at, remote_head, reflog_msg.buf);
 
 	/*
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 769af53ca4..e86c312473 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -28,6 +28,7 @@
 #include "promisor-remote.h"
 #include "commit-graph.h"
 #include "shallow.h"
+#include "hook.h"
 
 #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000)
 
@@ -1313,6 +1314,22 @@ static int consume_refs(struct transport *transport, struct ref *ref_map)
 				 ref_map);
 	transport_unlock_pack(transport);
 	trace2_region_leave("fetch", "consume_refs", the_repository);
+
+	if (hook_should_prompt_suggestions()) {
+		struct ref *ref;
+
+		for (ref = ref_map; ref; ref = ref->next) {
+			if (ref->peer_ref &&
+			    !strcmp(ref->peer_ref->name,
+				    "refs/remotes/origin/suggested-hooks") &&
+			    oidcmp(&ref->old_oid, &ref->peer_ref->old_oid)) {
+				fprintf(stderr, _("The remote has updated its suggested hooks.\n"));
+				fprintf(stderr, _("Run 'git hook install all' to update.\n"));
+				break;
+			}
+		}
+	}
+
 	return ret;
 }
 
diff --git a/builtin/hook.c b/builtin/hook.c
index c79a961e80..0334fee967 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -139,6 +139,17 @@ static int run(int argc, const char **argv, const char *prefix)
 	return rc;
 }
 
+static int install(int argc, const char **argv, const char *prefix)
+{
+	if (argc < 1)
+		die(_("You must specify a hook event to install."));
+	if (!strcmp(argv[1], "all"))
+		hook_update_suggested(NULL);
+	else
+		hook_update_suggested(argv[1]);
+	return 0;
+}
+
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
 	const char *run_hookdir = NULL;
@@ -152,10 +163,6 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, builtin_hook_options,
 			     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);
 
-	/* after the parse, we should have "<command> <hookname> <args...>" */
-	if (argc < 2)
-		usage_with_options(builtin_hook_usage, builtin_hook_options);
-
 	git_config(git_default_config, NULL);
 
 
@@ -185,6 +192,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 		return list(argc, argv, prefix);
 	if (!strcmp(argv[0], "run"))
 		return run(argc, argv, prefix);
+	if (!strcmp(argv[0], "install"))
+		return install(argc, argv, prefix);
 
 	usage_with_options(builtin_hook_usage, builtin_hook_options);
 }
diff --git a/hook.c b/hook.c
index 3ccacb72fa..32eee9abb6 100644
--- a/hook.c
+++ b/hook.c
@@ -4,6 +4,12 @@
 #include "config.h"
 #include "run-command.h"
 #include "prompt.h"
+#include "commit.h"
+#include "object.h"
+#include "refs.h"
+#include "tree-walk.h"
+#include "tree.h"
+#include "streaming.h"
 
 /*
  * NEEDSWORK: Doesn't look like there is a list of all possible hooks;
@@ -476,6 +482,60 @@ static int notify_hook_finished(int result,
 	return 0;
 }
 
+static struct tree *remote_suggested_hook_tree(int *warning_printed)
+{
+	struct object_id oid;
+	struct object *obj;
+	struct tree *tree;
+
+	if (read_ref("refs/remotes/origin/suggested-hooks", &oid))
+		return NULL;
+
+	obj = parse_object(the_repository, &oid);
+	if (obj == NULL) {
+		warning(_("object pointed to by refs/remotes/origin/suggested-hooks '%s' does not exist"),
+			oid_to_hex(&oid));
+		if (warning_printed)
+			*warning_printed = 1;
+		return NULL;
+	}
+	if (obj->type != OBJ_COMMIT) {
+		warning(_("object pointed to by refs/remotes/origin/suggested-hooks '%s' is not a commit"),
+			oid_to_hex(&oid));
+		if (warning_printed)
+			*warning_printed = 1;
+		return NULL;
+	}
+
+	tree = get_commit_tree((struct commit *) obj);
+	if (parse_tree(tree)) {
+		warning(_("could not parse tree"));
+		if (warning_printed)
+			*warning_printed = 1;
+		return NULL;
+	}
+
+	return tree;
+}
+
+static int has_suggested_hook(const char *hookname)
+{
+	struct tree *tree;
+	struct tree_desc desc;
+	struct name_entry entry;
+
+	tree = remote_suggested_hook_tree(NULL);
+	if (!tree)
+		return 0;
+
+	init_tree_desc(&desc, tree->buffer, tree->size);
+	while (tree_entry(&desc, &entry)) {
+		if (!strcmp(hookname, entry.path))
+			return 1;
+	}
+	return 0;
+}
+
 int run_hooks(const char *hookname, struct run_hooks_opt *options)
 {
 	struct list_head *to_run, *pos = NULL, *tmp = NULL;
@@ -497,8 +557,16 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 			    list_del(pos);
 	}
 
-	if (list_empty(to_run))
+	if (list_empty(to_run)) {
+		if (!strcmp("commit-msg", hookname)) {
+			if (has_suggested_hook(hookname) &&
+			    !hook_exists(hookname, HOOKDIR_USE_CONFIG)) {
+				fprintf(stderr, _("No commit-msg hook has been configured, but one is suggested by the remote.\n"));
+				fprintf(stderr, _("Run 'git hook install commit-msg' to install it.\n"));
+			}
+		}
 		return 0;
+	}
 
 	cb_data.head = to_run;
 	cb_data.run_me = list_entry(to_run->next, struct hook, list);
@@ -515,3 +583,84 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 
 	return cb_data.rc;
 }
+
+static int is_hook(const char *filename)
+{
+	int i;
+
+	for (i = 0; i < hook_name_count; i++) {
+		if (!strcmp(filename, hook_name[i]))
+			return 1;
+	}
+	return 0;
+}
+
+void hook_update_suggested(const char *hook_to_update)
+{
+	struct tree *tree;
+	int warning_printed = 0;
+	struct tree_desc desc;
+	struct name_entry entry;
+	struct strbuf path = STRBUF_INIT;
+	int hook_found = 0;
+
+	tree = remote_suggested_hook_tree(&warning_printed);
+	if (!tree) {
+		if (!warning_printed)
+			warning(_("no such ref refs/remotes/origin/suggested-hooks, not updating hooks"));
+		return;
+	}
+
+	init_tree_desc(&desc, tree->buffer, tree->size);
+	while (tree_entry(&desc, &entry)) {
+		int fd;
+
+		if (hook_to_update && strcmp(hook_to_update, entry.path))
+			/*
+			 * We only need to update one hook, and this is not the
+			 * hook we're looking for
+			 */
+			continue;
+
+		if (!hook_to_update && !is_hook(entry.path)) {
+			warning(_("file '%s' is not a hook; ignoring"),
+				entry.path);
+			continue;
+		}
+		hook_found = 1;
+		if (S_ISDIR(entry.mode) || S_ISGITLINK(entry.mode)) {
+			warning(_("file '%s' is not an ordinary file; ignoring"),
+				entry.path);
+			continue;
+		}
+
+		strbuf_reset(&path);
+		strbuf_git_path(&path, "hooks/%s", entry.path);
+		fd = open(path.buf, O_WRONLY | O_CREAT, 0755);
+		if (fd < 0) {
+			warning_errno(_("could not create file '%s'; skipping this hook"),
+				      path.buf);
+			continue;
+		}
+		if (stream_blob_to_fd(fd, &entry.oid, NULL, 1)) {
+			warning(_("could not write to file '%s'; skipping this hook"),
+				path.buf);
+			continue;
+		}
+		close(fd);
+	}
+	strbuf_release(&path);
+
+	if (hook_to_update && !hook_found)
+		warning(_("hook '%s' not found"), hook_to_update);
+
+	return;
+}
+
+int hook_should_prompt_suggestions(void)
+{
+	int dest = 0;
+
+	return !git_config_get_bool("hook.promptremotesuggested", &dest) &&
+	       dest;
+}
diff --git a/hook.h b/hook.h
index d902166408..438bf7122e 100644
--- a/hook.h
+++ b/hook.h
@@ -140,3 +140,6 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options);
 void free_hook(struct hook *ptr);
 /* Empties the list at 'head', calling 'free_hook()' on each entry */
 void clear_hook_list(struct list_head *head);
+
+void hook_update_suggested(const char *hook_to_update);
+int hook_should_prompt_suggestions(void);
diff --git a/t/t1361-remote-suggested-hooks.sh b/t/t1361-remote-suggested-hooks.sh
new file mode 100755
index 0000000000..223c65ac99
--- /dev/null
+++ b/t/t1361-remote-suggested-hooks.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+
+test_description='remote-suggested hooks'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+setup_server () {
+	git init server &&
+	test_when_finished "rm -rf server" &&
+	cat >server/commit-msg <<-'EOF' &&
+	echo "overwrite the commit message" >$1
+EOF
+	git -C server add commit-msg &&
+	test_commit -C server commit-with-hook &&
+	git -C server checkout -b suggested-hooks
+}
+
+add_hook_to_server () {
+	>server/pre-commit &&
+	git -C server add pre-commit &&
+	test_commit -C server additional-hook
+}
+
+test_expect_success 'suggestion upon clone' '
+	setup_server &&
+	test_when_finished "rm -rf client" &&
+	git -c hook.promptRemoteSuggested=1 clone server client 2>err &&
+	grep "The remote has suggested hooks" err
+'
+
+test_expect_success 'no suggestion upon clone if not configured' '
+	setup_server &&
+	test_when_finished "rm -rf client" &&
+	git clone server client 2>err &&
+	! grep "The remote has suggested hooks" err
+'
+
+test_expect_success 'suggestion upon fetch if server has updated hooks' '
+	setup_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+	add_hook_to_server &&
+
+	git -C client -c hook.promptRemoteSuggested=1 fetch 2>err &&
+	grep "The remote has updated its suggested hooks" err
+'
+
+test_expect_success 'no suggestion upon fetch if not configured' '
+	setup_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+	add_hook_to_server &&
+
+	git -C client fetch 2>err &&
+	! grep "The remote has updated its suggested hooks" err
+'
+
+test_expect_success 'no suggestion upon fetch if server has not updated hooks' '
+	setup_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+	git -C server checkout main &&
+	test_commit -C server not-a-hook-update &&
+
+	git -C client -c hook.promptRemoteSuggested=1 fetch 2>err &&
+	! grep "The remote has updated its suggested hooks" err
+'
+
+test_expect_success 'suggest commit-msg hook upon commit' '
+	setup_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+
+	test_commit -C client foo 2>err &&
+	grep "Run .git hook install commit-msg" err
+'
+
+test_expect_success 'install one suggested hook' '
+	setup_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+
+	git -C client hook install commit-msg &&
+
+	# Check that the hook was written by making a commit
+	test_commit -C client bar &&
+	git -C client show >commit &&
+	grep "overwrite the commit message" commit
+'
+
+test_expect_success 'install all suggested hooks' '
+	setup_server &&
+	add_hook_to_server &&
+	git clone server client &&
+	test_when_finished "rm -rf client" &&
+
+	git -C client hook install all &&
+	test -f client/.git/hooks/pre-commit &&
+	test -f client/.git/hooks/commit-msg
+'
+
+test_done
