git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[RFC PATCH 4/4] gpg: add gpg.treeHash to sign a tree-sha256 header by default

From
SCScott Chacon <scott@gitbutler.net>
Date
Oct 2, 2026, 08:18 UTC
Message-ID
<20261002081846.25144-5-scott@gitbutler.net>
In-Reply-To
<20261002081846.25144-1-scott@gitbutler.net>

Someone who wants their signatures to cover the contents of their trees wants it for every tag and commit they sign, and shouldn't have to remember "--hash=sha256" each time, much as "tag.gpgSign" and "commit.gpgSign" save them from remembering "-s" and "-S".

Add "gpg.treeHash", which "git tag" and "git commit" use as the default for "--hash". It is a single variable rather than one for each command, since the reason for wanting it is the same for both.

It only applies to objects that are signed: with it set, unsigned commits and annotated or lightweight tags are made as before, rather than failing as an explicit "--hash=sha256" without signing does. "--hash=none" overrides it.

---
 Documentation/config/gpg.adoc |  6 +++++
 Documentation/git-commit.adoc |  2 +-
 Documentation/git-tag.adoc    |  2 +-
 builtin/commit.c              |  8 +++++++
 builtin/tag.c                 | 14 ++++++++++-
 t/t7032-tree-sha256-signed.sh | 44 +++++++++++++++++++++++++++++++++++
 tree-sha256.h                 |  4 ++--
 7 files changed, 75 insertions(+), 5 deletions(-)
diff --git a/Documentation/config/gpg.adoc b/Documentation/config/gpg.adoc
index 240e46c050..6728c13a62 100644
--- a/Documentation/config/gpg.adoc
+++ b/Documentation/config/gpg.adoc
@@ -16,6 +16,12 @@ gpg.format::
 See linkgit:gitformat-signature[5] for the signature format, which differs
 based on the selected `gpg.format`.
 
+gpg.treeHash::
+	When set to `sha256`, `git commit` and `git tag` add a
+	`tree-sha256` header to every commit and tag they sign, as if
+	`--hash=sha256` were given. Defaults to `none`. See the
+	`--hash` option in linkgit:git-commit[1] and linkgit:git-tag[1].
+
 gpg.<format>.program::
 	Use this to customize the program used for the signing format you
 	chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still
diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index c027de2adb..1ed6635eee 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -405,7 +405,7 @@ changes to tracked files.
 	digest of every file in the commit's tree, including the
 	contents of checked-out submodules, so that the signature covers
 	the content directly rather than only its SHA-1 object names.
-	_<algorithm>_ is `sha256`, or `none` (the default).
+	_<algorithm>_ is `sha256`, or `none` to override `gpg.treeHash`.
 	Giving `--hash=sha256` without signing is an error. All
 	submodules must be checked out.
 
diff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc
index 8901090a6d..445be41db1 100644
--- a/Documentation/git-tag.adoc
+++ b/Documentation/git-tag.adoc
@@ -89,7 +89,7 @@ OPTIONS
 	digest of every file in the tagged object's tree, including the
 	contents of checked-out submodules, so that the signature covers
 	the content directly rather than only its SHA-1 object names.
-	_<algorithm>_ is `sha256`, or `none` (the default).
+	_<algorithm>_ is `sha256`, or `none` to override `gpg.treeHash`.
 	Giving `--hash=sha256` without signing is an error. All
 	submodules must be checked out.
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 871a2bdcd7..7e56c434db 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1687,6 +1687,14 @@ static int git_commit_config(const char *k, const char *v,
 		sign_commit = git_config_bool(k, v) ? "" : NULL;
 		return 0;
 	}
+	if (!strcmp(k, "gpg.treehash")) {
+		if (!v)
+			return config_error_nonbool(k);
+		tree_hash = parse_signing_hash(v);
+		if (tree_hash < 0)
+			return error(_("invalid value for '%s': '%s'"), k, v);
+		return 0;
+	}
 	if (!strcmp(k, "commit.verbose")) {
 		int is_bool;
 		config_commit_verbose = git_config_bool_or_int(k, v, ctx->kvi,
diff --git a/builtin/tag.c b/builtin/tag.c
index 9bc4c946d1..86871317ed 100644
--- a/builtin/tag.c
+++ b/builtin/tag.c
@@ -51,6 +51,7 @@ static const char * const git_tag_usage[] = {
 static unsigned int colopts;
 static int force_sign_annotate;
 static int config_sign_tag = -1; /* unspecified */
+static int config_tree_hash;
 
 static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,
 		     struct ref_format *format)
@@ -223,6 +224,15 @@ static int git_tag_config(const char *var, const char *value,
 		return 0;
 	}
 
+	if (!strcmp(var, "gpg.treehash")) {
+		if (!value)
+			return config_error_nonbool(var);
+		config_tree_hash = parse_signing_hash(value);
+		if (config_tree_hash < 0)
+			return error(_("invalid value for '%s': '%s'"), var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "tag.forcesignannotated")) {
 		force_sign_annotate = git_config_bool(var, value);
 		return 0;
@@ -601,6 +611,8 @@ int cmd_tag(int argc,
 	}
 	create_tag_object = (opt.sign || annotate || msg.given || msgfile ||
 			     edit_flag || trailer_args.nr || opt.tree_hash);
+	if (!hash_arg)
+		opt.tree_hash = config_tree_hash;
 
 	if ((create_tag_object || force) && (cmdmode != 0))
 		usage_with_options(git_tag_usage, options);
@@ -704,7 +716,7 @@ int cmd_tag(int argc,
 	if (create_tag_object) {
 		if (force_sign_annotate && !annotate)
 			opt.sign = 1;
-		if (opt.tree_hash && !opt.sign)
+		if (opt.tree_hash && !opt.sign && hash_arg)
 			die(_("--hash=%s requires a signed tag (-s or -u)"), hash_arg);
 		path = repo_git_path(the_repository, "TAG_EDITMSG");
 		create_tag(&object, object_ref, tag, &buf, &opt, &prev, &object,
diff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh
index 44c363b5d2..5a656a7816 100755
--- a/t/t7032-tree-sha256-signed.sh
+++ b/t/t7032-tree-sha256-signed.sh
@@ -122,4 +122,48 @@ test_expect_success GPGSSH 'amending recomputes or drops the header' '
 	test_must_be_empty actual
 '
 
+test_expect_success GPGSSH 'gpg.treeHash signs the header by default' '
+	test-tool tree-sha256 HEAD >expect &&
+	test_config gpg.treeHash sha256 &&
+	git tag -s -m release v6 &&
+	header_of tag v6 >actual &&
+	test_cmp expect actual &&
+	test_tick &&
+	git commit --allow-empty -S -m signed &&
+	header_of commit HEAD >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success GPGSSH 'gpg.treeHash leaves unsigned objects alone' '
+	test_config gpg.treeHash sha256 &&
+	git tag -a -m annotated v7 &&
+	header_of tag v7 >actual &&
+	test_must_be_empty actual &&
+	git tag v8 &&
+	test "$(git cat-file -t v8)" = commit &&
+	test_tick &&
+	git commit --allow-empty -m unsigned &&
+	header_of commit HEAD >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success GPGSSH '--hash=none overrides gpg.treeHash' '
+	test_config gpg.treeHash sha256 &&
+	git tag -s --hash=none -m release v9 &&
+	header_of tag v9 >actual &&
+	test_must_be_empty actual &&
+	test_tick &&
+	git commit --allow-empty -S --hash=none -m signed &&
+	header_of commit HEAD >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success GPGSSH 'invalid gpg.treeHash is an error' '
+	test_config gpg.treeHash md5 &&
+	test_must_fail git tag -s -m release v10 2>err &&
+	test_grep "invalid value for .gpg.treehash." err &&
+	test_must_fail git commit --allow-empty -S -m signed 2>err &&
+	test_grep "invalid value for .gpg.treehash." err
+'
+
 test_done
diff --git a/tree-sha256.h b/tree-sha256.h
index dc070129ea..6d54c2c9f9 100644
--- a/tree-sha256.h
+++ b/tree-sha256.h
@@ -28,8 +28,8 @@ int tree_sha256_hex(struct repository *r, const struct object_id *oid,
 		    struct strbuf *hex);
 
 /*
- * Parse the value of a --hash=<algorithm> option. Returns 1 for
- * "sha256", 0 for "none", and -1 for anything else.
+ * Parse the value of a --hash=<algorithm> option or of gpg.treeHash.
+ * Returns 1 for "sha256", 0 for "none", and -1 for anything else.
  */
 int parse_signing_hash(const char *value);
 
-- 
2.50.1 (Apple Git-155)
Previous: Scott ChaconNext: Junio C Hamano
Message 7 of 22 in “sign a SHA-256 digest of the tree in commits and tags”
  1. 0/4 sign a SHA-256 digest of the tree in commits and tagsScott Chacon, Oct 2, 2026
  2. 1/4 tree-sha256: hash the contents of a tree with SHA-256Scott Chacon, Oct 2, 2026
  3. Junio C HamanoOct 2, 2026
  4. 2/4 tag: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  5. Junio C HamanoOct 2, 2026
  6. 3/4 commit: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  7. 4/4 gpg: add gpg.treeHash to sign a tree-sha256 header by defaultScott Chacon, Oct 2, 2026
  8. Junio C HamanoOct 2, 2026
  9. brian m. carlsonOct 2, 2026
  10. Scott ChaconOct 5, 2026
  11. Patrick SteinhardtOct 5, 2026
  12. Scott ChaconOct 5, 2026
  13. brian m. carlsonOct 5, 2026
  14. Johannes SchindelinOct 6, 2026
  15. Kristoffer HaugsbakkOct 6, 2026
  16. brian m. carlsonOct 6, 2026
  17. Junio C HamanoOct 6, 2026
  18. brian m. carlsonOct 6, 2026
  19. Christian CouderOct 6, 2026
  20. brian m. carlsonOct 6, 2026
  21. Christian CouderOct 7, 2026
  22. brian m. carlsonOct 7, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.