From: "Chandra Kethi-Reddy via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Chandra Kethi-Reddy <chandrakr@pm.me>,
Chandra Kethi-Reddy <chandrakr@pm.me>
Subject: [PATCH] add: support pre-add hook
Date: Tue, 10 Feb 2026 15:32:53 +0000 [thread overview]
Message-ID: <pull.2045.git.1770737573475.gitgitgadget@gmail.com> (raw)
From: Chandra Kethi-Reddy <chandrakr@pm.me>
git has no hook that fires during 'git add'. Users who want to
validate files before staging must wrap 'git add' in a shell alias
or wait for pre-commit, which fires after staging is already done.
Add a pre-add hook that runs after pathspec validation and before
any files are staged. If the hook exits non-zero, 'git add' aborts
without modifying the index. The hook receives GIT_INDEX_FILE in
its environment, following the same convention as pre-commit.
The hook is bypassed with '--no-verify' (long flag only, since '-n'
is already '--dry-run' in 'git add'). It is not invoked for
--interactive, --patch, --edit, or --dry-run modes, nor by
'git commit -a' which stages files through its own code path in
builtin/commit.c.
The implementation calls run_hooks_opt() directly rather than the
run_commit_hook() wrapper, which sets GIT_EDITOR=: and is not
relevant for 'git add'. When no hook is installed, there is no
performance impact.
Disclosure: developed with guidance from Claude Code (Anthropic)
and Codex CLI (OpenAI) for development, review and standards
compliance. The contributor handtyped and reviewed all tests, code,
and documentation.
Signed-off-by: Chandra Kethi-Reddy <chandrakr@pm.me>
---
add: support pre-add hook
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2045%2Fshatachandra%2Fpre-add-hooks-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2045/shatachandra/pre-add-hooks-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2045
Documentation/git-add.adoc | 10 ++-
Documentation/githooks.adoc | 17 ++++++
builtin/add.c | 14 +++++
t/t3706-pre-add-hook.sh | 117 ++++++++++++++++++++++++++++++++++++
4 files changed, 157 insertions(+), 1 deletion(-)
create mode 100644 t/t3706-pre-add-hook.sh
diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
index 6192daeb03..c60e0c65a5 100644
--- a/Documentation/git-add.adoc
+++ b/Documentation/git-add.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
[synopsis]
git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]
- [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
+ [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--no-verify]
[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
[--] [<pathspec>...]
@@ -42,6 +42,9 @@ use the `--force` option to add ignored files. If you specify the exact
filename of an ignored file, `git add` will fail with a list of ignored
files. Otherwise it will silently ignore the file.
+A pre-add hook can be run to inspect or reject the add operation before
+it stages files. See linkgit:githooks[5] for details.
+
Please see linkgit:git-commit[1] for alternative ways to add content to a
commit.
@@ -163,6 +166,10 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.
Don't add the file(s), but only refresh their stat()
information in the index.
+`--no-verify`::
+ Bypass the pre-add hook if it exists. See linkgit:githooks[5] for
+ more information about hooks.
+
`--ignore-errors`::
If some files could not be added because of errors indexing
them, do not abort the operation, but continue adding the
@@ -451,6 +458,7 @@ linkgit:git-reset[1]
linkgit:git-mv[1]
linkgit:git-commit[1]
linkgit:git-update-index[1]
+linkgit:githooks[5]
GIT
---
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 056553788d..51156822bc 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -94,6 +94,23 @@ and is invoked after the patch is applied and a commit is made.
This hook is meant primarily for notification, and cannot affect
the outcome of `git am`.
+pre-add
+~~~~~~~
+
+This hook is invoked by linkgit:git-add[1], and can be bypassed with the
+`--no-verify` option. This hook is not invoked for `--interactive`, `--patch`,
+`--edit`, or `--dry-run`. It takes no parameters, and is invoked after pathspec
+validation and before any files are staged. Exiting with a non-zero status
+from this script causes the `git add` command to abort without modifying the
+index.
+
+This hook is invoked with the environment variable `GIT_INDEX_FILE`
+which points to the index file. This allows the hook to inspect what
+files would be staged before the operation proceeds.
+
+This hook is not invoked by `git commit -a` or `git commit --include` which
+still can run the pre-commit hook, providing a control point at commit time.
+
pre-commit
~~~~~~~~~~
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b3..7747b41d10 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -25,6 +25,7 @@
#include "strvec.h"
#include "submodule.h"
#include "add-interactive.h"
+#include "hook.h"
static const char * const builtin_add_usage[] = {
N_("git add [<options>] [--] <pathspec>..."),
@@ -36,6 +37,7 @@ static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
static int include_sparse;
+static int no_verify;
static const char *pathspec_from_file;
static int chmod_pathspec(struct repository *repo,
@@ -271,6 +273,7 @@ static struct option builtin_add_options[] = {
OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
+ OPT_BOOL( 0 , "no-verify", &no_verify, N_("bypass pre-add hook")),
OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
N_("override the executable bit of the listed files")),
@@ -576,6 +579,17 @@ int cmd_add(int argc,
string_list_clear(&only_match_skip_worktree, 0);
}
+ if (!show_only && !no_verify) {
+ struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+ strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s",
+ repo_get_index_file(repo));
+ if (run_hooks_opt(repo, "pre-add", &opt)) {
+ exit_status = 1;
+ goto finish;
+ }
+ }
+
transaction = odb_transaction_begin(repo->objects);
ps_matched = xcalloc(pathspec.nr, 1);
diff --git a/t/t3706-pre-add-hook.sh b/t/t3706-pre-add-hook.sh
new file mode 100644
index 0000000000..e64ee51b25
--- /dev/null
+++ b/t/t3706-pre-add-hook.sh
@@ -0,0 +1,117 @@
+#!/bin/sh
+
+test_description='pre-add hook tests
+
+These tests run git add with and without pre-add hooks to ensure functionality. Largely derived from t7503 (pre-commit and pre-merge-commit hooks) and t5571 (pre-push hooks).'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'with no hook' '
+ test_when_finished "rm -f actual" &&
+ echo content >file &&
+ git add file &&
+ test_path_is_missing actual
+'
+
+test_expect_success POSIXPERM 'with non-executable hook' '
+ test_when_finished "rm -f actual" &&
+ test_hook pre-add <<-\EOF &&
+ echo should-not-run >>actual
+ exit 1
+ EOF
+ chmod -x .git/hooks/pre-add &&
+
+ echo content >file &&
+ git add file &&
+ test_path_is_missing actual
+'
+
+test_expect_success '--no-verify with no hook' '
+ echo content >file &&
+ git add --no-verify file &&
+ test_path_is_missing actual
+'
+
+test_expect_success 'with succeeding hook' '
+ test_when_finished "rm -f actual expected" &&
+ echo "pre-add" >expected &&
+ test_hook pre-add <<-\EOF &&
+ echo pre-add >>actual
+ EOF
+
+ echo content >file &&
+ git add file &&
+ test_cmp expected actual
+'
+
+test_expect_success 'with failing hook' '
+ test_when_finished "rm -f actual" &&
+ test_hook pre-add <<-\EOF &&
+ echo pre-add-rejected >>actual
+ exit 1
+ EOF
+
+ echo content >file &&
+ test_must_fail git add file
+'
+
+test_expect_success '--no-verify with failing hook' '
+ test_when_finished "rm -f actual" &&
+ test_hook pre-add <<-\EOF &&
+ echo should-not-run >>actual
+ exit 1
+ EOF
+
+ echo content >file &&
+ git add --no-verify file &&
+ test_path_is_missing actual
+'
+
+test_expect_success 'hook receives GIT_INDEX_FILE environment variable' '
+ test_when_finished "rm -f actual expected" &&
+ echo "hook-saw-env" >expected &&
+ test_hook pre-add <<-\EOF &&
+ if test -z "$GIT_INDEX_FILE"
+ then
+ echo hook-missing-env >>actual
+ else
+ echo hook-saw-env >>actual
+ fi
+ EOF
+
+ echo content >file &&
+ git add file &&
+ test_cmp expected actual
+'
+
+test_expect_success 'with --dry-run (show-only) the hook is not invoked' '
+ test_when_finished "rm -f actual" &&
+ test_hook pre-add <<-\EOF &&
+ echo should-not-run >>actual
+ exit 1
+ EOF
+
+ echo content >file &&
+ git add --dry-run file &&
+ test_path_is_missing actual
+'
+
+test_expect_success 'hook is invoked with git add -u' '
+ test_when_finished "rm -f actual expected file" &&
+ echo "initial" >file &&
+ git add file &&
+ git commit -m "initial" &&
+ echo "pre-add" >expected &&
+ test_hook pre-add <<-\EOF &&
+ echo pre-add >>actual
+ EOF
+
+ echo modified >file &&
+ git add -u &&
+ test_cmp expected actual
+'
+
+test_done
base-commit: b2826b52eb7caff9f4ed6e85ec45e338bf02ad09
--
gitgitgadget
next reply other threads:[~2026-02-10 15:32 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-10 15:32 Chandra Kethi-Reddy via GitGitGadget [this message]
2026-02-10 18:16 ` [PATCH] add: support pre-add hook Junio C Hamano
2026-02-10 19:00 ` Junio C Hamano
2026-02-11 15:16 ` [PATCH] " Chandra
2026-02-11 15:05 ` [PATCH v2] " Chandra Kethi-Reddy via GitGitGadget
2026-02-11 19:50 ` Junio C Hamano
2026-02-11 21:11 ` Chandra
2026-02-11 21:24 ` Junio C Hamano
2026-02-11 21:54 ` Chandra
2026-02-25 2:15 ` [PATCH v3] " Chandra
2026-02-27 5:54 ` Chandra Kethi-Reddy via GitGitGadget
2026-03-03 23:06 ` Junio C Hamano
2026-03-04 9:49 ` Ben Knoble
2026-03-05 10:47 ` Phillip Wood
2026-03-05 11:40 ` [PATCH v4] " Chandra
2026-03-05 14:48 ` [PATCH v3] " Junio C Hamano
2026-03-05 11:36 ` [PATCH v4] " Chandra Kethi-Reddy via GitGitGadget
2026-03-05 12:03 ` Adrian Ratiu
2026-03-05 12:37 ` Chandra
2026-03-05 12:37 ` [PATCH v5] " Chandra Kethi-Reddy via GitGitGadget
2026-03-05 13:41 ` Adrian Ratiu
2026-03-05 13:46 ` Chandra
2026-03-05 19:23 ` Junio C Hamano
2026-03-06 2:20 ` Chandra
2026-03-13 14:39 ` Phillip Wood
2026-03-05 14:37 ` Phillip Wood
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2045.git.1770737573475.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=chandrakr@pm.me \
--cc=git@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox