{"thread":{"id":"66258","subject":"[PATCH] hooks: introduce 'hooks.allowNoVerify' configuration","startedAt":"2026-09-02T16:17:45Z","lastAt":"2026-09-02T22:22:19Z","messageCount":6,"participants":["Alessio Attilio via GitGitGadget","Junio C Hamano","brian m. carlson"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"551786","messageId":"pull.2215.git.1788365862670.gitgitgadget@gmail.com","threadId":"66258","inReplyTo":null,"subject":"[PATCH] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"Alessio Attilio via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T16:17:42Z","receivedAt":"2026-09-02T16:17:45Z","isPatch":true,"body":"From: Alessio Attilio <alessio.attilio@protonmail.com>\n\nIntroduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to\nprevent accidental bypass of hooks with '--no-verify' when set to false.\nAuthoritative enforcement remains server-side.\n\nSigned-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n---\n    hooks: introduce 'hooks.allowNoVerify' configuration\n    \n    Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail\n    (default: true) to prevent accidental bypass of hooks via '--no-verify'.\n    This setting is intended for workflows and managed environments to avoid\n    inadvertent bypasses, without altering Git's server-side security model.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2215\n\n Documentation/config.adoc       |   2 +\n Documentation/config/hooks.adoc |  10 +++\n builtin/am.c                    |   5 ++\n builtin/commit.c                |   8 ++\n builtin/merge.c                 |   6 ++\n builtin/push.c                  |   6 ++\n builtin/rebase.c                |  10 +++\n t/meson.build                   |   1 +\n t/t7599-hooks-allownoverify.sh  | 149 ++++++++++++++++++++++++++++++++\n 9 files changed, 197 insertions(+)\n create mode 100644 Documentation/config/hooks.adoc\n create mode 100755 t/t7599-hooks-allownoverify.sh\n\ndiff --git a/Documentation/config.adoc b/Documentation/config.adoc\nindex f67dcd2f8e..2ba351e6ee 100644\n--- a/Documentation/config.adoc\n+++ b/Documentation/config.adoc\n@@ -508,6 +508,8 @@ include::config/help.adoc[]\n \n include::config/hook.adoc[]\n \n+include::config/hooks.adoc[]\n+\n include::config/http.adoc[]\n \n include::config/i18n.adoc[]\ndiff --git a/Documentation/config/hooks.adoc b/Documentation/config/hooks.adoc\nnew file mode 100644\nindex 0000000000..ce46645a1e\n--- /dev/null\n+++ b/Documentation/config/hooks.adoc\n@@ -0,0 +1,10 @@\n+`hooks.allowNoVerify`::\n+\tA boolean to specify whether `--no-verify` (or `-n`) command-line\n+\toption is permitted in commands such as `git commit` and `git push`.\n+\tWhen set to `false`, attempting to bypass hooks with `--no-verify`\n+\twill cause Git to abort immediately with a fatal error. Defaults to\n+\t`true`.\n++\n+Note that this setting serves as an opt-in workflow guardrail against\n+accidental bypasses (for example in managed environments or CI runners),\n+and does not replace authoritative server-side hook enforcement.\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..8b82d4c1b6 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2320,6 +2320,7 @@ int cmd_am(int argc,\n \tint patch_format = PATCH_FORMAT_UNKNOWN;\n \tenum resume_type resume_mode = RESUME_FALSE;\n \tint in_progress;\n+\tint allow_no_verify = 1;\n \tint ret = 0;\n \n \tconst char * const usage[] = {\n@@ -2448,6 +2449,7 @@ int cmd_am(int argc,\n \tshow_usage_with_options_if_asked(argc, argv, usage, options);\n \n \trepo_config(the_repository, git_default_config, NULL);\n+\trepo_config_get_bool(the_repository, \"hooks.allownoverify\", &allow_no_verify);\n \n \tam_state_init(&state);\n \n@@ -2457,6 +2459,9 @@ int cmd_am(int argc,\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \n+\tif (state.no_verify && !allow_no_verify)\n+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n+\n \tif (binary >= 0)\n \t\tfprintf_ln(stderr, _(\"The -b/--binary option has been a no-op for long time, and\\n\"\n \t\t\t\t\"it will be removed. Please do not use it anymore.\"));\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..c59f7ded6e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -127,6 +127,7 @@ static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int edit_flag = -1; /* unspecified */\n static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;\n static int config_commit_verbose = -1; /* unspecified */\n+static int allow_no_verify = 1;\n static int no_post_rewrite, allow_empty_message, pathspec_file_nul;\n static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;\n static const char *sign_commit, *pathspec_from_file;\n@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tfinalize_deferred_config(s);\n \n+\tif (no_verify && !allow_no_verify)\n+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n+\n \tif (force_author && !strchr(force_author, '>'))\n \t\tforce_author = find_author_by_nickname(force_author);\n \n@@ -1691,6 +1695,10 @@ static int git_commit_config(const char *k, const char *v,\n \t\t\t\t\t\t\t       &is_bool);\n \t\treturn 0;\n \t}\n+\tif (!strcmp(k, \"hooks.allownoverify\")) {\n+\t\tallow_no_verify = git_config_bool(k, v);\n+\t\treturn 0;\n+\t}\n \n \treturn git_status_config(k, v, ctx, s);\n }\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..77fd6fc57e 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -96,6 +96,7 @@ static int signoff;\n static const char *sign_commit;\n static int autostash;\n static int no_verify;\n+static int allow_no_verify = 1;\n static char *into_name;\n \n static struct strategy all_strategy[] = {\n@@ -727,6 +728,9 @@ static int git_merge_config(const char *k, const char *v,\n \t} else if (!strcmp(k, \"commit.gpgsign\")) {\n \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n \t\treturn 0;\n+\t} else if (!strcmp(k, \"hooks.allownoverify\")) {\n+\t\tallow_no_verify = git_config_bool(k, v);\n+\t\treturn 0;\n \t} else if (!strcmp(k, \"gpg.mintrustlevel\")) {\n \t\tcheck_trust_level = 0;\n \t} else if (!strcmp(k, \"merge.autostash\")) {\n@@ -1408,6 +1412,8 @@ int cmd_merge(int argc,\n \t\tparse_branch_merge_options(branch_mergeoptions);\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\tif (no_verify && !allow_no_verify)\n+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n \tif (shortlog_len < 0)\n \t\tshortlog_len = (merge_log_config > 0) ? merge_log_config : 0;\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex 2377b5af55..216603bb4f 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -63,6 +63,7 @@ static int verbosity;\n static int progress = -1;\n static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n static enum transport_family family;\n+static int allow_no_verify = 1;\n \n static struct push_cas_option cas;\n \n@@ -543,6 +544,9 @@ static int git_push_config(const char *k, const char *v,\n \t\telse\n \t\t\t*flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES;\n \t\treturn 0;\n+\t} else if (!strcmp(k, \"hooks.allownoverify\")) {\n+\t\tallow_no_verify = git_config_bool(k, v);\n+\t\treturn 0;\n \t}\n \n \treturn git_default_config(k, v, ctx, NULL);\n@@ -746,6 +750,8 @@ int cmd_push(int argc,\n \tpacket_trace_identity(\"push\");\n \trepo_config(the_repository, git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n+\tif ((flags & TRANSPORT_PUSH_NO_HOOK) && !allow_no_verify)\n+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n \tpush_options = (push_options_cmdline.nr\n \t\t? &push_options_cmdline\n \t\t: &push_options_config);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 10a306310c..eb996f0aa1 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -790,6 +790,8 @@ static void parse_rebase_merges_value(struct rebase_options *options, const char\n \t\tdie(_(\"Unknown rebase-merges mode: %s\"), value);\n }\n \n+static int allow_no_verify = 1;\n+\n static int rebase_config(const char *var, const char *value,\n \t\t\t const struct config_context *ctx, void *data)\n {\n@@ -820,6 +822,11 @@ static int rebase_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"hooks.allownoverify\")) {\n+\t\tallow_no_verify = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"rebase.rebasemerges\")) {\n \t\topts->config_rebase_merges = git_parse_maybe_bool(value);\n \t\tif (opts->config_rebase_merges < 0) {\n@@ -1299,6 +1306,9 @@ int cmd_rebase(int argc,\n \t\t\t     builtin_rebase_options,\n \t\t\t     builtin_rebase_usage, 0);\n \n+\tif (ok_to_skip_pre_rebase && !allow_no_verify)\n+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n+\n \tif (options.trailer_args.nr) {\n \t\tif (validate_trailer_args(&options.trailer_args))\n \t\t\tdie(NULL);\ndiff --git a/t/meson.build b/t/meson.build\nindex 7f53cca7d1..ce6ca1f6bf 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -945,6 +945,7 @@ integration_tests = [\n   't7526-commit-pathspec-file.sh',\n   't7527-builtin-fsmonitor.sh',\n   't7528-signed-commit-ssh.sh',\n+  't7599-hooks-allownoverify.sh',\n   't7600-merge.sh',\n   't7601-merge-pull-config.sh',\n   't7602-merge-octopus-many.sh',\ndiff --git a/t/t7599-hooks-allownoverify.sh b/t/t7599-hooks-allownoverify.sh\nnew file mode 100755\nindex 0000000000..eed99c128b\n--- /dev/null\n+++ b/t/t7599-hooks-allownoverify.sh\n@@ -0,0 +1,149 @@\n+#!/bin/sh\n+\n+test_description='support hooks.allowNoVerify configuration to disallow --no-verify'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository and hooks' '\n+\ttest_commit init &&\n+\ttest_hook --setup pre-commit <<-\\HOOK_EOF &&\n+\techo \"pre-commit executed\" >>pre-commit.log\n+\tif test -f fail-pre-commit\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\ttest_hook --setup pre-push <<-\\HOOK_EOF &&\n+\techo \"pre-push executed\" >>pre-push.log\n+\tif test -f fail-pre-push\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\tgit init --bare remote.git &&\n+\tgit remote add origin remote.git &&\n+\tgit push -u origin main &&\n+\trm -f pre-commit.log pre-push.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change1\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: -n is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change2\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -n -m \"commit with -n (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\trm -f pre-push.log &&\n+\tgit push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'explicit hooks.allowNoVerify=true allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change3\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify allowed\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change4\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit -n' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change5\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -n -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git push --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git push --no-verify origin main 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git merge --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit checkout -b branch-merge main &&\n+\techo \"merge change\" >merge_file &&\n+\tgit add merge_file &&\n+\tgit commit -m \"merge commit\" &&\n+\tgit checkout main &&\n+\ttest_must_fail git merge --no-verify branch-merge -m \"merge fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git rebase --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git rebase --no-verify main branch-merge 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change6\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -m \"normal commit\" &&\n+\ttest_path_is_file pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false enforces hook execution (hook failure prevents commit)' '\n+\ttest_when_finished \"rm -f fail-pre-commit pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\ttouch fail-pre-commit &&\n+\techo \"change7\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -m \"failing hook\" &&\n+\ttest_must_fail git commit --no-verify -m \"cannot bypass\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs pre-push hook on git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit push origin main &&\n+\ttest_path_is_file pre-push.log\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=false overrides local true' '\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change8\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git -c hooks.allowNoVerify=false commit --no-verify -m \"override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'local hooks.allowNoVerify=false overrides global true' '\n+\ttest_config_global hooks.allowNoVerify true &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change9\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"local override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_done\n\nbase-commit: 1630431f326e15fcde608827b5ff38422528eb59\n-- \ngitgitgadget\n"},{"id":"551788","messageId":"pull.2215.v2.git.1788366925041.gitgitgadget@gmail.com","threadId":"66258","inReplyTo":"pull.2215.git.1788365862670.gitgitgadget@gmail.com","subject":"[PATCH v2] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"Alessio Attilio via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T16:35:25Z","receivedAt":"2026-09-02T16:35:27Z","isPatch":true,"body":"From: Alessio Attilio <alessio.attilio@protonmail.com>\n\nIntroduce the 'hooks.allowNoVerify' configuration variable to control\nwhether the '--no-verify' (or '-n') command-line option is permitted\nduring operations executing client-side hooks (commit, push, merge,\nrebase, am).\n\nClient-side hooks execute in the user's local repository and cannot serve\nas an authoritative security boundary; authoritative policy enforcement\nbelongs on the server (such as via pre-receive hooks). However,\ndevelopers often invoke '--no-verify' out of habit or muscle memory,\naccidentally skipping local checks.\n\nTo address both 'security theatre' concerns and avoid breaking legitimate\nemergency escape hatches ('big red button'), this implementation:\n\n1. Introduces granular values:\n   - 'true' (or 'always', default): '--no-verify' is permitted normally.\n   - 'warn': '--no-verify' is permitted, but prints a warning to stderr.\n   - 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.\n\n2. Preserves the emergency break-glass escape hatch:\n   When configured to 'false', Git does not create a dead-end. It outputs\n   actionable advice explaining that the setting is a workflow guardrail\n   against accidental bypass, and documents how to override it in an\n   emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'.\n   This eliminates the need for developers to resort to destructive local\n   hacks like deleting hooks or chmod -x.\n\n3. Centralizes the validation logic in 'hook.c' and 'hook.h' via\n   validate_no_verify().\n\nSigned-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n---\n    hooks: introduce 'hooks.allowNoVerify' configuration\n    \n    Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail\n    (default: true) to prevent accidental bypass of hooks via '--no-verify'.\n    This setting is intended for workflows and managed environments to avoid\n    inadvertent bypasses, without altering Git's server-side security model.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2215\n\nRange-diff vs v1:\n\n 1:  853636dad0 ! 1:  4e594568de hooks: introduce 'hooks.allowNoVerify' configuration\n     @@ Metadata\n       ## Commit message ##\n          hooks: introduce 'hooks.allowNoVerify' configuration\n      \n     -    Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to\n     -    prevent accidental bypass of hooks with '--no-verify' when set to false.\n     -    Authoritative enforcement remains server-side.\n     +    Introduce the 'hooks.allowNoVerify' configuration variable to control\n     +    whether the '--no-verify' (or '-n') command-line option is permitted\n     +    during operations executing client-side hooks (commit, push, merge,\n     +    rebase, am).\n     +\n     +    Client-side hooks execute in the user's local repository and cannot serve\n     +    as an authoritative security boundary; authoritative policy enforcement\n     +    belongs on the server (such as via pre-receive hooks). However,\n     +    developers often invoke '--no-verify' out of habit or muscle memory,\n     +    accidentally skipping local checks.\n     +\n     +    To address both 'security theatre' concerns and avoid breaking legitimate\n     +    emergency escape hatches ('big red button'), this implementation:\n     +\n     +    1. Introduces granular values:\n     +       - 'true' (or 'always', default): '--no-verify' is permitted normally.\n     +       - 'warn': '--no-verify' is permitted, but prints a warning to stderr.\n     +       - 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.\n     +\n     +    2. Preserves the emergency break-glass escape hatch:\n     +       When configured to 'false', Git does not create a dead-end. It outputs\n     +       actionable advice explaining that the setting is a workflow guardrail\n     +       against accidental bypass, and documents how to override it in an\n     +       emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'.\n     +       This eliminates the need for developers to resort to destructive local\n     +       hacks like deleting hooks or chmod -x.\n     +\n     +    3. Centralizes the validation logic in 'hook.c' and 'hook.h' via\n     +       validate_no_verify().\n      \n          Signed-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n      \n     @@ Documentation/config.adoc: include::config/help.adoc[]\n       ## Documentation/config/hooks.adoc (new) ##\n      @@\n      +`hooks.allowNoVerify`::\n     -+\tA boolean to specify whether `--no-verify` (or `-n`) command-line\n     -+\toption is permitted in commands such as `git commit` and `git push`.\n     -+\tWhen set to `false`, attempting to bypass hooks with `--no-verify`\n     -+\twill cause Git to abort immediately with a fatal error. Defaults to\n     -+\t`true`.\n     ++\tSpecifies whether the `--no-verify` (or `-n`) command-line option\n     ++\tis permitted in commands that run client-side hooks, such as `git commit`,\n     ++\t`git push`, `git merge`, `git rebase`, and `git am`.\n      ++\n     -+Note that this setting serves as an opt-in workflow guardrail against\n     -+accidental bypasses (for example in managed environments or CI runners),\n     -+and does not replace authoritative server-side hook enforcement.\n     ++Allowed values are:\n     +++\n     ++--\n     ++* `true` (or `always`): `--no-verify` is permitted normally. This is the default.\n     ++* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.\n     ++* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts\n     ++  with a fatal error accompanied by advice explaining how to override it.\n     ++--\n     +++\n     ++In an emergency (for example, when a local hook crashes or during a critical\n     ++production hotfix), this guardrail can be overridden without modifying\n     ++configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable\n     ++or by passing `-c hooks.allowNoVerify=true` on the command line.\n     +++\n     ++NOTE: Client-side hooks execute in the developer's environment and belong to\n     ++the user. This configuration serves strictly as an ergonomic workflow guardrail\n     ++against accidental bypasses (such as muscle-memory `-n` or automated scripts),\n     ++and must not be relied upon as a security boundary. Authoritative enforcement\n     ++must always be implemented server-side (for example, via `pre-receive` hooks).\n      \n     - ## builtin/am.c ##\n     -@@ builtin/am.c: int cmd_am(int argc,\n     - \tint patch_format = PATCH_FORMAT_UNKNOWN;\n     - \tenum resume_type resume_mode = RESUME_FALSE;\n     - \tint in_progress;\n     -+\tint allow_no_verify = 1;\n     - \tint ret = 0;\n     - \n     - \tconst char * const usage[] = {\n     -@@ builtin/am.c: int cmd_am(int argc,\n     - \tshow_usage_with_options_if_asked(argc, argv, usage, options);\n     - \n     - \trepo_config(the_repository, git_default_config, NULL);\n     -+\trepo_config_get_bool(the_repository, \"hooks.allownoverify\", &allow_no_verify);\n     - \n     - \tam_state_init(&state);\n     + ## Documentation/git.adoc ##\n     +@@ Documentation/git.adoc: on some performance improvements or features). This variable currently\n     + only affects clones and fetches; it is not yet used for pushes (but may\n     + be in the future).\n       \n     ++`GIT_ALLOW_NO_VERIFY`::\n     ++\tIf this Boolean environment variable is set to true (e.g. `1`), permits the use\n     ++\tof `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.\n     ++\tThis serves as an emergency override mechanism for workflows when hooks fail unexpectedly.\n     ++\n     + `GIT_OPTIONAL_LOCKS`::\n     + \tIf this Boolean environment variable is set to false, Git will complete any requested operation without\n     + \tperforming any optional sub-operations that require taking a lock.\n     +\n     + ## builtin/am.c ##\n      @@ builtin/am.c: int cmd_am(int argc,\n       \n       \targc = parse_options(argc, argv, prefix, options, usage, 0);\n       \n     -+\tif (state.no_verify && !allow_no_verify)\n     -+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n     ++\tif (state.no_verify)\n     ++\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n      +\n       \tif (binary >= 0)\n       \t\tfprintf_ln(stderr, _(\"The -b/--binary option has been a no-op for long time, and\\n\"\n       \t\t\t\t\"it will be removed. Please do not use it anymore.\"));\n      \n       ## builtin/commit.c ##\n     -@@ builtin/commit.c: static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n     - static int edit_flag = -1; /* unspecified */\n     - static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;\n     - static int config_commit_verbose = -1; /* unspecified */\n     -+static int allow_no_verify = 1;\n     - static int no_post_rewrite, allow_empty_message, pathspec_file_nul;\n     - static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;\n     - static const char *sign_commit, *pathspec_from_file;\n     +@@\n     + #include \"environment.h\"\n     + #include \"diff.h\"\n     + #include \"commit.h\"\n     ++#include \"hook.h\"\n     + #include \"add-interactive.h\"\n     + #include \"gettext.h\"\n     + #include \"revision.h\"\n      @@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],\n       \targc = parse_options(argc, argv, prefix, options, usage, 0);\n       \tfinalize_deferred_config(s);\n       \n     -+\tif (no_verify && !allow_no_verify)\n     -+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n     ++\tif (no_verify)\n     ++\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n      +\n       \tif (force_author && !strchr(force_author, '>'))\n       \t\tforce_author = find_author_by_nickname(force_author);\n       \n     -@@ builtin/commit.c: static int git_commit_config(const char *k, const char *v,\n     - \t\t\t\t\t\t\t       &is_bool);\n     - \t\treturn 0;\n     - \t}\n     -+\tif (!strcmp(k, \"hooks.allownoverify\")) {\n     -+\t\tallow_no_verify = git_config_bool(k, v);\n     -+\t\treturn 0;\n     -+\t}\n     - \n     - \treturn git_status_config(k, v, ctx, s);\n     - }\n      \n       ## builtin/merge.c ##\n     -@@ builtin/merge.c: static int signoff;\n     - static const char *sign_commit;\n     - static int autostash;\n     - static int no_verify;\n     -+static int allow_no_verify = 1;\n     - static char *into_name;\n     - \n     - static struct strategy all_strategy[] = {\n     -@@ builtin/merge.c: static int git_merge_config(const char *k, const char *v,\n     - \t} else if (!strcmp(k, \"commit.gpgsign\")) {\n     - \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n     - \t\treturn 0;\n     -+\t} else if (!strcmp(k, \"hooks.allownoverify\")) {\n     -+\t\tallow_no_verify = git_config_bool(k, v);\n     -+\t\treturn 0;\n     - \t} else if (!strcmp(k, \"gpg.mintrustlevel\")) {\n     - \t\tcheck_trust_level = 0;\n     - \t} else if (!strcmp(k, \"merge.autostash\")) {\n      @@ builtin/merge.c: int cmd_merge(int argc,\n       \t\tparse_branch_merge_options(branch_mergeoptions);\n       \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n       \t\t\tbuiltin_merge_usage, 0);\n     -+\tif (no_verify && !allow_no_verify)\n     -+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n     ++\tif (no_verify)\n     ++\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n       \tif (shortlog_len < 0)\n       \t\tshortlog_len = (merge_log_config > 0) ? merge_log_config : 0;\n       \n      \n       ## builtin/push.c ##\n     -@@ builtin/push.c: static int verbosity;\n     - static int progress = -1;\n     - static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;\n     - static enum transport_family family;\n     -+static int allow_no_verify = 1;\n     - \n     - static struct push_cas_option cas;\n     - \n     -@@ builtin/push.c: static int git_push_config(const char *k, const char *v,\n     - \t\telse\n     - \t\t\t*flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES;\n     - \t\treturn 0;\n     -+\t} else if (!strcmp(k, \"hooks.allownoverify\")) {\n     -+\t\tallow_no_verify = git_config_bool(k, v);\n     -+\t\treturn 0;\n     - \t}\n     - \n     - \treturn git_default_config(k, v, ctx, NULL);\n     +@@\n     + #include \"environment.h\"\n     + #include \"gettext.h\"\n     + #include \"hex.h\"\n     ++#include \"hook.h\"\n     + #include \"refspec.h\"\n     + #include \"run-command.h\"\n     + #include \"remote.h\"\n      @@ builtin/push.c: int cmd_push(int argc,\n       \tpacket_trace_identity(\"push\");\n       \trepo_config(the_repository, git_push_config, &flags);\n       \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n     -+\tif ((flags & TRANSPORT_PUSH_NO_HOOK) && !allow_no_verify)\n     -+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n     ++\tif (flags & TRANSPORT_PUSH_NO_HOOK)\n     ++\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n       \tpush_options = (push_options_cmdline.nr\n       \t\t? &push_options_cmdline\n       \t\t: &push_options_config);\n      \n       ## builtin/rebase.c ##\n     -@@ builtin/rebase.c: static void parse_rebase_merges_value(struct rebase_options *options, const char\n     - \t\tdie(_(\"Unknown rebase-merges mode: %s\"), value);\n     - }\n     - \n     -+static int allow_no_verify = 1;\n     -+\n     - static int rebase_config(const char *var, const char *value,\n     - \t\t\t const struct config_context *ctx, void *data)\n     - {\n     -@@ builtin/rebase.c: static int rebase_config(const char *var, const char *value,\n     - \t\treturn 0;\n     - \t}\n     - \n     -+\tif (!strcmp(var, \"hooks.allownoverify\")) {\n     -+\t\tallow_no_verify = git_config_bool(var, value);\n     -+\t\treturn 0;\n     -+\t}\n     -+\n     - \tif (!strcmp(var, \"rebase.rebasemerges\")) {\n     - \t\topts->config_rebase_merges = git_parse_maybe_bool(value);\n     - \t\tif (opts->config_rebase_merges < 0) {\n      @@ builtin/rebase.c: int cmd_rebase(int argc,\n       \t\t\t     builtin_rebase_options,\n       \t\t\t     builtin_rebase_usage, 0);\n       \n     -+\tif (ok_to_skip_pre_rebase && !allow_no_verify)\n     -+\t\tdie(_(\"the use of '--no-verify' is disabled by 'hooks.allowNoVerify'\"));\n     ++\tif (ok_to_skip_pre_rebase)\n     ++\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n      +\n       \tif (options.trailer_args.nr) {\n       \t\tif (validate_trailer_args(&options.trailer_args))\n       \t\t\tdie(NULL);\n      \n     + ## hook.c ##\n     +@@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)\n     + \n     + \treturn run_hooks_opt(r, hook_name, &opt);\n     + }\n     ++\n     ++void validate_no_verify(struct repository *r, const char *opt)\n     ++{\n     ++\tconst char *val = NULL;\n     ++\tint maybe_bool;\n     ++\n     ++\tif (git_env_bool(\"GIT_ALLOW_NO_VERIFY\", 0))\n     ++\t\treturn;\n     ++\n     ++\tif (!r || repo_config_get_value(r, \"hooks.allownoverify\", &val))\n     ++\t\treturn;\n     ++\n     ++\tmaybe_bool = git_parse_maybe_bool(val);\n     ++\tif (maybe_bool == 1 || !strcasecmp(val, \"always\")) {\n     ++\t\treturn;\n     ++\t} else if (!strcasecmp(val, \"warn\")) {\n     ++\t\twarning(_(\"bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'\"), opt);\n     ++\t\treturn;\n     ++\t} else if (maybe_bool == 0 || !strcasecmp(val, \"never\") || !strcasecmp(val, \"error\")) {\n     ++\t\tadvise(_(\"this repository disallows '%s' as a workflow guardrail against accidental bypass.\\n\"\n     ++\t\t\t \"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\\n\"\n     ++\t\t\t \"  git -c hooks.allowNoVerify=true <command>\\n\"\n     ++\t\t\t \"or:\\n\"\n     ++\t\t\t \"  GIT_ALLOW_NO_VERIFY=1 git <command>\"), opt);\n     ++\t\tdie(_(\"the use of '%s' is disabled by 'hooks.allowNoVerify'\"), opt);\n     ++\t} else {\n     ++\t\twarning(_(\"unknown value for 'hooks.allowNoVerify': '%s'\"), val);\n     ++\t}\n     ++}\n     ++\n     +\n     + ## hook.h ##\n     +@@ hook.h: int run_hooks(struct repository *r, const char *hook_name);\n     +  */\n     + LAST_ARG_MUST_BE_NULL\n     + int run_hooks_l(struct repository *r, const char *hook_name, ...);\n     ++\n     ++/**\n     ++ * Check if the use of '--no-verify' (or '-n') is permitted according to\n     ++ * the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment\n     ++ * variable.\n     ++ *\n     ++ * If permitted, this function returns normally (or emits a warning if configured\n     ++ * to 'warn'). If disallowed, it outputs advice on how to override the workflow\n     ++ * guardrail in an emergency, then aborts with die().\n     ++ */\n     ++void validate_no_verify(struct repository *r, const char *opt);\n     ++\n     + #endif\n     ++\n     +\n       ## t/meson.build ##\n      @@ t/meson.build: integration_tests = [\n         't7526-commit-pathspec-file.sh',\n     @@ t/t7599-hooks-allownoverify.sh (new)\n      +\ttest_grep \"hooks.allowNoVerify\" err\n      +'\n      +\n     ++test_expect_success 'CLI -c hooks.allowNoVerify=true overrides local false' '\n     ++\ttest_config hooks.allowNoVerify false &&\n     ++\techo \"change10\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\tgit -c hooks.allowNoVerify=true commit --no-verify -m \"override false with CLI true\"\n     ++'\n     ++\n     ++test_expect_success 'hooks.allowNoVerify=false provides emergency override advice' '\n     ++\ttest_config hooks.allowNoVerify false &&\n     ++\techo \"change11\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\ttest_must_fail git commit --no-verify -m \"fail advice\" 2>err &&\n     ++\ttest_grep \"GIT_ALLOW_NO_VERIFY=1\" err &&\n     ++\ttest_grep \"git -c hooks.allowNoVerify=true\" err\n     ++'\n     ++\n     ++test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '\n     ++\ttest_when_finished \"rm -f pre-commit.log\" &&\n     ++\ttest_config hooks.allowNoVerify false &&\n     ++\techo \"change12\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\tGIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m \"emergency commit\" &&\n     ++\ttest_path_is_missing pre-commit.log\n     ++'\n     ++\n     ++test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '\n     ++\ttest_when_finished \"rm -f pre-push.log\" &&\n     ++\ttest_config hooks.allowNoVerify false &&\n     ++\tGIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&\n     ++\ttest_path_is_missing pre-push.log\n     ++'\n     ++\n     ++test_expect_success 'hooks.allowNoVerify=warn permits --no-verify and warns on stderr' '\n     ++\ttest_when_finished \"rm -f pre-commit.log err\" &&\n     ++\ttest_config hooks.allowNoVerify warn &&\n     ++\techo \"change13\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\tgit commit --no-verify -m \"commit with warn\" 2>err &&\n     ++\ttest_path_is_missing pre-commit.log &&\n     ++\ttest_grep \"bypassing hooks with .--no-verify. is discouraged\" err\n     ++'\n     ++\n     ++test_expect_success 'hooks.allowNoVerify=never disallows --no-verify' '\n     ++\ttest_config hooks.allowNoVerify never &&\n     ++\techo \"change14\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\ttest_must_fail git commit --no-verify -m \"fail never\" 2>err &&\n     ++\ttest_grep \"hooks.allowNoVerify\" err\n     ++'\n     ++\n     ++test_expect_success 'hooks.allowNoVerify=error disallows --no-verify' '\n     ++\ttest_config hooks.allowNoVerify error &&\n     ++\techo \"change15\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\ttest_must_fail git commit --no-verify -m \"fail error\" 2>err &&\n     ++\ttest_grep \"hooks.allowNoVerify\" err\n     ++'\n     ++\n     ++test_expect_success 'hooks.allowNoVerify=always allows --no-verify' '\n     ++\ttest_when_finished \"rm -f pre-commit.log\" &&\n     ++\ttest_config hooks.allowNoVerify always &&\n     ++\techo \"change16\" >>init.t &&\n     ++\tgit add init.t &&\n     ++\tgit commit --no-verify -m \"commit always\" &&\n     ++\ttest_path_is_missing pre-commit.log\n     ++'\n     ++\n      +test_done\n     ++\n\n\n Documentation/config.adoc       |   2 +\n Documentation/config/hooks.adoc |  24 ++++\n Documentation/git.adoc          |   5 +\n builtin/am.c                    |   3 +\n builtin/commit.c                |   4 +\n builtin/merge.c                 |   2 +\n builtin/push.c                  |   3 +\n builtin/rebase.c                |   3 +\n hook.c                          |  30 +++++\n hook.h                          |  13 ++\n t/meson.build                   |   1 +\n t/t7599-hooks-allownoverify.sh  | 217 ++++++++++++++++++++++++++++++++\n 12 files changed, 307 insertions(+)\n create mode 100644 Documentation/config/hooks.adoc\n create mode 100755 t/t7599-hooks-allownoverify.sh\n\ndiff --git a/Documentation/config.adoc b/Documentation/config.adoc\nindex f67dcd2f8e..2ba351e6ee 100644\n--- a/Documentation/config.adoc\n+++ b/Documentation/config.adoc\n@@ -508,6 +508,8 @@ include::config/help.adoc[]\n \n include::config/hook.adoc[]\n \n+include::config/hooks.adoc[]\n+\n include::config/http.adoc[]\n \n include::config/i18n.adoc[]\ndiff --git a/Documentation/config/hooks.adoc b/Documentation/config/hooks.adoc\nnew file mode 100644\nindex 0000000000..07830c5147\n--- /dev/null\n+++ b/Documentation/config/hooks.adoc\n@@ -0,0 +1,24 @@\n+`hooks.allowNoVerify`::\n+\tSpecifies whether the `--no-verify` (or `-n`) command-line option\n+\tis permitted in commands that run client-side hooks, such as `git commit`,\n+\t`git push`, `git merge`, `git rebase`, and `git am`.\n++\n+Allowed values are:\n++\n+--\n+* `true` (or `always`): `--no-verify` is permitted normally. This is the default.\n+* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.\n+* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts\n+  with a fatal error accompanied by advice explaining how to override it.\n+--\n++\n+In an emergency (for example, when a local hook crashes or during a critical\n+production hotfix), this guardrail can be overridden without modifying\n+configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable\n+or by passing `-c hooks.allowNoVerify=true` on the command line.\n++\n+NOTE: Client-side hooks execute in the developer's environment and belong to\n+the user. This configuration serves strictly as an ergonomic workflow guardrail\n+against accidental bypasses (such as muscle-memory `-n` or automated scripts),\n+and must not be relied upon as a security boundary. Authoritative enforcement\n+must always be implemented server-side (for example, via `pre-receive` hooks).\ndiff --git a/Documentation/git.adoc b/Documentation/git.adoc\nindex 8a5cdd3b3d..1b3af061a3 100644\n--- a/Documentation/git.adoc\n+++ b/Documentation/git.adoc\n@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently\n only affects clones and fetches; it is not yet used for pushes (but may\n be in the future).\n \n+`GIT_ALLOW_NO_VERIFY`::\n+\tIf this Boolean environment variable is set to true (e.g. `1`), permits the use\n+\tof `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.\n+\tThis serves as an emergency override mechanism for workflows when hooks fail unexpectedly.\n+\n `GIT_OPTIONAL_LOCKS`::\n \tIf this Boolean environment variable is set to false, Git will complete any requested operation without\n \tperforming any optional sub-operations that require taking a lock.\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..c79b9a82f0 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2457,6 +2457,9 @@ int cmd_am(int argc,\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \n+\tif (state.no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (binary >= 0)\n \t\tfprintf_ln(stderr, _(\"The -b/--binary option has been a no-op for long time, and\\n\"\n \t\t\t\t\"it will be removed. Please do not use it anymore.\"));\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..ef28c2cb9e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"environment.h\"\n #include \"diff.h\"\n #include \"commit.h\"\n+#include \"hook.h\"\n #include \"add-interactive.h\"\n #include \"gettext.h\"\n #include \"revision.h\"\n@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tfinalize_deferred_config(s);\n \n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (force_author && !strchr(force_author, '>'))\n \t\tforce_author = find_author_by_nickname(force_author);\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..0e6c2d4345 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,\n \t\tparse_branch_merge_options(branch_mergeoptions);\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tif (shortlog_len < 0)\n \t\tshortlog_len = (merge_log_config > 0) ? merge_log_config : 0;\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex 2377b5af55..98830da7f7 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -12,6 +12,7 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n+#include \"hook.h\"\n #include \"refspec.h\"\n #include \"run-command.h\"\n #include \"remote.h\"\n@@ -746,6 +747,8 @@ int cmd_push(int argc,\n \tpacket_trace_identity(\"push\");\n \trepo_config(the_repository, git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n+\tif (flags & TRANSPORT_PUSH_NO_HOOK)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tpush_options = (push_options_cmdline.nr\n \t\t? &push_options_cmdline\n \t\t: &push_options_config);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 10a306310c..dff28f0119 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc,\n \t\t\t     builtin_rebase_options,\n \t\t\t     builtin_rebase_usage, 0);\n \n+\tif (ok_to_skip_pre_rebase)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (options.trailer_args.nr) {\n \t\tif (validate_trailer_args(&options.trailer_args))\n \t\t\tdie(NULL);\ndiff --git a/hook.c b/hook.c\nindex d10eef4763..daed1b1c4b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -858,3 +858,33 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)\n \n \treturn run_hooks_opt(r, hook_name, &opt);\n }\n+\n+void validate_no_verify(struct repository *r, const char *opt)\n+{\n+\tconst char *val = NULL;\n+\tint maybe_bool;\n+\n+\tif (git_env_bool(\"GIT_ALLOW_NO_VERIFY\", 0))\n+\t\treturn;\n+\n+\tif (!r || repo_config_get_value(r, \"hooks.allownoverify\", &val))\n+\t\treturn;\n+\n+\tmaybe_bool = git_parse_maybe_bool(val);\n+\tif (maybe_bool == 1 || !strcasecmp(val, \"always\")) {\n+\t\treturn;\n+\t} else if (!strcasecmp(val, \"warn\")) {\n+\t\twarning(_(\"bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'\"), opt);\n+\t\treturn;\n+\t} else if (maybe_bool == 0 || !strcasecmp(val, \"never\") || !strcasecmp(val, \"error\")) {\n+\t\tadvise(_(\"this repository disallows '%s' as a workflow guardrail against accidental bypass.\\n\"\n+\t\t\t \"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\\n\"\n+\t\t\t \"  git -c hooks.allowNoVerify=true <command>\\n\"\n+\t\t\t \"or:\\n\"\n+\t\t\t \"  GIT_ALLOW_NO_VERIFY=1 git <command>\"), opt);\n+\t\tdie(_(\"the use of '%s' is disabled by 'hooks.allowNoVerify'\"), opt);\n+\t} else {\n+\t\twarning(_(\"unknown value for 'hooks.allowNoVerify': '%s'\"), val);\n+\t}\n+}\n+\ndiff --git a/hook.h b/hook.h\nindex 27bb1aeb2e..0590ceee2d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -280,4 +280,17 @@ int run_hooks(struct repository *r, const char *hook_name);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hooks_l(struct repository *r, const char *hook_name, ...);\n+\n+/**\n+ * Check if the use of '--no-verify' (or '-n') is permitted according to\n+ * the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment\n+ * variable.\n+ *\n+ * If permitted, this function returns normally (or emits a warning if configured\n+ * to 'warn'). If disallowed, it outputs advice on how to override the workflow\n+ * guardrail in an emergency, then aborts with die().\n+ */\n+void validate_no_verify(struct repository *r, const char *opt);\n+\n #endif\n+\ndiff --git a/t/meson.build b/t/meson.build\nindex 7f53cca7d1..ce6ca1f6bf 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -945,6 +945,7 @@ integration_tests = [\n   't7526-commit-pathspec-file.sh',\n   't7527-builtin-fsmonitor.sh',\n   't7528-signed-commit-ssh.sh',\n+  't7599-hooks-allownoverify.sh',\n   't7600-merge.sh',\n   't7601-merge-pull-config.sh',\n   't7602-merge-octopus-many.sh',\ndiff --git a/t/t7599-hooks-allownoverify.sh b/t/t7599-hooks-allownoverify.sh\nnew file mode 100755\nindex 0000000000..5a7dcf1d6e\n--- /dev/null\n+++ b/t/t7599-hooks-allownoverify.sh\n@@ -0,0 +1,217 @@\n+#!/bin/sh\n+\n+test_description='support hooks.allowNoVerify configuration to disallow --no-verify'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository and hooks' '\n+\ttest_commit init &&\n+\ttest_hook --setup pre-commit <<-\\HOOK_EOF &&\n+\techo \"pre-commit executed\" >>pre-commit.log\n+\tif test -f fail-pre-commit\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\ttest_hook --setup pre-push <<-\\HOOK_EOF &&\n+\techo \"pre-push executed\" >>pre-push.log\n+\tif test -f fail-pre-push\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\tgit init --bare remote.git &&\n+\tgit remote add origin remote.git &&\n+\tgit push -u origin main &&\n+\trm -f pre-commit.log pre-push.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change1\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: -n is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change2\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -n -m \"commit with -n (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\trm -f pre-push.log &&\n+\tgit push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'explicit hooks.allowNoVerify=true allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change3\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify allowed\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change4\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit -n' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change5\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -n -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git push --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git push --no-verify origin main 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git merge --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit checkout -b branch-merge main &&\n+\techo \"merge change\" >merge_file &&\n+\tgit add merge_file &&\n+\tgit commit -m \"merge commit\" &&\n+\tgit checkout main &&\n+\ttest_must_fail git merge --no-verify branch-merge -m \"merge fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git rebase --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git rebase --no-verify main branch-merge 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change6\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -m \"normal commit\" &&\n+\ttest_path_is_file pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false enforces hook execution (hook failure prevents commit)' '\n+\ttest_when_finished \"rm -f fail-pre-commit pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\ttouch fail-pre-commit &&\n+\techo \"change7\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -m \"failing hook\" &&\n+\ttest_must_fail git commit --no-verify -m \"cannot bypass\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs pre-push hook on git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit push origin main &&\n+\ttest_path_is_file pre-push.log\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=false overrides local true' '\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change8\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git -c hooks.allowNoVerify=false commit --no-verify -m \"override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'local hooks.allowNoVerify=false overrides global true' '\n+\ttest_config_global hooks.allowNoVerify true &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change9\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"local override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=true overrides local false' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change10\" >>init.t &&\n+\tgit add init.t &&\n+\tgit -c hooks.allowNoVerify=true commit --no-verify -m \"override false with CLI true\"\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false provides emergency override advice' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change11\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail advice\" 2>err &&\n+\ttest_grep \"GIT_ALLOW_NO_VERIFY=1\" err &&\n+\ttest_grep \"git -c hooks.allowNoVerify=true\" err\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change12\" >>init.t &&\n+\tgit add init.t &&\n+\tGIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m \"emergency commit\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tGIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=warn permits --no-verify and warns on stderr' '\n+\ttest_when_finished \"rm -f pre-commit.log err\" &&\n+\ttest_config hooks.allowNoVerify warn &&\n+\techo \"change13\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with warn\" 2>err &&\n+\ttest_path_is_missing pre-commit.log &&\n+\ttest_grep \"bypassing hooks with .--no-verify. is discouraged\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=never disallows --no-verify' '\n+\ttest_config hooks.allowNoVerify never &&\n+\techo \"change14\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail never\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=error disallows --no-verify' '\n+\ttest_config hooks.allowNoVerify error &&\n+\techo \"change15\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail error\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=always allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify always &&\n+\techo \"change16\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit always\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_done\n+\n\nbase-commit: 1630431f326e15fcde608827b5ff38422528eb59\n-- \ngitgitgadget\n"},{"id":"551792","messageId":"pull.2215.v3.git.1788369794965.gitgitgadget@gmail.com","threadId":"66258","inReplyTo":"pull.2215.git.1788365862670.gitgitgadget@gmail.com","subject":"[PATCH v3] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"Alessio Attilio via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T17:23:14Z","receivedAt":"2026-09-02T17:23:18Z","isPatch":true,"body":"From: Alessio Attilio <alessio.attilio@protonmail.com>\n\nIntroduce the 'hooks.allowNoVerify' configuration variable to control\nwhether the '--no-verify' (or '-n') command-line option is permitted\nduring operations executing client-side hooks (commit, push, merge,\nrebase, am).\n\nClient-side hooks execute in the user's local repository and cannot serve\nas an authoritative security boundary; authoritative policy enforcement\nbelongs on the server (such as via pre-receive hooks). However,\ndevelopers often invoke '--no-verify' out of habit or muscle memory,\ninadvertently skipping local checks.\n\nTo address concerns regarding false senses of security without breaking\nlegitimate emergency escape hatches, allow configuring the variable to\n'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits\nthe bypass while emitting a warning to standard error, ensuring\nvisibility without interrupting urgent workflows.\n\nWhen set to 'false', Git aborts execution and provides actionable advice\nexplaining that the setting is an ergonomic workflow guardrail. To avoid\ntrapping developers during broken hook scripts or critical hotfixes,\nthe guardrail can be overridden by passing '-c hooks.allowNoVerify=true'\nor by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This\nprevents developers from having to resort to destructive workarounds\nsuch as removing hook files or clearing execute permissions.\n\nCentralize the option verification logic across all affected commands into\nvalidate_no_verify() in hook.c.\n\nSigned-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n---\n    hooks: introduce 'hooks.allowNoVerify' configuration\n    \n    Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail\n    (default: true) to prevent accidental bypass of hooks via '--no-verify'.\n    This setting is intended for workflows and managed environments to avoid\n    inadvertent bypasses, without altering Git's server-side security model.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2215\n\nRange-diff vs v2:\n\n 1:  4e594568de ! 1:  cc98af8a9a hooks: introduce 'hooks.allowNoVerify' configuration\n     @@ Commit message\n          as an authoritative security boundary; authoritative policy enforcement\n          belongs on the server (such as via pre-receive hooks). However,\n          developers often invoke '--no-verify' out of habit or muscle memory,\n     -    accidentally skipping local checks.\n     +    inadvertently skipping local checks.\n      \n     -    To address both 'security theatre' concerns and avoid breaking legitimate\n     -    emergency escape hatches ('big red button'), this implementation:\n     +    To address concerns regarding false senses of security without breaking\n     +    legitimate emergency escape hatches, allow configuring the variable to\n     +    'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits\n     +    the bypass while emitting a warning to standard error, ensuring\n     +    visibility without interrupting urgent workflows.\n      \n     -    1. Introduces granular values:\n     -       - 'true' (or 'always', default): '--no-verify' is permitted normally.\n     -       - 'warn': '--no-verify' is permitted, but prints a warning to stderr.\n     -       - 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.\n     +    When set to 'false', Git aborts execution and provides actionable advice\n     +    explaining that the setting is an ergonomic workflow guardrail. To avoid\n     +    trapping developers during broken hook scripts or critical hotfixes,\n     +    the guardrail can be overridden by passing '-c hooks.allowNoVerify=true'\n     +    or by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This\n     +    prevents developers from having to resort to destructive workarounds\n     +    such as removing hook files or clearing execute permissions.\n      \n     -    2. Preserves the emergency break-glass escape hatch:\n     -       When configured to 'false', Git does not create a dead-end. It outputs\n     -       actionable advice explaining that the setting is a workflow guardrail\n     -       against accidental bypass, and documents how to override it in an\n     -       emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'.\n     -       This eliminates the need for developers to resort to destructive local\n     -       hacks like deleting hooks or chmod -x.\n     -\n     -    3. Centralizes the validation logic in 'hook.c' and 'hook.h' via\n     -       validate_no_verify().\n     +    Centralize the option verification logic across all affected commands into\n     +    validate_no_verify() in hook.c.\n      \n          Signed-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n      \n     @@ Documentation/config/hooks.adoc (new)\n      +Allowed values are:\n      ++\n      +--\n     -+* `true` (or `always`): `--no-verify` is permitted normally. This is the default.\n     ++* `true`: `--no-verify` is permitted normally. This is the default.\n      +* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.\n     -+* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts\n     ++* `false`: `--no-verify` is disallowed and Git aborts\n      +  with a fatal error accompanied by advice explaining how to override it.\n      +--\n      ++\n     @@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)\n      +\t\treturn;\n      +\n      +\tmaybe_bool = git_parse_maybe_bool(val);\n     -+\tif (maybe_bool == 1 || !strcasecmp(val, \"always\")) {\n     ++\tif (maybe_bool == 1) {\n      +\t\treturn;\n      +\t} else if (!strcasecmp(val, \"warn\")) {\n      +\t\twarning(_(\"bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'\"), opt);\n      +\t\treturn;\n     -+\t} else if (maybe_bool == 0 || !strcasecmp(val, \"never\") || !strcasecmp(val, \"error\")) {\n     ++\t} else if (maybe_bool == 0) {\n      +\t\tadvise(_(\"this repository disallows '%s' as a workflow guardrail against accidental bypass.\\n\"\n      +\t\t\t \"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\\n\"\n      +\t\t\t \"  git -c hooks.allowNoVerify=true <command>\\n\"\n     @@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)\n      +\t\twarning(_(\"unknown value for 'hooks.allowNoVerify': '%s'\"), val);\n      +\t}\n      +}\n     -+\n      \n       ## hook.h ##\n      @@ hook.h: int run_hooks(struct repository *r, const char *hook_name);\n     @@ hook.h: int run_hooks(struct repository *r, const char *hook_name);\n      +void validate_no_verify(struct repository *r, const char *opt);\n      +\n       #endif\n     -+\n      \n       ## t/meson.build ##\n      @@ t/meson.build: integration_tests = [\n     @@ t/t7599-hooks-allownoverify.sh (new)\n      +\ttest_grep \"bypassing hooks with .--no-verify. is discouraged\" err\n      +'\n      +\n     -+test_expect_success 'hooks.allowNoVerify=never disallows --no-verify' '\n     -+\ttest_config hooks.allowNoVerify never &&\n     ++test_expect_success 'hooks.allowNoVerify=0 disallows --no-verify' '\n     ++\ttest_config hooks.allowNoVerify 0 &&\n      +\techo \"change14\" >>init.t &&\n      +\tgit add init.t &&\n     -+\ttest_must_fail git commit --no-verify -m \"fail never\" 2>err &&\n     -+\ttest_grep \"hooks.allowNoVerify\" err\n     -+'\n     -+\n     -+test_expect_success 'hooks.allowNoVerify=error disallows --no-verify' '\n     -+\ttest_config hooks.allowNoVerify error &&\n     -+\techo \"change15\" >>init.t &&\n     -+\tgit add init.t &&\n     -+\ttest_must_fail git commit --no-verify -m \"fail error\" 2>err &&\n     ++\ttest_must_fail git commit --no-verify -m \"fail 0\" 2>err &&\n      +\ttest_grep \"hooks.allowNoVerify\" err\n      +'\n      +\n     -+test_expect_success 'hooks.allowNoVerify=always allows --no-verify' '\n     ++test_expect_success 'hooks.allowNoVerify=1 allows --no-verify' '\n      +\ttest_when_finished \"rm -f pre-commit.log\" &&\n     -+\ttest_config hooks.allowNoVerify always &&\n     -+\techo \"change16\" >>init.t &&\n     ++\ttest_config hooks.allowNoVerify 1 &&\n     ++\techo \"change15\" >>init.t &&\n      +\tgit add init.t &&\n     -+\tgit commit --no-verify -m \"commit always\" &&\n     ++\tgit commit --no-verify -m \"commit 1\" &&\n      +\ttest_path_is_missing pre-commit.log\n      +'\n      +\n      +test_done\n     -+\n\n\n Documentation/config.adoc       |   2 +\n Documentation/config/hooks.adoc |  24 ++++\n Documentation/git.adoc          |   5 +\n builtin/am.c                    |   3 +\n builtin/commit.c                |   4 +\n builtin/merge.c                 |   2 +\n builtin/push.c                  |   3 +\n builtin/rebase.c                |   3 +\n hook.c                          |  29 +++++\n hook.h                          |  12 ++\n t/meson.build                   |   1 +\n t/t7599-hooks-allownoverify.sh  | 208 ++++++++++++++++++++++++++++++++\n 12 files changed, 296 insertions(+)\n create mode 100644 Documentation/config/hooks.adoc\n create mode 100755 t/t7599-hooks-allownoverify.sh\n\ndiff --git a/Documentation/config.adoc b/Documentation/config.adoc\nindex f67dcd2f8e..2ba351e6ee 100644\n--- a/Documentation/config.adoc\n+++ b/Documentation/config.adoc\n@@ -508,6 +508,8 @@ include::config/help.adoc[]\n \n include::config/hook.adoc[]\n \n+include::config/hooks.adoc[]\n+\n include::config/http.adoc[]\n \n include::config/i18n.adoc[]\ndiff --git a/Documentation/config/hooks.adoc b/Documentation/config/hooks.adoc\nnew file mode 100644\nindex 0000000000..d94ac4b9b5\n--- /dev/null\n+++ b/Documentation/config/hooks.adoc\n@@ -0,0 +1,24 @@\n+`hooks.allowNoVerify`::\n+\tSpecifies whether the `--no-verify` (or `-n`) command-line option\n+\tis permitted in commands that run client-side hooks, such as `git commit`,\n+\t`git push`, `git merge`, `git rebase`, and `git am`.\n++\n+Allowed values are:\n++\n+--\n+* `true`: `--no-verify` is permitted normally. This is the default.\n+* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.\n+* `false`: `--no-verify` is disallowed and Git aborts\n+  with a fatal error accompanied by advice explaining how to override it.\n+--\n++\n+In an emergency (for example, when a local hook crashes or during a critical\n+production hotfix), this guardrail can be overridden without modifying\n+configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable\n+or by passing `-c hooks.allowNoVerify=true` on the command line.\n++\n+NOTE: Client-side hooks execute in the developer's environment and belong to\n+the user. This configuration serves strictly as an ergonomic workflow guardrail\n+against accidental bypasses (such as muscle-memory `-n` or automated scripts),\n+and must not be relied upon as a security boundary. Authoritative enforcement\n+must always be implemented server-side (for example, via `pre-receive` hooks).\ndiff --git a/Documentation/git.adoc b/Documentation/git.adoc\nindex 8a5cdd3b3d..1b3af061a3 100644\n--- a/Documentation/git.adoc\n+++ b/Documentation/git.adoc\n@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently\n only affects clones and fetches; it is not yet used for pushes (but may\n be in the future).\n \n+`GIT_ALLOW_NO_VERIFY`::\n+\tIf this Boolean environment variable is set to true (e.g. `1`), permits the use\n+\tof `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.\n+\tThis serves as an emergency override mechanism for workflows when hooks fail unexpectedly.\n+\n `GIT_OPTIONAL_LOCKS`::\n \tIf this Boolean environment variable is set to false, Git will complete any requested operation without\n \tperforming any optional sub-operations that require taking a lock.\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..c79b9a82f0 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2457,6 +2457,9 @@ int cmd_am(int argc,\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \n+\tif (state.no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (binary >= 0)\n \t\tfprintf_ln(stderr, _(\"The -b/--binary option has been a no-op for long time, and\\n\"\n \t\t\t\t\"it will be removed. Please do not use it anymore.\"));\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..ef28c2cb9e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"environment.h\"\n #include \"diff.h\"\n #include \"commit.h\"\n+#include \"hook.h\"\n #include \"add-interactive.h\"\n #include \"gettext.h\"\n #include \"revision.h\"\n@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tfinalize_deferred_config(s);\n \n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (force_author && !strchr(force_author, '>'))\n \t\tforce_author = find_author_by_nickname(force_author);\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..0e6c2d4345 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,\n \t\tparse_branch_merge_options(branch_mergeoptions);\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tif (shortlog_len < 0)\n \t\tshortlog_len = (merge_log_config > 0) ? merge_log_config : 0;\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex 2377b5af55..98830da7f7 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -12,6 +12,7 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n+#include \"hook.h\"\n #include \"refspec.h\"\n #include \"run-command.h\"\n #include \"remote.h\"\n@@ -746,6 +747,8 @@ int cmd_push(int argc,\n \tpacket_trace_identity(\"push\");\n \trepo_config(the_repository, git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n+\tif (flags & TRANSPORT_PUSH_NO_HOOK)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tpush_options = (push_options_cmdline.nr\n \t\t? &push_options_cmdline\n \t\t: &push_options_config);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 10a306310c..dff28f0119 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc,\n \t\t\t     builtin_rebase_options,\n \t\t\t     builtin_rebase_usage, 0);\n \n+\tif (ok_to_skip_pre_rebase)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (options.trailer_args.nr) {\n \t\tif (validate_trailer_args(&options.trailer_args))\n \t\t\tdie(NULL);\ndiff --git a/hook.c b/hook.c\nindex d10eef4763..f972c66bd2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -858,3 +858,32 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)\n \n \treturn run_hooks_opt(r, hook_name, &opt);\n }\n+\n+void validate_no_verify(struct repository *r, const char *opt)\n+{\n+\tconst char *val = NULL;\n+\tint maybe_bool;\n+\n+\tif (git_env_bool(\"GIT_ALLOW_NO_VERIFY\", 0))\n+\t\treturn;\n+\n+\tif (!r || repo_config_get_value(r, \"hooks.allownoverify\", &val))\n+\t\treturn;\n+\n+\tmaybe_bool = git_parse_maybe_bool(val);\n+\tif (maybe_bool == 1) {\n+\t\treturn;\n+\t} else if (!strcasecmp(val, \"warn\")) {\n+\t\twarning(_(\"bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'\"), opt);\n+\t\treturn;\n+\t} else if (maybe_bool == 0) {\n+\t\tadvise(_(\"this repository disallows '%s' as a workflow guardrail against accidental bypass.\\n\"\n+\t\t\t \"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\\n\"\n+\t\t\t \"  git -c hooks.allowNoVerify=true <command>\\n\"\n+\t\t\t \"or:\\n\"\n+\t\t\t \"  GIT_ALLOW_NO_VERIFY=1 git <command>\"), opt);\n+\t\tdie(_(\"the use of '%s' is disabled by 'hooks.allowNoVerify'\"), opt);\n+\t} else {\n+\t\twarning(_(\"unknown value for 'hooks.allowNoVerify': '%s'\"), val);\n+\t}\n+}\ndiff --git a/hook.h b/hook.h\nindex 27bb1aeb2e..b9e0b6703c 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hooks_l(struct repository *r, const char *hook_name, ...);\n+\n+/**\n+ * Check if the use of '--no-verify' (or '-n') is permitted according to\n+ * the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment\n+ * variable.\n+ *\n+ * If permitted, this function returns normally (or emits a warning if configured\n+ * to 'warn'). If disallowed, it outputs advice on how to override the workflow\n+ * guardrail in an emergency, then aborts with die().\n+ */\n+void validate_no_verify(struct repository *r, const char *opt);\n+\n #endif\ndiff --git a/t/meson.build b/t/meson.build\nindex 7f53cca7d1..ce6ca1f6bf 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -945,6 +945,7 @@ integration_tests = [\n   't7526-commit-pathspec-file.sh',\n   't7527-builtin-fsmonitor.sh',\n   't7528-signed-commit-ssh.sh',\n+  't7599-hooks-allownoverify.sh',\n   't7600-merge.sh',\n   't7601-merge-pull-config.sh',\n   't7602-merge-octopus-many.sh',\ndiff --git a/t/t7599-hooks-allownoverify.sh b/t/t7599-hooks-allownoverify.sh\nnew file mode 100755\nindex 0000000000..f02c0b1709\n--- /dev/null\n+++ b/t/t7599-hooks-allownoverify.sh\n@@ -0,0 +1,208 @@\n+#!/bin/sh\n+\n+test_description='support hooks.allowNoVerify configuration to disallow --no-verify'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository and hooks' '\n+\ttest_commit init &&\n+\ttest_hook --setup pre-commit <<-\\HOOK_EOF &&\n+\techo \"pre-commit executed\" >>pre-commit.log\n+\tif test -f fail-pre-commit\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\ttest_hook --setup pre-push <<-\\HOOK_EOF &&\n+\techo \"pre-push executed\" >>pre-push.log\n+\tif test -f fail-pre-push\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\tgit init --bare remote.git &&\n+\tgit remote add origin remote.git &&\n+\tgit push -u origin main &&\n+\trm -f pre-commit.log pre-push.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change1\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: -n is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change2\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -n -m \"commit with -n (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\trm -f pre-push.log &&\n+\tgit push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'explicit hooks.allowNoVerify=true allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change3\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify allowed\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change4\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit -n' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change5\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -n -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git push --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git push --no-verify origin main 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git merge --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit checkout -b branch-merge main &&\n+\techo \"merge change\" >merge_file &&\n+\tgit add merge_file &&\n+\tgit commit -m \"merge commit\" &&\n+\tgit checkout main &&\n+\ttest_must_fail git merge --no-verify branch-merge -m \"merge fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git rebase --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git rebase --no-verify main branch-merge 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change6\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -m \"normal commit\" &&\n+\ttest_path_is_file pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false enforces hook execution (hook failure prevents commit)' '\n+\ttest_when_finished \"rm -f fail-pre-commit pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\ttouch fail-pre-commit &&\n+\techo \"change7\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -m \"failing hook\" &&\n+\ttest_must_fail git commit --no-verify -m \"cannot bypass\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs pre-push hook on git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit push origin main &&\n+\ttest_path_is_file pre-push.log\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=false overrides local true' '\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change8\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git -c hooks.allowNoVerify=false commit --no-verify -m \"override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'local hooks.allowNoVerify=false overrides global true' '\n+\ttest_config_global hooks.allowNoVerify true &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change9\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"local override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=true overrides local false' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change10\" >>init.t &&\n+\tgit add init.t &&\n+\tgit -c hooks.allowNoVerify=true commit --no-verify -m \"override false with CLI true\"\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false provides emergency override advice' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change11\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail advice\" 2>err &&\n+\ttest_grep \"GIT_ALLOW_NO_VERIFY=1\" err &&\n+\ttest_grep \"git -c hooks.allowNoVerify=true\" err\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change12\" >>init.t &&\n+\tgit add init.t &&\n+\tGIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m \"emergency commit\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tGIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=warn permits --no-verify and warns on stderr' '\n+\ttest_when_finished \"rm -f pre-commit.log err\" &&\n+\ttest_config hooks.allowNoVerify warn &&\n+\techo \"change13\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with warn\" 2>err &&\n+\ttest_path_is_missing pre-commit.log &&\n+\ttest_grep \"bypassing hooks with .--no-verify. is discouraged\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=0 disallows --no-verify' '\n+\ttest_config hooks.allowNoVerify 0 &&\n+\techo \"change14\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail 0\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=1 allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify 1 &&\n+\techo \"change15\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit 1\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_done\n\nbase-commit: 1630431f326e15fcde608827b5ff38422528eb59\n-- \ngitgitgadget\n"},{"id":"551793","messageId":"pull.2215.v4.git.1788371123325.gitgitgadget@gmail.com","threadId":"66258","inReplyTo":"pull.2215.git.1788365862670.gitgitgadget@gmail.com","subject":"[PATCH v4] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"Alessio Attilio via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-02T17:45:23Z","receivedAt":"2026-09-02T17:45:27Z","isPatch":true,"body":"From: Alessio Attilio <alessio.attilio@protonmail.com>\n\nIntroduce the 'hooks.allowNoVerify' configuration variable to control\nwhether the '--no-verify' (or '-n') command-line option is permitted\nduring operations executing client-side hooks (commit, push, merge,\nrebase, am).\n\nClient-side hooks execute in the user's local repository and cannot serve\nas an authoritative security boundary; authoritative policy enforcement\nbelongs on the server (such as via pre-receive hooks). However,\ndevelopers often invoke '--no-verify' out of habit or muscle memory,\ninadvertently skipping local checks.\n\nTo address concerns regarding false senses of security without breaking\nlegitimate emergency escape hatches, allow configuring the variable to\n'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits\nthe bypass while emitting a warning to standard error, ensuring\nvisibility without interrupting urgent workflows.\n\nWhen set to 'false', Git aborts execution and provides actionable advice\nexplaining that the setting is an ergonomic workflow guardrail. To avoid\ntrapping developers during broken hook scripts or critical hotfixes,\nthe guardrail can be overridden by passing '-c hooks.allowNoVerify=true'\nor by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This\nprevents developers from having to resort to destructive workarounds\nsuch as removing hook files or clearing execute permissions.\n\nCentralize the option verification logic across all affected commands into\nvalidate_no_verify() in hook.c.\n\nSigned-off-by: Alessio Attilio <alessio.attilio@protonmail.com>\n---\n    hooks: introduce 'hooks.allowNoVerify' configuration\n    \n    Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail\n    (default: true) to prevent accidental bypass of hooks via '--no-verify'.\n    This setting is intended for workflows and managed environments to avoid\n    inadvertent bypasses, without altering Git's server-side security model.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2215\n\nRange-diff vs v3:\n\n 1:  cc98af8a9a ! 1:  a9f75413d3 hooks: introduce 'hooks.allowNoVerify' configuration\n     @@ t/t7599-hooks-allownoverify.sh (new)\n      +\ttest_grep \"hooks.allowNoVerify\" err\n      +'\n      +\n     ++test_expect_success 'hooks.allowNoVerify=false disallows git am --no-verify' '\n     ++\ttest_when_finished \"rm -f patch && git am --abort || true\" &&\n     ++\ttest_config hooks.allowNoVerify false &&\n     ++\tgit format-patch -1 --stdout branch-merge >patch &&\n     ++\ttest_must_fail git am --no-verify patch 2>err &&\n     ++\ttest_grep \"hooks.allowNoVerify\" err\n     ++'\n     ++\n      +test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '\n      +\ttest_when_finished \"rm -f pre-commit.log\" &&\n      +\ttest_config hooks.allowNoVerify false &&\n\n\n Documentation/config.adoc       |   2 +\n Documentation/config/hooks.adoc |  24 ++++\n Documentation/git.adoc          |   5 +\n builtin/am.c                    |   3 +\n builtin/commit.c                |   4 +\n builtin/merge.c                 |   2 +\n builtin/push.c                  |   3 +\n builtin/rebase.c                |   3 +\n hook.c                          |  29 +++++\n hook.h                          |  12 ++\n t/meson.build                   |   1 +\n t/t7599-hooks-allownoverify.sh  | 216 ++++++++++++++++++++++++++++++++\n 12 files changed, 304 insertions(+)\n create mode 100644 Documentation/config/hooks.adoc\n create mode 100755 t/t7599-hooks-allownoverify.sh\n\ndiff --git a/Documentation/config.adoc b/Documentation/config.adoc\nindex f67dcd2f8e..2ba351e6ee 100644\n--- a/Documentation/config.adoc\n+++ b/Documentation/config.adoc\n@@ -508,6 +508,8 @@ include::config/help.adoc[]\n \n include::config/hook.adoc[]\n \n+include::config/hooks.adoc[]\n+\n include::config/http.adoc[]\n \n include::config/i18n.adoc[]\ndiff --git a/Documentation/config/hooks.adoc b/Documentation/config/hooks.adoc\nnew file mode 100644\nindex 0000000000..d94ac4b9b5\n--- /dev/null\n+++ b/Documentation/config/hooks.adoc\n@@ -0,0 +1,24 @@\n+`hooks.allowNoVerify`::\n+\tSpecifies whether the `--no-verify` (or `-n`) command-line option\n+\tis permitted in commands that run client-side hooks, such as `git commit`,\n+\t`git push`, `git merge`, `git rebase`, and `git am`.\n++\n+Allowed values are:\n++\n+--\n+* `true`: `--no-verify` is permitted normally. This is the default.\n+* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.\n+* `false`: `--no-verify` is disallowed and Git aborts\n+  with a fatal error accompanied by advice explaining how to override it.\n+--\n++\n+In an emergency (for example, when a local hook crashes or during a critical\n+production hotfix), this guardrail can be overridden without modifying\n+configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable\n+or by passing `-c hooks.allowNoVerify=true` on the command line.\n++\n+NOTE: Client-side hooks execute in the developer's environment and belong to\n+the user. This configuration serves strictly as an ergonomic workflow guardrail\n+against accidental bypasses (such as muscle-memory `-n` or automated scripts),\n+and must not be relied upon as a security boundary. Authoritative enforcement\n+must always be implemented server-side (for example, via `pre-receive` hooks).\ndiff --git a/Documentation/git.adoc b/Documentation/git.adoc\nindex 8a5cdd3b3d..1b3af061a3 100644\n--- a/Documentation/git.adoc\n+++ b/Documentation/git.adoc\n@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently\n only affects clones and fetches; it is not yet used for pushes (but may\n be in the future).\n \n+`GIT_ALLOW_NO_VERIFY`::\n+\tIf this Boolean environment variable is set to true (e.g. `1`), permits the use\n+\tof `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.\n+\tThis serves as an emergency override mechanism for workflows when hooks fail unexpectedly.\n+\n `GIT_OPTIONAL_LOCKS`::\n \tIf this Boolean environment variable is set to false, Git will complete any requested operation without\n \tperforming any optional sub-operations that require taking a lock.\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e9623b8307..c79b9a82f0 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2457,6 +2457,9 @@ int cmd_am(int argc,\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \n+\tif (state.no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (binary >= 0)\n \t\tfprintf_ln(stderr, _(\"The -b/--binary option has been a no-op for long time, and\\n\"\n \t\t\t\t\"it will be removed. Please do not use it anymore.\"));\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 28f6174503..ef28c2cb9e 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"environment.h\"\n #include \"diff.h\"\n #include \"commit.h\"\n+#include \"hook.h\"\n #include \"add-interactive.h\"\n #include \"gettext.h\"\n #include \"revision.h\"\n@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tfinalize_deferred_config(s);\n \n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (force_author && !strchr(force_author, '>'))\n \t\tforce_author = find_author_by_nickname(force_author);\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5b4eb23a83..0e6c2d4345 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,\n \t\tparse_branch_merge_options(branch_mergeoptions);\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\tif (no_verify)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tif (shortlog_len < 0)\n \t\tshortlog_len = (merge_log_config > 0) ? merge_log_config : 0;\n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex 2377b5af55..98830da7f7 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -12,6 +12,7 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n+#include \"hook.h\"\n #include \"refspec.h\"\n #include \"run-command.h\"\n #include \"remote.h\"\n@@ -746,6 +747,8 @@ int cmd_push(int argc,\n \tpacket_trace_identity(\"push\");\n \trepo_config(the_repository, git_push_config, &flags);\n \targc = parse_options(argc, argv, prefix, options, push_usage, 0);\n+\tif (flags & TRANSPORT_PUSH_NO_HOOK)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n \tpush_options = (push_options_cmdline.nr\n \t\t? &push_options_cmdline\n \t\t: &push_options_config);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 10a306310c..dff28f0119 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc,\n \t\t\t     builtin_rebase_options,\n \t\t\t     builtin_rebase_usage, 0);\n \n+\tif (ok_to_skip_pre_rebase)\n+\t\tvalidate_no_verify(the_repository, \"--no-verify\");\n+\n \tif (options.trailer_args.nr) {\n \t\tif (validate_trailer_args(&options.trailer_args))\n \t\t\tdie(NULL);\ndiff --git a/hook.c b/hook.c\nindex d10eef4763..f972c66bd2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -858,3 +858,32 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)\n \n \treturn run_hooks_opt(r, hook_name, &opt);\n }\n+\n+void validate_no_verify(struct repository *r, const char *opt)\n+{\n+\tconst char *val = NULL;\n+\tint maybe_bool;\n+\n+\tif (git_env_bool(\"GIT_ALLOW_NO_VERIFY\", 0))\n+\t\treturn;\n+\n+\tif (!r || repo_config_get_value(r, \"hooks.allownoverify\", &val))\n+\t\treturn;\n+\n+\tmaybe_bool = git_parse_maybe_bool(val);\n+\tif (maybe_bool == 1) {\n+\t\treturn;\n+\t} else if (!strcasecmp(val, \"warn\")) {\n+\t\twarning(_(\"bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'\"), opt);\n+\t\treturn;\n+\t} else if (maybe_bool == 0) {\n+\t\tadvise(_(\"this repository disallows '%s' as a workflow guardrail against accidental bypass.\\n\"\n+\t\t\t \"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\\n\"\n+\t\t\t \"  git -c hooks.allowNoVerify=true <command>\\n\"\n+\t\t\t \"or:\\n\"\n+\t\t\t \"  GIT_ALLOW_NO_VERIFY=1 git <command>\"), opt);\n+\t\tdie(_(\"the use of '%s' is disabled by 'hooks.allowNoVerify'\"), opt);\n+\t} else {\n+\t\twarning(_(\"unknown value for 'hooks.allowNoVerify': '%s'\"), val);\n+\t}\n+}\ndiff --git a/hook.h b/hook.h\nindex 27bb1aeb2e..b9e0b6703c 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hooks_l(struct repository *r, const char *hook_name, ...);\n+\n+/**\n+ * Check if the use of '--no-verify' (or '-n') is permitted according to\n+ * the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment\n+ * variable.\n+ *\n+ * If permitted, this function returns normally (or emits a warning if configured\n+ * to 'warn'). If disallowed, it outputs advice on how to override the workflow\n+ * guardrail in an emergency, then aborts with die().\n+ */\n+void validate_no_verify(struct repository *r, const char *opt);\n+\n #endif\ndiff --git a/t/meson.build b/t/meson.build\nindex 7f53cca7d1..ce6ca1f6bf 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -945,6 +945,7 @@ integration_tests = [\n   't7526-commit-pathspec-file.sh',\n   't7527-builtin-fsmonitor.sh',\n   't7528-signed-commit-ssh.sh',\n+  't7599-hooks-allownoverify.sh',\n   't7600-merge.sh',\n   't7601-merge-pull-config.sh',\n   't7602-merge-octopus-many.sh',\ndiff --git a/t/t7599-hooks-allownoverify.sh b/t/t7599-hooks-allownoverify.sh\nnew file mode 100755\nindex 0000000000..75e6e65ef0\n--- /dev/null\n+++ b/t/t7599-hooks-allownoverify.sh\n@@ -0,0 +1,216 @@\n+#!/bin/sh\n+\n+test_description='support hooks.allowNoVerify configuration to disallow --no-verify'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup test repository and hooks' '\n+\ttest_commit init &&\n+\ttest_hook --setup pre-commit <<-\\HOOK_EOF &&\n+\techo \"pre-commit executed\" >>pre-commit.log\n+\tif test -f fail-pre-commit\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\ttest_hook --setup pre-push <<-\\HOOK_EOF &&\n+\techo \"pre-push executed\" >>pre-push.log\n+\tif test -f fail-pre-push\n+\tthen\n+\t\texit 1\n+\tfi\n+\texit 0\n+\tHOOK_EOF\n+\tgit init --bare remote.git &&\n+\tgit remote add origin remote.git &&\n+\tgit push -u origin main &&\n+\trm -f pre-commit.log pre-push.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change1\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: -n is permitted for git commit' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\techo \"change2\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -n -m \"commit with -n (default)\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'default: --no-verify is permitted for git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\trm -f pre-push.log &&\n+\tgit push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'explicit hooks.allowNoVerify=true allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change3\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with no-verify allowed\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change4\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git commit -n' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change5\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -n -m \"should fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git push --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git push --no-verify origin main 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git merge --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit checkout -b branch-merge main &&\n+\techo \"merge change\" >merge_file &&\n+\tgit add merge_file &&\n+\tgit commit -m \"merge commit\" &&\n+\tgit checkout main &&\n+\ttest_must_fail git merge --no-verify branch-merge -m \"merge fail\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git rebase --no-verify' '\n+\ttest_config hooks.allowNoVerify false &&\n+\ttest_must_fail git rebase --no-verify main branch-merge 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false disallows git am --no-verify' '\n+\ttest_when_finished \"rm -f patch && git am --abort || true\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit format-patch -1 --stdout branch-merge >patch &&\n+\ttest_must_fail git am --no-verify patch 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change6\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit -m \"normal commit\" &&\n+\ttest_path_is_file pre-commit.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false enforces hook execution (hook failure prevents commit)' '\n+\ttest_when_finished \"rm -f fail-pre-commit pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\ttouch fail-pre-commit &&\n+\techo \"change7\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit -m \"failing hook\" &&\n+\ttest_must_fail git commit --no-verify -m \"cannot bypass\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false still runs pre-push hook on git push' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tgit push origin main &&\n+\ttest_path_is_file pre-push.log\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=false overrides local true' '\n+\ttest_config hooks.allowNoVerify true &&\n+\techo \"change8\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git -c hooks.allowNoVerify=false commit --no-verify -m \"override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'local hooks.allowNoVerify=false overrides global true' '\n+\ttest_config_global hooks.allowNoVerify true &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change9\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"local override\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'CLI -c hooks.allowNoVerify=true overrides local false' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change10\" >>init.t &&\n+\tgit add init.t &&\n+\tgit -c hooks.allowNoVerify=true commit --no-verify -m \"override false with CLI true\"\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=false provides emergency override advice' '\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change11\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail advice\" 2>err &&\n+\ttest_grep \"GIT_ALLOW_NO_VERIFY=1\" err &&\n+\ttest_grep \"git -c hooks.allowNoVerify=true\" err\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\techo \"change12\" >>init.t &&\n+\tgit add init.t &&\n+\tGIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m \"emergency commit\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '\n+\ttest_when_finished \"rm -f pre-push.log\" &&\n+\ttest_config hooks.allowNoVerify false &&\n+\tGIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&\n+\ttest_path_is_missing pre-push.log\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=warn permits --no-verify and warns on stderr' '\n+\ttest_when_finished \"rm -f pre-commit.log err\" &&\n+\ttest_config hooks.allowNoVerify warn &&\n+\techo \"change13\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit with warn\" 2>err &&\n+\ttest_path_is_missing pre-commit.log &&\n+\ttest_grep \"bypassing hooks with .--no-verify. is discouraged\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=0 disallows --no-verify' '\n+\ttest_config hooks.allowNoVerify 0 &&\n+\techo \"change14\" >>init.t &&\n+\tgit add init.t &&\n+\ttest_must_fail git commit --no-verify -m \"fail 0\" 2>err &&\n+\ttest_grep \"hooks.allowNoVerify\" err\n+'\n+\n+test_expect_success 'hooks.allowNoVerify=1 allows --no-verify' '\n+\ttest_when_finished \"rm -f pre-commit.log\" &&\n+\ttest_config hooks.allowNoVerify 1 &&\n+\techo \"change15\" >>init.t &&\n+\tgit add init.t &&\n+\tgit commit --no-verify -m \"commit 1\" &&\n+\ttest_path_is_missing pre-commit.log\n+'\n+\n+test_done\n\nbase-commit: 1630431f326e15fcde608827b5ff38422528eb59\n-- \ngitgitgadget\n"},{"id":"551801","messageId":"xmqqa4pzihnm.fsf@gitster.g","threadId":"66258","inReplyTo":"pull.2215.git.1788365862670.gitgitgadget@gmail.com","subject":"Re: [PATCH] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-02T19:21:01Z","receivedAt":"2026-09-02T19:21:04Z","isPatch":true,"body":"\"Alessio Attilio via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Alessio Attilio <alessio.attilio@protonmail.com>\n>\n> Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to\n> prevent accidental bypass of hooks with '--no-verify' when set to false.\n> Authoritative enforcement remains server-side.\n\nAccidental\n\n\t$ git foo --no-verify\n\nmay be prevented by setting this configuration variable, but then\nwould we need another layer of protection to prevent accidental\n\n\t$ git -c hooks.allownoverify foo --no-verify\n\nby introducing another configuration variable to forbid\nhooks.allownoverify to be overriden?\n\nI do not think we want to go into this slipperly slope.  Thanks for\nsending a patch, but I am personally not interested.\n\nBesides, verifications that users may be tempted to bypass, but want\nto instill discipline to prevent bypassing, may not be implemented\nas hooks, and the way they are bypassed may not be \"--[no-]verify\"\ncommand line option.  When one wants a way to prevent such\nverifications from getting disabled, the mechanism should also allow\nforbidding verification that is built into the system from getting\ndisabled.  Limiting a settings to hooks is probably not a good idea,\nand introducing a new \"hooks.\" hierarchy for this setting is not\nsomething we want to see.\n\n"},{"id":"551812","messageId":"apif0QhIHkAAXJmn@fruit.crustytoothpaste.net","threadId":"66258","inReplyTo":"pull.2215.v3.git.1788369794965.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] hooks: introduce 'hooks.allowNoVerify' configuration","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-09-02T22:14:41Z","receivedAt":"2026-09-02T22:22:19Z","isPatch":true,"body":"On 2026-09-02 at 17:23:14, Alessio Attilio via GitGitGadget wrote:\n> From: Alessio Attilio <alessio.attilio@protonmail.com>\n> \n> Introduce the 'hooks.allowNoVerify' configuration variable to control\n> whether the '--no-verify' (or '-n') command-line option is permitted\n> during operations executing client-side hooks (commit, push, merge,\n> rebase, am).\n> \n> Client-side hooks execute in the user's local repository and cannot serve\n> as an authoritative security boundary; authoritative policy enforcement\n> belongs on the server (such as via pre-receive hooks). However,\n> developers often invoke '--no-verify' out of habit or muscle memory,\n> inadvertently skipping local checks.\n\nI agree with Junio that this doesn't seem like a good idea.  It's up to\nthe user whether they want to install or use hooks and they are free to\ndisable them or override them as they see fit.  If the user doesn't want\nto use local hooks on an individual case basis, then `--no-verify` is\nthe right option.\n\nIn my case, I use hooks for Git LFS when I use that software, but I\nnever use repository owner-provided hooks, although I may use my own. As\na result, I almost never use `--no-verify`.\n\nIf in your environment you are trying to force developers to use local\nhooks, the Git FAQ mentions that this is not an effective control and\nyou should stop trying to do that.  In fact, I would even argue that you\nshould simply not install hooks by default for repositories using your\nscripts or build tools because it's up to users whether those are useful\nfor them.  The Git FAQ mentions reasons why forced installation of hooks\nis harmful for many workflows and I don't think we should be hassling\nusers like that.\n\nIn any event, it's trivial to simply bypass all hooks by setting\n`core.hooksPath` to `/dev/null` using a variety of different techniques.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"}]}