Subject: [PATCH v2 5/8] hook: allow disabling config hooks

From: Adrian Ratiu <[email-masked]>
Date: Thu, 19 Feb 2026 00:23:49 +0200

Hooks specified via configs are always enabled, however users
might want to disable them without removing from the config,
like locally disabling a global hook.

Add a hook.<name>.enabled config which defaults to true and
can be optionally set for each configured hook.

Suggested-by: Patrick Steinhardt <[email-masked]>
Signed-off-by: Adrian Ratiu <[email-masked]>
---
 Documentation/config/hook.adoc |  7 +++++++
 hook.c                         | 20 ++++++++++++++++++++
 t/t1800-hook.sh                | 32 ++++++++++++++++++++++++++++++++
 3 files changed, 59 insertions(+)

diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc
index 9faafe3016..0cda4745a6 100644
--- a/Documentation/config/hook.adoc
+++ b/Documentation/config/hook.adoc
@@ -13,3 +13,10 @@ hook.<name>.event::
 	specified event, the associated `hook.<name>.command` is executed.
 	This is a multi-valued key. To run `hook.<name>` on multiple
 	events, specify the key more than once. See linkgit:git-hook[1].
+
+hook.<name>.enabled::
+	Whether the hook `hook.<name>` is enabled. Defaults to `true`.
+	Set to `false` to disable the hook without removing its
+	configuration. This is particularly useful when a hook is defined
+	in a system or global config file and needs to be disabled for a
+	specific repository. See linkgit:git-hook[1].
diff --git a/hook.c b/hook.c
index 8a9b405f76..35c24bf33d 100644
--- a/hook.c
+++ b/hook.c
@@ -164,6 +164,21 @@ static int hook_config_lookup_all(const char *key, const char *value,
 		char *old = strmap_put(&data->commands, hook_name,
 				       xstrdup(value));
 		free(old);
+	} else if (!strcmp(subkey, "enabled")) {
+		switch (git_parse_maybe_bool(value)) {
+		case 0: /* disabled */
+			if (!unsorted_string_list_lookup(&data->disabled_hooks,
+							 hook_name))
+				string_list_append(&data->disabled_hooks,
+						   hook_name);
+			break;
+		case 1: /* enabled: undo a prior disabled entry */
+			unsorted_string_list_remove(&data->disabled_hooks,
+						    hook_name);
+			break;
+		default:
+			break; /* ignore unrecognised values */
+		}
 	}
 
 	free(hook_name);
@@ -216,6 +231,11 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
 			const char *hname = hook_names->items[i].string;
 			char *command;
 
+			/* filter out disabled hooks */
+			if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
+							hname))
+				continue;
+
 			command = strmap_get(&cb_data.commands, hname);
 			if (!command)
 				die(_("'hook.%s.command' must be configured or "
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index f1048a5119..9797802735 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -318,6 +318,38 @@ test_expect_success 'rejects hooks with no commands configured' '
 	test_grep "hook.broken.command" actual
 '
 
+test_expect_success 'disabled hook is not run' '
+	test_config hook.skipped.event "test-hook" &&
+	test_config hook.skipped.command "echo \"Should not run\"" &&
+	test_config hook.skipped.enabled false &&
+
+	git hook run --ignore-missing test-hook 2>actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success 'disabled hook does not appear in git hook list' '
+	test_config hook.active.event "pre-commit" &&
+	test_config hook.active.command "echo active" &&
+	test_config hook.inactive.event "pre-commit" &&
+	test_config hook.inactive.command "echo inactive" &&
+	test_config hook.inactive.enabled false &&
+
+	git hook list pre-commit >actual &&
+	test_grep "active" actual &&
+	test_grep ! "inactive" actual
+'
+
+test_expect_success 'globally disabled hook can be re-enabled locally' '
+	test_config_global hook.global-hook.event "test-hook" &&
+	test_config_global hook.global-hook.command "echo \"global-hook ran\"" &&
+	test_config_global hook.global-hook.enabled false &&
+	test_config hook.global-hook.enabled true &&
+
+	echo "global-hook ran" >expected &&
+	git hook run test-hook 2>actual &&
+	test_cmp expected actual
+'
+
 test_expect_success 'git hook run a hook with a bad shebang' '
 	test_when_finished "rm -rf bad-hooks" &&
 	mkdir bad-hooks &&
-- 
2.52.0.732.gb351b5166d.dirty