hooks: introduce 'hooks.allowNoVerify' configuration
Subsystems (2)
PATCH 1/1 hooks: introduce 'hooks.allowNoVerify' configuration
From: Alessio Attilio <[email protected]>
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to prevent accidental bypass of hooks with '--no-verify' when set to false. Authoritative enforcement remains server-side.
---
hooks: introduce 'hooks.allowNoVerify' configuration
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail (default: true) to prevent accidental bypass of hooks via '--no-verify'. This setting is intended for workflows and managed environments to avoid inadvertent bypasses, without altering Git's server-side security model.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2215
Documentation/config.adoc | 2 + Documentation/config/hooks.adoc | 10 +++ builtin/am.c | 5 ++ builtin/commit.c | 8 ++ builtin/merge.c | 6 ++ builtin/push.c | 6 ++ builtin/rebase.c | 10 +++ t/meson.build | 1 + t/t7599-hooks-allownoverify.sh | 149 ++++++++++++++++++++++++++++++++ 9 files changed, 197 insertions(+) create mode 100644 Documentation/config/hooks.adoc create mode 100755 t/t7599-hooks-allownoverify.sh
Documentation/config.adoc +2 -0
@@ -508,6 +508,8 @@ include::config/help.adoc[] include::config/hook.adoc[] +include::config/hooks.adoc[] + include::config/http.adoc[] include::config/i18n.adoc[]
Documentation/config/hooks.adoc new file +10 -0
@@ -0,0 +1,10 @@ +`hooks.allowNoVerify`:: + A boolean to specify whether `--no-verify` (or `-n`) command-line + option is permitted in commands such as `git commit` and `git push`. + When set to `false`, attempting to bypass hooks with `--no-verify` + will cause Git to abort immediately with a fatal error. Defaults to + `true`. ++ +Note that this setting serves as an opt-in workflow guardrail against +accidental bypasses (for example in managed environments or CI runners), +and does not replace authoritative server-side hook enforcement.
builtin/am.c +5 -0
@@ -2320,6 +2320,7 @@ int cmd_am(int argc,int patch_format =PATCH_FORMAT_UNKNOWN ;enum resume_type resume_mode =RESUME_FALSE ;int in_progress; +int allow_no_verify =1 ;int ret =0 ;const char *const usage[] = {
@@ -2448,6 +2449,7 @@ int cmd_am(int argc,show_usage_with_options_if_asked (argc, argv, usage, options);repo_config (the_repository, git_default_config,NULL ); +repo_config_get_bool (the_repository,"hooks.allownoverify" , &allow_no_verify);am_state_init (&state);
@@ -2457,6 +2459,9 @@ int cmd_am(int argc, argc =parse_options (argc, argv, prefix, options, usage,0 ); +if (state.no_verify && !allow_no_verify) +die (_ ("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'" )); +if (binary >=0 )fprintf_ln (stderr,_ ("The -b/--binary option has been a no-op for long time, and\n" "it will be removed. Please do not use it anymore." ));
builtin/commit.c +8 -0
@@ -127,6 +127,7 @@ static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;static int edit_flag =-1 ;/* unspecified */ static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;static int config_commit_verbose =-1 ;/* unspecified */ +static int allow_no_verify =1 ;static int no_post_rewrite, allow_empty_message, pathspec_file_nul;static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;static const char *sign_commit, *pathspec_from_file;
@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[], argc =parse_options (argc, argv, prefix, options, usage,0 );finalize_deferred_config (s); +if (no_verify && !allow_no_verify) +die (_ ("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'" )); +if (force_author && !strchr (force_author,'>' )) force_author =find_author_by_nickname (force_author);
@@ -1691,6 +1695,10 @@ static int git_commit_config(const char *k, const char *v, &is_bool);return 0 ; } +if (!strcmp (k,"hooks.allownoverify" )) { + allow_no_verify =git_config_bool (k, v); +return 0 ; + }return git_status_config (k, v, ctx, s); }
builtin/merge.c +6 -0
@@ -96,6 +96,7 @@ static int signoff;static const char *sign_commit;static int autostash;static int no_verify; +static int allow_no_verify =1 ;static char *into_name;static struct strategy all_strategy[] = {
@@ -727,6 +728,9 @@ static int git_merge_config(const char *k, const char *v, }else if (!strcmp (k,"commit.gpgsign" )) { sign_commit =git_config_bool (k, v) ?"" :NULL ;return 0 ; + }else if (!strcmp (k,"hooks.allownoverify" )) { + allow_no_verify =git_config_bool (k, v); +return 0 ; }else if (!strcmp (k,"gpg.mintrustlevel" )) { check_trust_level =0 ; }else if (!strcmp (k,"merge.autostash" )) {
@@ -1408,6 +1412,8 @@ int cmd_merge(int argc,parse_branch_merge_options (branch_mergeoptions); argc =parse_options (argc, argv, prefix, builtin_merge_options, builtin_merge_usage,0 ); +if (no_verify && !allow_no_verify) +die (_ ("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'" ));if (shortlog_len <0 ) shortlog_len = (merge_log_config >0 ) ? merge_log_config :0 ;
builtin/push.c +6 -0
@@ -63,6 +63,7 @@ static int verbosity;static int progress =-1 ;static int recurse_submodules =RECURSE_SUBMODULES_DEFAULT ;static enum transport_family family; +static int allow_no_verify =1 ;static struct push_cas_option cas;
@@ -543,6 +544,9 @@ static int git_push_config(const char *k, const char *v,else *flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES ;return 0 ; + }else if (!strcmp (k,"hooks.allownoverify" )) { + allow_no_verify =git_config_bool (k, v); +return 0 ; }return git_default_config (k, v, ctx,NULL );
@@ -746,6 +750,8 @@ int cmd_push(int argc,packet_trace_identity ("push" );repo_config (the_repository, git_push_config, &flags); argc =parse_options (argc, argv, prefix, options, push_usage,0 ); +if ((flags &TRANSPORT_PUSH_NO_HOOK ) && !allow_no_verify) +die (_ ("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'" )); push_options = (push_options_cmdline.nr ? &push_options_cmdline : &push_options_config);
builtin/rebase.c +10 -0
@@ -790,6 +790,8 @@ static void parse_rebase_merges_value(struct rebase_options *options, const chardie (_ ("Unknown rebase-merges mode: %s" ), value); } +static int allow_no_verify =1 ; +static int rebase_config (const char *var,const char *value,const struct config_context *ctx,void *data) {
@@ -820,6 +822,11 @@ static int rebase_config(const char *var, const char *value,return 0 ; } +if (!strcmp (var,"hooks.allownoverify" )) { + allow_no_verify =git_config_bool (var, value); +return 0 ; + } +if (!strcmp (var,"rebase.rebasemerges" )) { opts->config_rebase_merges =git_parse_maybe_bool (value);if (opts->config_rebase_merges <0 ) {
@@ -1299,6 +1306,9 @@ int cmd_rebase(int argc, builtin_rebase_options, builtin_rebase_usage, 0); +if (ok_to_skip_pre_rebase && !allow_no_verify) +die (_ ("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'" )); +if (options.trailer_args.nr) {if (validate_trailer_args (&options.trailer_args))die (NULL );
t/meson.build +1 -0
@@ -945,6 +945,7 @@ integration_tests = [ 't7526-commit-pathspec-file.sh', 't7527-builtin-fsmonitor.sh', 't7528-signed-commit-ssh.sh', + 't7599-hooks-allownoverify.sh', 't7600-merge.sh', 't7601-merge-pull-config.sh', 't7602-merge-octopus-many.sh',
t/t7599-hooks-allownoverify.sh new file +149 -0
149 lines not on this page · raw message
base-commit: 1630431f326e15fcde608827b5ff38422528eb59
Signature · 1 line
gitgitgadget
PATCH v2 1/1 hooks: introduce 'hooks.allowNoVerify' configuration
From: Alessio Attilio <[email protected]>
Introduce the 'hooks.allowNoVerify' configuration variable to control whether the '--no-verify' (or '-n') command-line option is permitted during operations executing client-side hooks (commit, push, merge, rebase, am).
Client-side hooks execute in the user's local repository and cannot serve as an authoritative security boundary; authoritative policy enforcement belongs on the server (such as via pre-receive hooks). However, developers often invoke '--no-verify' out of habit or muscle memory, accidentally skipping local checks.
To address both 'security theatre' concerns and avoid breaking legitimate emergency escape hatches ('big red button'), this implementation:
1. Introduces granular values:
- 'true' (or 'always', default): '--no-verify' is permitted normally.
- 'warn': '--no-verify' is permitted, but prints a warning to stderr.
- 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.
2. Preserves the emergency break-glass escape hatch:
When configured to 'false', Git does not create a dead-end. It outputs actionable advice explaining that the setting is a workflow guardrail against accidental bypass, and documents how to override it in an emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'. This eliminates the need for developers to resort to destructive local hacks like deleting hooks or chmod -x.
3. Centralizes the validation logic in 'hook.c' and 'hook.h' via validate_no_verify().
---
hooks: introduce 'hooks.allowNoVerify' configuration
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail (default: true) to prevent accidental bypass of hooks via '--no-verify'. This setting is intended for workflows and managed environments to avoid inadvertent bypasses, without altering Git's server-side security model.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2215
Range-diff vs v1:
1: 853636dad0 ! 1: 4e594568de hooks: introduce 'hooks.allowNoVerify' configuration
@@ Metadata
## Commit message ##
hooks: introduce 'hooks.allowNoVerify' configuration
- Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to
- prevent accidental bypass of hooks with '--no-verify' when set to false.
- Authoritative enforcement remains server-side.
+ Introduce the 'hooks.allowNoVerify' configuration variable to control
+ whether the '--no-verify' (or '-n') command-line option is permitted
+ during operations executing client-side hooks (commit, push, merge,
+ rebase, am).
+
+ Client-side hooks execute in the user's local repository and cannot serve
+ as an authoritative security boundary; authoritative policy enforcement
+ belongs on the server (such as via pre-receive hooks). However,
+ developers often invoke '--no-verify' out of habit or muscle memory,
+ accidentally skipping local checks.
+
+ To address both 'security theatre' concerns and avoid breaking legitimate
+ emergency escape hatches ('big red button'), this implementation:
+
+ 1. Introduces granular values:
+ - 'true' (or 'always', default): '--no-verify' is permitted normally.
+ - 'warn': '--no-verify' is permitted, but prints a warning to stderr.
+ - 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.
+
+ 2. Preserves the emergency break-glass escape hatch:
+ When configured to 'false', Git does not create a dead-end. It outputs
+ actionable advice explaining that the setting is a workflow guardrail
+ against accidental bypass, and documents how to override it in an
+ emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'.
+ This eliminates the need for developers to resort to destructive local
+ hacks like deleting hooks or chmod -x.
+
+ 3. Centralizes the validation logic in 'hook.c' and 'hook.h' via
+ validate_no_verify().
Signed-off-by: Alessio Attilio <[email protected]>
@@ Documentation/config.adoc: include::config/help.adoc[]
## Documentation/config/hooks.adoc (new) ##
@@
+`hooks.allowNoVerify`::
-+ A boolean to specify whether `--no-verify` (or `-n`) command-line
-+ option is permitted in commands such as `git commit` and `git push`.
-+ When set to `false`, attempting to bypass hooks with `--no-verify`
-+ will cause Git to abort immediately with a fatal error. Defaults to
-+ `true`.
++ Specifies whether the `--no-verify` (or `-n`) command-line option
++ is permitted in commands that run client-side hooks, such as `git commit`,
++ `git push`, `git merge`, `git rebase`, and `git am`.
++
-+Note that this setting serves as an opt-in workflow guardrail against
-+accidental bypasses (for example in managed environments or CI runners),
-+and does not replace authoritative server-side hook enforcement.
++Allowed values are:
+++
++--
++* `true` (or `always`): `--no-verify` is permitted normally. This is the default.
++* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.
++* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts
++ with a fatal error accompanied by advice explaining how to override it.
++--
+++
++In an emergency (for example, when a local hook crashes or during a critical
++production hotfix), this guardrail can be overridden without modifying
++configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable
++or by passing `-c hooks.allowNoVerify=true` on the command line.
+++
++NOTE: Client-side hooks execute in the developer's environment and belong to
++the user. This configuration serves strictly as an ergonomic workflow guardrail
++against accidental bypasses (such as muscle-memory `-n` or automated scripts),
++and must not be relied upon as a security boundary. Authoritative enforcement
++must always be implemented server-side (for example, via `pre-receive` hooks).
- ## builtin/am.c ##
-@@ builtin/am.c: int cmd_am(int argc,
- int patch_format = PATCH_FORMAT_UNKNOWN;
- enum resume_type resume_mode = RESUME_FALSE;
- int in_progress;
-+ int allow_no_verify = 1;
- int ret = 0;
-
- const char * const usage[] = {
-@@ builtin/am.c: int cmd_am(int argc,
- show_usage_with_options_if_asked(argc, argv, usage, options);
-
- repo_config(the_repository, git_default_config, NULL);
-+ repo_config_get_bool(the_repository, "hooks.allownoverify", &allow_no_verify);
-
- am_state_init(&state);
+ ## Documentation/git.adoc ##
+@@ Documentation/git.adoc: on some performance improvements or features). This variable currently
+ only affects clones and fetches; it is not yet used for pushes (but may
+ be in the future).
++`GIT_ALLOW_NO_VERIFY`::
++ If this Boolean environment variable is set to true (e.g. `1`), permits the use
++ of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.
++ This serves as an emergency override mechanism for workflows when hooks fail unexpectedly.
++
+ `GIT_OPTIONAL_LOCKS`::
+ If this Boolean environment variable is set to false, Git will complete any requested operation without
+ performing any optional sub-operations that require taking a lock.
+
+ ## builtin/am.c ##
@@ builtin/am.c: int cmd_am(int argc,
argc = parse_options(argc, argv, prefix, options, usage, 0);
-+ if (state.no_verify && !allow_no_verify)
-+ die(_("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'"));
++ if (state.no_verify)
++ validate_no_verify(the_repository, "--no-verify");
+
if (binary >= 0)
fprintf_ln(stderr, _("The -b/--binary option has been a no-op for long time, and\n"
"it will be removed. Please do not use it anymore."));
## builtin/commit.c ##
-@@ builtin/commit.c: static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
- static int edit_flag = -1; /* unspecified */
- static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
- static int config_commit_verbose = -1; /* unspecified */
-+static int allow_no_verify = 1;
- static int no_post_rewrite, allow_empty_message, pathspec_file_nul;
- static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;
- static const char *sign_commit, *pathspec_from_file;
+@@
+ #include "environment.h"
+ #include "diff.h"
+ #include "commit.h"
++#include "hook.h"
+ #include "add-interactive.h"
+ #include "gettext.h"
+ #include "revision.h"
@@ builtin/commit.c: static int parse_and_validate_options(int argc, const char *argv[],
argc = parse_options(argc, argv, prefix, options, usage, 0);
finalize_deferred_config(s);
-+ if (no_verify && !allow_no_verify)
-+ die(_("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'"));
++ if (no_verify)
++ validate_no_verify(the_repository, "--no-verify");
+
if (force_author && !strchr(force_author, '>'))
force_author = find_author_by_nickname(force_author);
-@@ builtin/commit.c: static int git_commit_config(const char *k, const char *v,
- &is_bool);
- return 0;
- }
-+ if (!strcmp(k, "hooks.allownoverify")) {
-+ allow_no_verify = git_config_bool(k, v);
-+ return 0;
-+ }
-
- return git_status_config(k, v, ctx, s);
- }
## builtin/merge.c ##
-@@ builtin/merge.c: static int signoff;
- static const char *sign_commit;
- static int autostash;
- static int no_verify;
-+static int allow_no_verify = 1;
- static char *into_name;
-
- static struct strategy all_strategy[] = {
-@@ builtin/merge.c: static int git_merge_config(const char *k, const char *v,
- } else if (!strcmp(k, "commit.gpgsign")) {
- sign_commit = git_config_bool(k, v) ? "" : NULL;
- return 0;
-+ } else if (!strcmp(k, "hooks.allownoverify")) {
-+ allow_no_verify = git_config_bool(k, v);
-+ return 0;
- } else if (!strcmp(k, "gpg.mintrustlevel")) {
- check_trust_level = 0;
- } else if (!strcmp(k, "merge.autostash")) {
@@ builtin/merge.c: int cmd_merge(int argc,
parse_branch_merge_options(branch_mergeoptions);
argc = parse_options(argc, argv, prefix, builtin_merge_options,
builtin_merge_usage, 0);
-+ if (no_verify && !allow_no_verify)
-+ die(_("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'"));
++ if (no_verify)
++ validate_no_verify(the_repository, "--no-verify");
if (shortlog_len < 0)
shortlog_len = (merge_log_config > 0) ? merge_log_config : 0;
## builtin/push.c ##
-@@ builtin/push.c: static int verbosity;
- static int progress = -1;
- static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;
- static enum transport_family family;
-+static int allow_no_verify = 1;
-
- static struct push_cas_option cas;
-
-@@ builtin/push.c: static int git_push_config(const char *k, const char *v,
- else
- *flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES;
- return 0;
-+ } else if (!strcmp(k, "hooks.allownoverify")) {
-+ allow_no_verify = git_config_bool(k, v);
-+ return 0;
- }
-
- return git_default_config(k, v, ctx, NULL);
+@@
+ #include "environment.h"
+ #include "gettext.h"
+ #include "hex.h"
++#include "hook.h"
+ #include "refspec.h"
+ #include "run-command.h"
+ #include "remote.h"
@@ builtin/push.c: int cmd_push(int argc,
packet_trace_identity("push");
repo_config(the_repository, git_push_config, &flags);
argc = parse_options(argc, argv, prefix, options, push_usage, 0);
-+ if ((flags & TRANSPORT_PUSH_NO_HOOK) && !allow_no_verify)
-+ die(_("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'"));
++ if (flags & TRANSPORT_PUSH_NO_HOOK)
++ validate_no_verify(the_repository, "--no-verify");
push_options = (push_options_cmdline.nr
? &push_options_cmdline
: &push_options_config);
## builtin/rebase.c ##
-@@ builtin/rebase.c: static void parse_rebase_merges_value(struct rebase_options *options, const char
- die(_("Unknown rebase-merges mode: %s"), value);
- }
-
-+static int allow_no_verify = 1;
-+
- static int rebase_config(const char *var, const char *value,
- const struct config_context *ctx, void *data)
- {
-@@ builtin/rebase.c: static int rebase_config(const char *var, const char *value,
- return 0;
- }
-
-+ if (!strcmp(var, "hooks.allownoverify")) {
-+ allow_no_verify = git_config_bool(var, value);
-+ return 0;
-+ }
-+
- if (!strcmp(var, "rebase.rebasemerges")) {
- opts->config_rebase_merges = git_parse_maybe_bool(value);
- if (opts->config_rebase_merges < 0) {
@@ builtin/rebase.c: int cmd_rebase(int argc,
builtin_rebase_options,
builtin_rebase_usage, 0);
-+ if (ok_to_skip_pre_rebase && !allow_no_verify)
-+ die(_("the use of '--no-verify' is disabled by 'hooks.allowNoVerify'"));
++ if (ok_to_skip_pre_rebase)
++ validate_no_verify(the_repository, "--no-verify");
+
if (options.trailer_args.nr) {
if (validate_trailer_args(&options.trailer_args))
die(NULL);
+ ## hook.c ##
+@@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)
+
+ return run_hooks_opt(r, hook_name, &opt);
+ }
++
++void validate_no_verify(struct repository *r, const char *opt)
++{
++ const char *val = NULL;
++ int maybe_bool;
++
++ if (git_env_bool("GIT_ALLOW_NO_VERIFY", 0))
++ return;
++
++ if (!r || repo_config_get_value(r, "hooks.allownoverify", &val))
++ return;
++
++ maybe_bool = git_parse_maybe_bool(val);
++ if (maybe_bool == 1 || !strcasecmp(val, "always")) {
++ return;
++ } else if (!strcasecmp(val, "warn")) {
++ warning(_("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'"), opt);
++ return;
++ } else if (maybe_bool == 0 || !strcasecmp(val, "never") || !strcasecmp(val, "error")) {
++ advise(_("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n"
++ "In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n"
++ " git -c hooks.allowNoVerify=true <command>\n"
++ "or:\n"
++ " GIT_ALLOW_NO_VERIFY=1 git <command>"), opt);
++ die(_("the use of '%s' is disabled by 'hooks.allowNoVerify'"), opt);
++ } else {
++ warning(_("unknown value for 'hooks.allowNoVerify': '%s'"), val);
++ }
++}
++
+
+ ## hook.h ##
+@@ hook.h: int run_hooks(struct repository *r, const char *hook_name);
+ */
+ LAST_ARG_MUST_BE_NULL
+ int run_hooks_l(struct repository *r, const char *hook_name, ...);
++
++/**
++ * Check if the use of '--no-verify' (or '-n') is permitted according to
++ * the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment
++ * variable.
++ *
++ * If permitted, this function returns normally (or emits a warning if configured
++ * to 'warn'). If disallowed, it outputs advice on how to override the workflow
++ * guardrail in an emergency, then aborts with die().
++ */
++void validate_no_verify(struct repository *r, const char *opt);
++
+ #endif
++
+
## t/meson.build ##
@@ t/meson.build: integration_tests = [
't7526-commit-pathspec-file.sh',
@@ t/t7599-hooks-allownoverify.sh (new)
+ test_grep "hooks.allowNoVerify" err
+'
+
++test_expect_success 'CLI -c hooks.allowNoVerify=true overrides local false' '
++ test_config hooks.allowNoVerify false &&
++ echo "change10" >>init.t &&
++ git add init.t &&
++ git -c hooks.allowNoVerify=true commit --no-verify -m "override false with CLI true"
++'
++
++test_expect_success 'hooks.allowNoVerify=false provides emergency override advice' '
++ test_config hooks.allowNoVerify false &&
++ echo "change11" >>init.t &&
++ git add init.t &&
++ test_must_fail git commit --no-verify -m "fail advice" 2>err &&
++ test_grep "GIT_ALLOW_NO_VERIFY=1" err &&
++ test_grep "git -c hooks.allowNoVerify=true" err
++'
++
++test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '
++ test_when_finished "rm -f pre-commit.log" &&
++ test_config hooks.allowNoVerify false &&
++ echo "change12" >>init.t &&
++ git add init.t &&
++ GIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m "emergency commit" &&
++ test_path_is_missing pre-commit.log
++'
++
++test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '
++ test_when_finished "rm -f pre-push.log" &&
++ test_config hooks.allowNoVerify false &&
++ GIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&
++ test_path_is_missing pre-push.log
++'
++
++test_expect_success 'hooks.allowNoVerify=warn permits --no-verify and warns on stderr' '
++ test_when_finished "rm -f pre-commit.log err" &&
++ test_config hooks.allowNoVerify warn &&
++ echo "change13" >>init.t &&
++ git add init.t &&
++ git commit --no-verify -m "commit with warn" 2>err &&
++ test_path_is_missing pre-commit.log &&
++ test_grep "bypassing hooks with .--no-verify. is discouraged" err
++'
++
++test_expect_success 'hooks.allowNoVerify=never disallows --no-verify' '
++ test_config hooks.allowNoVerify never &&
++ echo "change14" >>init.t &&
++ git add init.t &&
++ test_must_fail git commit --no-verify -m "fail never" 2>err &&
++ test_grep "hooks.allowNoVerify" err
++'
++
++test_expect_success 'hooks.allowNoVerify=error disallows --no-verify' '
++ test_config hooks.allowNoVerify error &&
++ echo "change15" >>init.t &&
++ git add init.t &&
++ test_must_fail git commit --no-verify -m "fail error" 2>err &&
++ test_grep "hooks.allowNoVerify" err
++'
++
++test_expect_success 'hooks.allowNoVerify=always allows --no-verify' '
++ test_when_finished "rm -f pre-commit.log" &&
++ test_config hooks.allowNoVerify always &&
++ echo "change16" >>init.t &&
++ git add init.t &&
++ git commit --no-verify -m "commit always" &&
++ test_path_is_missing pre-commit.log
++'
++
+test_done
++Documentation/config.adoc | 2 + Documentation/config/hooks.adoc | 24 ++++ Documentation/git.adoc | 5 + builtin/am.c | 3 + builtin/commit.c | 4 + builtin/merge.c | 2 + builtin/push.c | 3 + builtin/rebase.c | 3 + hook.c | 30 +++++ hook.h | 13 ++ t/meson.build | 1 + t/t7599-hooks-allownoverify.sh | 217 ++++++++++++++++++++++++++++++++ 12 files changed, 307 insertions(+) create mode 100644 Documentation/config/hooks.adoc create mode 100755 t/t7599-hooks-allownoverify.sh
Documentation/config.adoc +2 -0
@@ -508,6 +508,8 @@ include::config/help.adoc[] include::config/hook.adoc[] +include::config/hooks.adoc[] + include::config/http.adoc[] include::config/i18n.adoc[]
Documentation/config/hooks.adoc new file +24 -0
@@ -0,0 +1,24 @@ +`hooks.allowNoVerify`:: + Specifies whether the `--no-verify` (or `-n`) command-line option + is permitted in commands that run client-side hooks, such as `git commit`, + `git push`, `git merge`, `git rebase`, and `git am`. ++ +Allowed values are: ++ +-- +* `true` (or `always`): `--no-verify` is permitted normally. This is the default. +* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr. +* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts + with a fatal error accompanied by advice explaining how to override it. +-- ++ +In an emergency (for example, when a local hook crashes or during a critical +production hotfix), this guardrail can be overridden without modifying +configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable +or by passing `-c hooks.allowNoVerify=true` on the command line. ++ +NOTE: Client-side hooks execute in the developer's environment and belong to +the user. This configuration serves strictly as an ergonomic workflow guardrail +against accidental bypasses (such as muscle-memory `-n` or automated scripts), +and must not be relied upon as a security boundary. Authoritative enforcement +must always be implemented server-side (for example, via `pre-receive` hooks).
Documentation/git.adoc +5 -0
@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently only affects clones and fetches; it is not yet used for pushes (but may be in the future). +`GIT_ALLOW_NO_VERIFY`:: + If this Boolean environment variable is set to true (e.g. `1`), permits the use + of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`. + This serves as an emergency override mechanism for workflows when hooks fail unexpectedly. + `GIT_OPTIONAL_LOCKS`:: If this Boolean environment variable is set to false, Git will complete any requested operation without performing any optional sub-operations that require taking a lock.
builtin/am.c +3 -0
@@ -2457,6 +2457,9 @@ int cmd_am(int argc, argc =parse_options (argc, argv, prefix, options, usage,0 ); +if (state.no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (binary >=0 )fprintf_ln (stderr,_ ("The -b/--binary option has been a no-op for long time, and\n" "it will be removed. Please do not use it anymore." ));
builtin/commit.c +4 -0
@@ -19,6 +19,7 @@#include "environment.h" #include "diff.h" #include "commit.h" +#include "hook.h" #include "add-interactive.h" #include "gettext.h" #include "revision.h"
@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[], argc =parse_options (argc, argv, prefix, options, usage,0 );finalize_deferred_config (s); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (force_author && !strchr (force_author,'>' )) force_author =find_author_by_nickname (force_author);
builtin/merge.c +2 -0
@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,parse_branch_merge_options (branch_mergeoptions); argc =parse_options (argc, argv, prefix, builtin_merge_options, builtin_merge_usage,0 ); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" );if (shortlog_len <0 ) shortlog_len = (merge_log_config >0 ) ? merge_log_config :0 ;
builtin/push.c +3 -0
@@ -12,6 +12,7 @@#include "environment.h" #include "gettext.h" #include "hex.h" +#include "hook.h" #include "refspec.h" #include "run-command.h" #include "remote.h"
@@ -746,6 +747,8 @@ int cmd_push(int argc,packet_trace_identity ("push" );repo_config (the_repository, git_push_config, &flags); argc =parse_options (argc, argv, prefix, options, push_usage,0 ); +if (flags &TRANSPORT_PUSH_NO_HOOK ) +validate_no_verify (the_repository,"--no-verify" ); push_options = (push_options_cmdline.nr ? &push_options_cmdline : &push_options_config);
builtin/rebase.c +3 -0
@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc, builtin_rebase_options, builtin_rebase_usage, 0); +if (ok_to_skip_pre_rebase) +validate_no_verify (the_repository,"--no-verify" ); +if (options.trailer_args.nr) {if (validate_trailer_args (&options.trailer_args))die (NULL );
hook.c +30 -0
@@ -858,3 +858,33 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)return run_hooks_opt (r, hook_name, &opt); } + +void validate_no_verify (struct repository *r,const char *opt) +{ +const char *val =NULL ; +int maybe_bool; + +if (git_env_bool ("GIT_ALLOW_NO_VERIFY" ,0 )) +return ; + +if (!r ||repo_config_get_value (r,"hooks.allownoverify" , &val)) +return ; + + maybe_bool =git_parse_maybe_bool (val); +if (maybe_bool ==1 || !strcasecmp (val,"always" )) { +return ; + }else if (!strcasecmp (val,"warn" )) { +warning (_ ("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'" ), opt); +return ; + }else if (maybe_bool ==0 || !strcasecmp (val,"never" ) || !strcasecmp (val,"error" )) { +advise (_ ("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n" +"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n" +" git -c hooks.allowNoVerify=true <command>\n" +"or:\n" +" GIT_ALLOW_NO_VERIFY=1 git <command>" ), opt); +die (_ ("the use of '%s' is disabled by 'hooks.allowNoVerify'" ), opt); + }else { +warning (_ ("unknown value for 'hooks.allowNoVerify': '%s'" ), val); + } +} +
hook.h +13 -0
@@ -280,4 +280,17 @@ int run_hooks(struct repository *r, const char *hook_name);*/ LAST_ARG_MUST_BE_NULL int run_hooks_l(struct repository *r, const char *hook_name, ...); + +/** +* Check if the use of '--no-verify' (or '-n') is permitted according to +* the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment +* variable. +* +* If permitted, this function returns normally (or emits a warning if configured +* to 'warn'). If disallowed, it outputs advice on how to override the workflow +* guardrail in an emergency, then aborts with die(). +*/ +void validate_no_verify (struct repository *r,const char *opt); +#endif +
t/meson.build +1 -0
@@ -945,6 +945,7 @@ integration_tests = [ 't7526-commit-pathspec-file.sh', 't7527-builtin-fsmonitor.sh', 't7528-signed-commit-ssh.sh', + 't7599-hooks-allownoverify.sh', 't7600-merge.sh', 't7601-merge-pull-config.sh', 't7602-merge-octopus-many.sh',
t/t7599-hooks-allownoverify.sh new file +217 -0
217 lines not on this page · raw message
base-commit: 1630431f326e15fcde608827b5ff38422528eb59
Signature · 1 line
gitgitgadget
PATCH v3 1/1 hooks: introduce 'hooks.allowNoVerify' configuration
In reply to Alessio Attilio via GitGitGadgetFrom: Alessio Attilio <[email protected]>
Introduce the 'hooks.allowNoVerify' configuration variable to control whether the '--no-verify' (or '-n') command-line option is permitted during operations executing client-side hooks (commit, push, merge, rebase, am).
Client-side hooks execute in the user's local repository and cannot serve as an authoritative security boundary; authoritative policy enforcement belongs on the server (such as via pre-receive hooks). However, developers often invoke '--no-verify' out of habit or muscle memory, inadvertently skipping local checks.
To address concerns regarding false senses of security without breaking legitimate emergency escape hatches, allow configuring the variable to 'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits the bypass while emitting a warning to standard error, ensuring visibility without interrupting urgent workflows.
When set to 'false', Git aborts execution and provides actionable advice explaining that the setting is an ergonomic workflow guardrail. To avoid trapping developers during broken hook scripts or critical hotfixes, the guardrail can be overridden by passing '-c hooks.allowNoVerify=true' or by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This prevents developers from having to resort to destructive workarounds such as removing hook files or clearing execute permissions.
Centralize the option verification logic across all affected commands into validate_no_verify() in hook.c.
---
hooks: introduce 'hooks.allowNoVerify' configuration
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail (default: true) to prevent accidental bypass of hooks via '--no-verify'. This setting is intended for workflows and managed environments to avoid inadvertent bypasses, without altering Git's server-side security model.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2215
Range-diff vs v2:
1: 4e594568de ! 1: cc98af8a9a hooks: introduce 'hooks.allowNoVerify' configuration
@@ Commit message
as an authoritative security boundary; authoritative policy enforcement
belongs on the server (such as via pre-receive hooks). However,
developers often invoke '--no-verify' out of habit or muscle memory,
- accidentally skipping local checks.
+ inadvertently skipping local checks.
- To address both 'security theatre' concerns and avoid breaking legitimate
- emergency escape hatches ('big red button'), this implementation:
+ To address concerns regarding false senses of security without breaking
+ legitimate emergency escape hatches, allow configuring the variable to
+ 'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits
+ the bypass while emitting a warning to standard error, ensuring
+ visibility without interrupting urgent workflows.
- 1. Introduces granular values:
- - 'true' (or 'always', default): '--no-verify' is permitted normally.
- - 'warn': '--no-verify' is permitted, but prints a warning to stderr.
- - 'false' (or 'never', 'error'): '--no-verify' is disallowed by default.
+ When set to 'false', Git aborts execution and provides actionable advice
+ explaining that the setting is an ergonomic workflow guardrail. To avoid
+ trapping developers during broken hook scripts or critical hotfixes,
+ the guardrail can be overridden by passing '-c hooks.allowNoVerify=true'
+ or by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This
+ prevents developers from having to resort to destructive workarounds
+ such as removing hook files or clearing execute permissions.
- 2. Preserves the emergency break-glass escape hatch:
- When configured to 'false', Git does not create a dead-end. It outputs
- actionable advice explaining that the setting is a workflow guardrail
- against accidental bypass, and documents how to override it in an
- emergency via 'GIT_ALLOW_NO_VERIFY=1' or '-c hooks.allowNoVerify=true'.
- This eliminates the need for developers to resort to destructive local
- hacks like deleting hooks or chmod -x.
-
- 3. Centralizes the validation logic in 'hook.c' and 'hook.h' via
- validate_no_verify().
+ Centralize the option verification logic across all affected commands into
+ validate_no_verify() in hook.c.
Signed-off-by: Alessio Attilio <[email protected]>
@@ Documentation/config/hooks.adoc (new)
+Allowed values are:
++
+--
-+* `true` (or `always`): `--no-verify` is permitted normally. This is the default.
++* `true`: `--no-verify` is permitted normally. This is the default.
+* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.
-+* `false` (or `never`, `error`): `--no-verify` is disallowed and Git aborts
++* `false`: `--no-verify` is disallowed and Git aborts
+ with a fatal error accompanied by advice explaining how to override it.
+--
++
@@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)
+ return;
+
+ maybe_bool = git_parse_maybe_bool(val);
-+ if (maybe_bool == 1 || !strcasecmp(val, "always")) {
++ if (maybe_bool == 1) {
+ return;
+ } else if (!strcasecmp(val, "warn")) {
+ warning(_("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'"), opt);
+ return;
-+ } else if (maybe_bool == 0 || !strcasecmp(val, "never") || !strcasecmp(val, "error")) {
++ } else if (maybe_bool == 0) {
+ advise(_("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n"
+ "In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n"
+ " git -c hooks.allowNoVerify=true <command>\n"
@@ hook.c: int run_hooks_l(struct repository *r, const char *hook_name, ...)
+ warning(_("unknown value for 'hooks.allowNoVerify': '%s'"), val);
+ }
+}
-+
## hook.h ##
@@ hook.h: int run_hooks(struct repository *r, const char *hook_name);
@@ hook.h: int run_hooks(struct repository *r, const char *hook_name);
+void validate_no_verify(struct repository *r, const char *opt);
+
#endif
-+
## t/meson.build ##
@@ t/meson.build: integration_tests = [
@@ t/t7599-hooks-allownoverify.sh (new)
+ test_grep "bypassing hooks with .--no-verify. is discouraged" err
+'
+
-+test_expect_success 'hooks.allowNoVerify=never disallows --no-verify' '
-+ test_config hooks.allowNoVerify never &&
++test_expect_success 'hooks.allowNoVerify=0 disallows --no-verify' '
++ test_config hooks.allowNoVerify 0 &&
+ echo "change14" >>init.t &&
+ git add init.t &&
-+ test_must_fail git commit --no-verify -m "fail never" 2>err &&
-+ test_grep "hooks.allowNoVerify" err
-+'
-+
-+test_expect_success 'hooks.allowNoVerify=error disallows --no-verify' '
-+ test_config hooks.allowNoVerify error &&
-+ echo "change15" >>init.t &&
-+ git add init.t &&
-+ test_must_fail git commit --no-verify -m "fail error" 2>err &&
++ test_must_fail git commit --no-verify -m "fail 0" 2>err &&
+ test_grep "hooks.allowNoVerify" err
+'
+
-+test_expect_success 'hooks.allowNoVerify=always allows --no-verify' '
++test_expect_success 'hooks.allowNoVerify=1 allows --no-verify' '
+ test_when_finished "rm -f pre-commit.log" &&
-+ test_config hooks.allowNoVerify always &&
-+ echo "change16" >>init.t &&
++ test_config hooks.allowNoVerify 1 &&
++ echo "change15" >>init.t &&
+ git add init.t &&
-+ git commit --no-verify -m "commit always" &&
++ git commit --no-verify -m "commit 1" &&
+ test_path_is_missing pre-commit.log
+'
+
+test_done
-+Documentation/config.adoc | 2 + Documentation/config/hooks.adoc | 24 ++++ Documentation/git.adoc | 5 + builtin/am.c | 3 + builtin/commit.c | 4 + builtin/merge.c | 2 + builtin/push.c | 3 + builtin/rebase.c | 3 + hook.c | 29 +++++ hook.h | 12 ++ t/meson.build | 1 + t/t7599-hooks-allownoverify.sh | 208 ++++++++++++++++++++++++++++++++ 12 files changed, 296 insertions(+) create mode 100644 Documentation/config/hooks.adoc create mode 100755 t/t7599-hooks-allownoverify.sh
Documentation/config.adoc +2 -0
@@ -508,6 +508,8 @@ include::config/help.adoc[] include::config/hook.adoc[] +include::config/hooks.adoc[] + include::config/http.adoc[] include::config/i18n.adoc[]
Documentation/config/hooks.adoc new file +24 -0
@@ -0,0 +1,24 @@ +`hooks.allowNoVerify`:: + Specifies whether the `--no-verify` (or `-n`) command-line option + is permitted in commands that run client-side hooks, such as `git commit`, + `git push`, `git merge`, `git rebase`, and `git am`. ++ +Allowed values are: ++ +-- +* `true`: `--no-verify` is permitted normally. This is the default. +* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr. +* `false`: `--no-verify` is disallowed and Git aborts + with a fatal error accompanied by advice explaining how to override it. +-- ++ +In an emergency (for example, when a local hook crashes or during a critical +production hotfix), this guardrail can be overridden without modifying +configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable +or by passing `-c hooks.allowNoVerify=true` on the command line. ++ +NOTE: Client-side hooks execute in the developer's environment and belong to +the user. This configuration serves strictly as an ergonomic workflow guardrail +against accidental bypasses (such as muscle-memory `-n` or automated scripts), +and must not be relied upon as a security boundary. Authoritative enforcement +must always be implemented server-side (for example, via `pre-receive` hooks).
Documentation/git.adoc +5 -0
@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently only affects clones and fetches; it is not yet used for pushes (but may be in the future). +`GIT_ALLOW_NO_VERIFY`:: + If this Boolean environment variable is set to true (e.g. `1`), permits the use + of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`. + This serves as an emergency override mechanism for workflows when hooks fail unexpectedly. + `GIT_OPTIONAL_LOCKS`:: If this Boolean environment variable is set to false, Git will complete any requested operation without performing any optional sub-operations that require taking a lock.
builtin/am.c +3 -0
@@ -2457,6 +2457,9 @@ int cmd_am(int argc, argc =parse_options (argc, argv, prefix, options, usage,0 ); +if (state.no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (binary >=0 )fprintf_ln (stderr,_ ("The -b/--binary option has been a no-op for long time, and\n" "it will be removed. Please do not use it anymore." ));
builtin/commit.c +4 -0
@@ -19,6 +19,7 @@#include "environment.h" #include "diff.h" #include "commit.h" +#include "hook.h" #include "add-interactive.h" #include "gettext.h" #include "revision.h"
@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[], argc =parse_options (argc, argv, prefix, options, usage,0 );finalize_deferred_config (s); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (force_author && !strchr (force_author,'>' )) force_author =find_author_by_nickname (force_author);
builtin/merge.c +2 -0
@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,parse_branch_merge_options (branch_mergeoptions); argc =parse_options (argc, argv, prefix, builtin_merge_options, builtin_merge_usage,0 ); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" );if (shortlog_len <0 ) shortlog_len = (merge_log_config >0 ) ? merge_log_config :0 ;
builtin/push.c +3 -0
@@ -12,6 +12,7 @@#include "environment.h" #include "gettext.h" #include "hex.h" +#include "hook.h" #include "refspec.h" #include "run-command.h" #include "remote.h"
@@ -746,6 +747,8 @@ int cmd_push(int argc,packet_trace_identity ("push" );repo_config (the_repository, git_push_config, &flags); argc =parse_options (argc, argv, prefix, options, push_usage,0 ); +if (flags &TRANSPORT_PUSH_NO_HOOK ) +validate_no_verify (the_repository,"--no-verify" ); push_options = (push_options_cmdline.nr ? &push_options_cmdline : &push_options_config);
builtin/rebase.c +3 -0
@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc, builtin_rebase_options, builtin_rebase_usage, 0); +if (ok_to_skip_pre_rebase) +validate_no_verify (the_repository,"--no-verify" ); +if (options.trailer_args.nr) {if (validate_trailer_args (&options.trailer_args))die (NULL );
hook.c +29 -0
@@ -858,3 +858,32 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)return run_hooks_opt (r, hook_name, &opt); } + +void validate_no_verify (struct repository *r,const char *opt) +{ +const char *val =NULL ; +int maybe_bool; + +if (git_env_bool ("GIT_ALLOW_NO_VERIFY" ,0 )) +return ; + +if (!r ||repo_config_get_value (r,"hooks.allownoverify" , &val)) +return ; + + maybe_bool =git_parse_maybe_bool (val); +if (maybe_bool ==1 ) { +return ; + }else if (!strcasecmp (val,"warn" )) { +warning (_ ("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'" ), opt); +return ; + }else if (maybe_bool ==0 ) { +advise (_ ("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n" +"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n" +" git -c hooks.allowNoVerify=true <command>\n" +"or:\n" +" GIT_ALLOW_NO_VERIFY=1 git <command>" ), opt); +die (_ ("the use of '%s' is disabled by 'hooks.allowNoVerify'" ), opt); + }else { +warning (_ ("unknown value for 'hooks.allowNoVerify': '%s'" ), val); + } +}
hook.h +12 -0
@@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);*/ LAST_ARG_MUST_BE_NULL int run_hooks_l(struct repository *r, const char *hook_name, ...); + +/** +* Check if the use of '--no-verify' (or '-n') is permitted according to +* the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment +* variable. +* +* If permitted, this function returns normally (or emits a warning if configured +* to 'warn'). If disallowed, it outputs advice on how to override the workflow +* guardrail in an emergency, then aborts with die(). +*/ +void validate_no_verify (struct repository *r,const char *opt); +#endif
t/meson.build +1 -0
@@ -945,6 +945,7 @@ integration_tests = [ 't7526-commit-pathspec-file.sh', 't7527-builtin-fsmonitor.sh', 't7528-signed-commit-ssh.sh', + 't7599-hooks-allownoverify.sh', 't7600-merge.sh', 't7601-merge-pull-config.sh', 't7602-merge-octopus-many.sh',
t/t7599-hooks-allownoverify.sh new file +208 -0
208 lines not on this page · raw message
base-commit: 1630431f326e15fcde608827b5ff38422528eb59
Signature · 1 line
gitgitgadget
Quoted 7 lines · Alessio Attilio via GitGitGadget
From: Alessio Attilio <[email protected]>
Introduce the 'hooks.allowNoVerify' configuration variable to control whether the '--no-verify' (or '-n') command-line option is permitted during operations executing client-side hooks (commit, push, merge, rebase, am).
Quoted 5 lines
Client-side hooks execute in the user's local repository and cannot serve as an authoritative security boundary; authoritative policy enforcement belongs on the server (such as via pre-receive hooks). However, developers often invoke '--no-verify' out of habit or muscle memory, inadvertently skipping local checks.
I agree with Junio that this doesn't seem like a good idea. It's up to the user whether they want to install or use hooks and they are free to disable them or override them as they see fit. If the user doesn't want to use local hooks on an individual case basis, then --no-verify is the right option.
In my case, I use hooks for Git LFS when I use that software, but I never use repository owner-provided hooks, although I may use my own. As a result, I almost never use --no-verify.
If in your environment you are trying to force developers to use local hooks, the Git FAQ mentions that this is not an effective control and you should stop trying to do that. In fact, I would even argue that you should simply not install hooks by default for repositories using your scripts or build tools because it's up to users whether those are useful for them. The Git FAQ mentions reasons why forced installation of hooks is harmful for many workflows and I don't think we should be hassling users like that.
In any event, it's trivial to simply bypass all hooks by setting core.hooksPath to /dev/null using a variety of different techniques.
Signature · 2 lines
brian m. carlson (they/them) Toronto, Ontario, CA
PATCH v4 1/1 hooks: introduce 'hooks.allowNoVerify' configuration
In reply to Alessio Attilio via GitGitGadgetFrom: Alessio Attilio <[email protected]>
Introduce the 'hooks.allowNoVerify' configuration variable to control whether the '--no-verify' (or '-n') command-line option is permitted during operations executing client-side hooks (commit, push, merge, rebase, am).
Client-side hooks execute in the user's local repository and cannot serve as an authoritative security boundary; authoritative policy enforcement belongs on the server (such as via pre-receive hooks). However, developers often invoke '--no-verify' out of habit or muscle memory, inadvertently skipping local checks.
To address concerns regarding false senses of security without breaking legitimate emergency escape hatches, allow configuring the variable to 'true' (the default), 'warn', or 'false'. In 'warn' mode, Git permits the bypass while emitting a warning to standard error, ensuring visibility without interrupting urgent workflows.
When set to 'false', Git aborts execution and provides actionable advice explaining that the setting is an ergonomic workflow guardrail. To avoid trapping developers during broken hook scripts or critical hotfixes, the guardrail can be overridden by passing '-c hooks.allowNoVerify=true' or by setting the 'GIT_ALLOW_NO_VERIFY=1' environment variable. This prevents developers from having to resort to destructive workarounds such as removing hook files or clearing execute permissions.
Centralize the option verification logic across all affected commands into validate_no_verify() in hook.c.
---
hooks: introduce 'hooks.allowNoVerify' configuration
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail (default: true) to prevent accidental bypass of hooks via '--no-verify'. This setting is intended for workflows and managed environments to avoid inadvertent bypasses, without altering Git's server-side security model.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2215%2Fkairosci%2Fhooks-allownoverify-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2215/kairosci/hooks-allownoverify-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/2215
Range-diff vs v3:
1: cc98af8a9a ! 1: a9f75413d3 hooks: introduce 'hooks.allowNoVerify' configuration
@@ t/t7599-hooks-allownoverify.sh (new)
+ test_grep "hooks.allowNoVerify" err
+'
+
++test_expect_success 'hooks.allowNoVerify=false disallows git am --no-verify' '
++ test_when_finished "rm -f patch && git am --abort || true" &&
++ test_config hooks.allowNoVerify false &&
++ git format-patch -1 --stdout branch-merge >patch &&
++ test_must_fail git am --no-verify patch 2>err &&
++ test_grep "hooks.allowNoVerify" err
++'
++
+test_expect_success 'hooks.allowNoVerify=false still runs hooks when --no-verify is not used' '
+ test_when_finished "rm -f pre-commit.log" &&
+ test_config hooks.allowNoVerify false &&Documentation/config.adoc | 2 + Documentation/config/hooks.adoc | 24 ++++ Documentation/git.adoc | 5 + builtin/am.c | 3 + builtin/commit.c | 4 + builtin/merge.c | 2 + builtin/push.c | 3 + builtin/rebase.c | 3 + hook.c | 29 +++++ hook.h | 12 ++ t/meson.build | 1 + t/t7599-hooks-allownoverify.sh | 216 ++++++++++++++++++++++++++++++++ 12 files changed, 304 insertions(+) create mode 100644 Documentation/config/hooks.adoc create mode 100755 t/t7599-hooks-allownoverify.sh
Documentation/config.adoc +2 -0
@@ -508,6 +508,8 @@ include::config/help.adoc[] include::config/hook.adoc[] +include::config/hooks.adoc[] + include::config/http.adoc[] include::config/i18n.adoc[]
Documentation/config/hooks.adoc new file +24 -0
@@ -0,0 +1,24 @@ +`hooks.allowNoVerify`:: + Specifies whether the `--no-verify` (or `-n`) command-line option + is permitted in commands that run client-side hooks, such as `git commit`, + `git push`, `git merge`, `git rebase`, and `git am`. ++ +Allowed values are: ++ +-- +* `true`: `--no-verify` is permitted normally. This is the default. +* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr. +* `false`: `--no-verify` is disallowed and Git aborts + with a fatal error accompanied by advice explaining how to override it. +-- ++ +In an emergency (for example, when a local hook crashes or during a critical +production hotfix), this guardrail can be overridden without modifying +configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable +or by passing `-c hooks.allowNoVerify=true` on the command line. ++ +NOTE: Client-side hooks execute in the developer's environment and belong to +the user. This configuration serves strictly as an ergonomic workflow guardrail +against accidental bypasses (such as muscle-memory `-n` or automated scripts), +and must not be relied upon as a security boundary. Authoritative enforcement +must always be implemented server-side (for example, via `pre-receive` hooks).
Documentation/git.adoc +5 -0
@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently only affects clones and fetches; it is not yet used for pushes (but may be in the future). +`GIT_ALLOW_NO_VERIFY`:: + If this Boolean environment variable is set to true (e.g. `1`), permits the use + of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`. + This serves as an emergency override mechanism for workflows when hooks fail unexpectedly. + `GIT_OPTIONAL_LOCKS`:: If this Boolean environment variable is set to false, Git will complete any requested operation without performing any optional sub-operations that require taking a lock.
builtin/am.c +3 -0
@@ -2457,6 +2457,9 @@ int cmd_am(int argc, argc =parse_options (argc, argv, prefix, options, usage,0 ); +if (state.no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (binary >=0 )fprintf_ln (stderr,_ ("The -b/--binary option has been a no-op for long time, and\n" "it will be removed. Please do not use it anymore." ));
builtin/commit.c +4 -0
@@ -19,6 +19,7 @@#include "environment.h" #include "diff.h" #include "commit.h" +#include "hook.h" #include "add-interactive.h" #include "gettext.h" #include "revision.h"
@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[], argc =parse_options (argc, argv, prefix, options, usage,0 );finalize_deferred_config (s); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" ); +if (force_author && !strchr (force_author,'>' )) force_author =find_author_by_nickname (force_author);
builtin/merge.c +2 -0
@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,parse_branch_merge_options (branch_mergeoptions); argc =parse_options (argc, argv, prefix, builtin_merge_options, builtin_merge_usage,0 ); +if (no_verify) +validate_no_verify (the_repository,"--no-verify" );if (shortlog_len <0 ) shortlog_len = (merge_log_config >0 ) ? merge_log_config :0 ;
builtin/push.c +3 -0
@@ -12,6 +12,7 @@#include "environment.h" #include "gettext.h" #include "hex.h" +#include "hook.h" #include "refspec.h" #include "run-command.h" #include "remote.h"
@@ -746,6 +747,8 @@ int cmd_push(int argc,packet_trace_identity ("push" );repo_config (the_repository, git_push_config, &flags); argc =parse_options (argc, argv, prefix, options, push_usage,0 ); +if (flags &TRANSPORT_PUSH_NO_HOOK ) +validate_no_verify (the_repository,"--no-verify" ); push_options = (push_options_cmdline.nr ? &push_options_cmdline : &push_options_config);
builtin/rebase.c +3 -0
@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc, builtin_rebase_options, builtin_rebase_usage, 0); +if (ok_to_skip_pre_rebase) +validate_no_verify (the_repository,"--no-verify" ); +if (options.trailer_args.nr) {if (validate_trailer_args (&options.trailer_args))die (NULL );
hook.c +29 -0
@@ -858,3 +858,32 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)return run_hooks_opt (r, hook_name, &opt); } + +void validate_no_verify (struct repository *r,const char *opt) +{ +const char *val =NULL ; +int maybe_bool; + +if (git_env_bool ("GIT_ALLOW_NO_VERIFY" ,0 )) +return ; + +if (!r ||repo_config_get_value (r,"hooks.allownoverify" , &val)) +return ; + + maybe_bool =git_parse_maybe_bool (val); +if (maybe_bool ==1 ) { +return ; + }else if (!strcasecmp (val,"warn" )) { +warning (_ ("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'" ), opt); +return ; + }else if (maybe_bool ==0 ) { +advise (_ ("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n" +"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n" +" git -c hooks.allowNoVerify=true <command>\n" +"or:\n" +" GIT_ALLOW_NO_VERIFY=1 git <command>" ), opt); +die (_ ("the use of '%s' is disabled by 'hooks.allowNoVerify'" ), opt); + }else { +warning (_ ("unknown value for 'hooks.allowNoVerify': '%s'" ), val); + } +}
hook.h +12 -0
@@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);*/ LAST_ARG_MUST_BE_NULL int run_hooks_l(struct repository *r, const char *hook_name, ...); + +/** +* Check if the use of '--no-verify' (or '-n') is permitted according to +* the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment +* variable. +* +* If permitted, this function returns normally (or emits a warning if configured +* to 'warn'). If disallowed, it outputs advice on how to override the workflow +* guardrail in an emergency, then aborts with die(). +*/ +void validate_no_verify (struct repository *r,const char *opt); +#endif
t/meson.build +1 -0
@@ -945,6 +945,7 @@ integration_tests = [ 't7526-commit-pathspec-file.sh', 't7527-builtin-fsmonitor.sh', 't7528-signed-commit-ssh.sh', + 't7599-hooks-allownoverify.sh', 't7600-merge.sh', 't7601-merge-pull-config.sh', 't7602-merge-octopus-many.sh',
t/t7599-hooks-allownoverify.sh new file +216 -0
216 lines not on this page · raw message
base-commit: 1630431f326e15fcde608827b5ff38422528eb59
Signature · 1 line
gitgitgadget
Quoted 5 lines · Alessio Attilio via GitGitGadget
From: Alessio Attilio <[email protected]>
Introduce 'hooks.allowNoVerify' as an opt-in workflow guardrail to prevent accidental bypass of hooks with '--no-verify' when set to false. Authoritative enforcement remains server-side.
Accidental
$ git foo --no-verify
may be prevented by setting this configuration variable, but then would we need another layer of protection to prevent accidental
$ git -c hooks.allownoverify foo --no-verify
by introducing another configuration variable to forbid hooks.allownoverify to be overriden?
I do not think we want to go into this slipperly slope. Thanks for sending a patch, but I am personally not interested.
Besides, verifications that users may be tempted to bypass, but want to instill discipline to prevent bypassing, may not be implemented as hooks, and the way they are bypassed may not be "--[no-]verify" command line option. When one wants a way to prevent such verifications from getting disabled, the mechanism should also allow forbidding verification that is built into the system from getting disabled. Limiting a settings to hooks is probably not a good idea, and introducing a new "hooks." hierarchy for this setting is not something we want to see.