Skip to content

Commit 992ec60

Browse files
committed
hooks: introduce 'hooks.allowNoVerify' configuration
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. In automated or non-interactive environments such as CI/CD runners, 'warn' mode avoids pipeline failures while preserving audit visibility, and 'GIT_ALLOW_NO_VERIFY=1' provides a clean override without modifying configuration files. Centralize the option verification logic across all affected commands into validate_no_verify() in hook.c. Signed-off-by: Alessio Attilio <alessio.attilio@protonmail.com>
1 parent 1630431 commit 992ec60

12 files changed

Lines changed: 306 additions & 0 deletions

File tree

Documentation/config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,8 @@ include::config/help.adoc[]
508508

509509
include::config/hook.adoc[]
510510

511+
include::config/hooks.adoc[]
512+
511513
include::config/http.adoc[]
512514

513515
include::config/i18n.adoc[]

Documentation/config/hooks.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
`hooks.allowNoVerify`::
2+
Specifies whether the `--no-verify` (or `-n`) command-line option
3+
is permitted in commands that run client-side hooks, such as `git commit`,
4+
`git push`, `git merge`, `git rebase`, and `git am`.
5+
+
6+
Allowed values are:
7+
+
8+
--
9+
* `true`: `--no-verify` is permitted normally. This is the default.
10+
* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.
11+
* `false`: `--no-verify` is disallowed and Git aborts
12+
with a fatal error accompanied by advice explaining how to override it.
13+
--
14+
+
15+
In an emergency (for example, when a local hook crashes or during a critical
16+
production hotfix), this guardrail can be overridden without modifying
17+
configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable
18+
or by passing `-c hooks.allowNoVerify=true` on the command line.
19+
In automated or scripted environments (such as CI/CD runners), `hooks.allowNoVerify = warn`
20+
provides visibility on stderr while allowing unattended workflows to complete without failure.
21+
+
22+
NOTE: Client-side hooks execute in the developer's environment and belong to
23+
the user. This configuration serves strictly as an ergonomic workflow guardrail
24+
against accidental bypasses (such as muscle-memory `-n` or automated scripts),
25+
and must not be relied upon as a security boundary. Authoritative enforcement
26+
must always be implemented server-side (for example, via `pre-receive` hooks).

Documentation/git.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently
10181018
only affects clones and fetches; it is not yet used for pushes (but may
10191019
be in the future).
10201020

1021+
`GIT_ALLOW_NO_VERIFY`::
1022+
If this Boolean environment variable is set to true (e.g. `1`), permits the use
1023+
of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.
1024+
This serves as an emergency override mechanism for workflows when hooks fail unexpectedly.
1025+
10211026
`GIT_OPTIONAL_LOCKS`::
10221027
If this Boolean environment variable is set to false, Git will complete any requested operation without
10231028
performing any optional sub-operations that require taking a lock.

builtin/am.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2457,6 +2457,9 @@ int cmd_am(int argc,
24572457

24582458
argc = parse_options(argc, argv, prefix, options, usage, 0);
24592459

2460+
if (state.no_verify)
2461+
validate_no_verify(the_repository, "--no-verify");
2462+
24602463
if (binary >= 0)
24612464
fprintf_ln(stderr, _("The -b/--binary option has been a no-op for long time, and\n"
24622465
"it will be removed. Please do not use it anymore."));

builtin/commit.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "environment.h"
2020
#include "diff.h"
2121
#include "commit.h"
22+
#include "hook.h"
2223
#include "add-interactive.h"
2324
#include "gettext.h"
2425
#include "revision.h"
@@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
13161317
argc = parse_options(argc, argv, prefix, options, usage, 0);
13171318
finalize_deferred_config(s);
13181319

1320+
if (no_verify)
1321+
validate_no_verify(the_repository, "--no-verify");
1322+
13191323
if (force_author && !strchr(force_author, '>'))
13201324
force_author = find_author_by_nickname(force_author);
13211325

builtin/merge.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,8 @@ int cmd_merge(int argc,
14081408
parse_branch_merge_options(branch_mergeoptions);
14091409
argc = parse_options(argc, argv, prefix, builtin_merge_options,
14101410
builtin_merge_usage, 0);
1411+
if (no_verify)
1412+
validate_no_verify(the_repository, "--no-verify");
14111413
if (shortlog_len < 0)
14121414
shortlog_len = (merge_log_config > 0) ? merge_log_config : 0;
14131415

builtin/push.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "environment.h"
1313
#include "gettext.h"
1414
#include "hex.h"
15+
#include "hook.h"
1516
#include "refspec.h"
1617
#include "run-command.h"
1718
#include "remote.h"
@@ -746,6 +747,8 @@ int cmd_push(int argc,
746747
packet_trace_identity("push");
747748
repo_config(the_repository, git_push_config, &flags);
748749
argc = parse_options(argc, argv, prefix, options, push_usage, 0);
750+
if (flags & TRANSPORT_PUSH_NO_HOOK)
751+
validate_no_verify(the_repository, "--no-verify");
749752
push_options = (push_options_cmdline.nr
750753
? &push_options_cmdline
751754
: &push_options_config);

builtin/rebase.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,9 @@ int cmd_rebase(int argc,
12991299
builtin_rebase_options,
13001300
builtin_rebase_usage, 0);
13011301

1302+
if (ok_to_skip_pre_rebase)
1303+
validate_no_verify(the_repository, "--no-verify");
1304+
13021305
if (options.trailer_args.nr) {
13031306
if (validate_trailer_args(&options.trailer_args))
13041307
die(NULL);

hook.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,3 +858,32 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)
858858

859859
return run_hooks_opt(r, hook_name, &opt);
860860
}
861+
862+
void validate_no_verify(struct repository *r, const char *opt)
863+
{
864+
const char *val = NULL;
865+
int maybe_bool;
866+
867+
if (git_env_bool("GIT_ALLOW_NO_VERIFY", 0))
868+
return;
869+
870+
if (!r || repo_config_get_value(r, "hooks.allownoverify", &val))
871+
return;
872+
873+
maybe_bool = git_parse_maybe_bool(val);
874+
if (maybe_bool == 1) {
875+
return;
876+
} else if (val && !strcasecmp(val, "warn")) {
877+
warning(_("bypassing hooks with '%s' is discouraged by 'hooks.allowNoVerify'"), opt);
878+
return;
879+
} else if (maybe_bool == 0) {
880+
advise(_("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n"
881+
"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n"
882+
" git -c hooks.allowNoVerify=true <command>\n"
883+
"or:\n"
884+
" GIT_ALLOW_NO_VERIFY=1 git <command>"), opt);
885+
die(_("the use of '%s' is disabled by 'hooks.allowNoVerify'"), opt);
886+
} else {
887+
warning(_("unknown value for 'hooks.allowNoVerify': '%s'"), val);
888+
}
889+
}

hook.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);
280280
*/
281281
LAST_ARG_MUST_BE_NULL
282282
int run_hooks_l(struct repository *r, const char *hook_name, ...);
283+
284+
/**
285+
* Check if the use of '--no-verify' (or '-n') is permitted according to
286+
* the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment
287+
* variable.
288+
*
289+
* If permitted, this function returns normally (or emits a warning if configured
290+
* to 'warn'). If disallowed, it outputs advice on how to override the workflow
291+
* guardrail in an emergency, then aborts with die().
292+
*/
293+
void validate_no_verify(struct repository *r, const char *opt);
294+
283295
#endif

0 commit comments

Comments
 (0)