diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 6d8b7d6c63..024f92befc 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -101,6 +101,17 @@ remote.<name>.serverOption::
 	The default set of server options used when fetching from this remote.
 	These server options can be overridden by the `--server-option=` command
 	line arguments.
+
+remote.<name>.followRemoteHEAD::
+	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+	The default value is "create", which will create `remotes/<name>/HEAD`
+	if it exists on the remote, but not locally, but will not touch an
+	already existing local reference.  Setting to "warn" will print
+	a message if the remote has a different value, than the local one and
+	in case there is no local reference, it behaves like "create". Setting
+	to "always" will silently update it to the value on the remote.
+	Finally, setting it to "never" will never change or create the local
+	reference.
 +
 This is a multi-valued variable, and an empty value can be used in a higher
 priority configuration file (e.g. `.git/config` in a repository) to clear
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2f416cf867..b619bddd7a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
-static int set_head(const struct ref *remote_refs)
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
+			remote, head_name, prev_head);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (updateres && buf_prev->len) {
+		printf("'HEAD' at '%s' is '%s', "
+			"but we have a detached HEAD pointing to '%s' locally.\n",
+			remote, head_name, buf_prev->buf);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static int set_head(const struct ref *remote_refs, int follow_remote_head)
 {
-	int result = 0, is_bare;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int result = 0, create_only, is_bare, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		      b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
 	struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
 		string_list_append(&heads, strip_refshead(ref->name));
 	}
 
+	if (follow_remote_head < 0)
+		goto cleanup;
 
 	if (!heads.nr)
 		result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
 	if (!head_name)
 		goto cleanup;
 	is_bare = is_bare_repository();
+	create_only = follow_remote_head == 2 ? 0 : !is_bare;
 	if (is_bare) {
 		strbuf_addstr(&b_head, "HEAD");
 		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
 		result = 1;
 		goto cleanup;
 	}
-	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, !is_bare))
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, create_only);
+	if (was_detached == -1) {
 		result = 1;
+		goto cleanup;
+	}
+	if (follow_remote_head == 1 && verbosity >= 0)
+		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
 	free(head_name);
@@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs)
 	free_refs(matches);
 	string_list_clear(&heads, 0);
 	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
 	strbuf_release(&b_remote_head);
 	return result;
 }
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs))
+	if (set_head(remote_refs, transport->remote->follow_remote_head))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 10104d11e3..5a768ddac2 100644
--- a/remote.c
+++ b/remote.c
@@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
 	} else if (!strcmp(subkey, "serveroption")) {
 		return parse_transport_option(key, value,
 					      &remote->server_options);
+	} else if (!strcmp(subkey, "followremotehead")) {
+		if (!strcmp(value, "never"))
+			remote->follow_remote_head = -1;
+		else if (!strcmp(value, "create"))
+			remote->follow_remote_head = 0;
+		else if (!strcmp(value, "warn"))
+			remote->follow_remote_head = 1;
+		else if (!strcmp(value, "always"))
+			remote->follow_remote_head = 2;
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index a7e5c4e07c..3ceadac820 100644
--- a/remote.h
+++ b/remote.h
@@ -107,6 +107,15 @@ struct remote {
 	char *http_proxy_authmethod;
 
 	struct string_list server_options;
+
+	/*
+	 * The setting for whether to update HEAD for the remote.
+	 * -1 never update
+	 * 0 create only (default)
+	 * 1 warn on change
+	 * 2 always update
+	 */
+	int follow_remote_head;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..2467027d34 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
 	branch=$(git rev-parse refs/remotes/origin/other) &&
 	test "z$head" = "z$branch"'
 
+test_expect_success "fetch test followRemoteHEAD never" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "never" &&
+		git fetch &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn no change" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn create" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn detached" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have a detached HEAD pointing to" \
+			"${SQ}${HEAD}${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn quiet" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		output=$(git fetch --quiet) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD always" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git fetch &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
