Content Addressing — SHA-1, SHA-256, and the Git 3.0 Switch
Content Addressing — SHA-1, SHA-256, and the Git 3.0 switch
Every Git object is named by a hash of its own bytes. That one decision is why history is tamper-evident, why two clones agree on what “commit 7c77260” means without talking to each other, and why deduplication is free. It is also why changing the hash function is the hardest migration Git has ever planned: the hash is the name, so a new function renames every object in every repository.
Git has supported SHA-256 repositories since 2.29. The project’s BreakingChanges document now commits to making SHA-256 the default for new repositories in Git 3.0 (no release date yet), alongside main as the initial branch and reftable as the ref backend. This chapter shows what content addressing actually hashes, what a SHA-256 repository looks like, what does not cross between the two formats, and how to get ready without breaking anything today.
Mental model
object bytes = "<type> <size>\0" + payload
object name = H(object bytes) H = SHA-1 (40 hex, 20 bytes)
H = SHA-256 (64 hex, 32 bytes)
blob ──┐
blob ──┼── tree ── commit ── commit ── … every arrow is a hash
blob ──┘ → change one byte, every name above it changes
one repository = ONE object format (extensions.objectFormat)
two formats = two different names for the same content → no shared history on the wire
| SHA-1 repository | SHA-256 repository | |
|---|---|---|
| Object name length | 40 hex | 64 hex |
core.repositoryformatversion |
0 (or 1) | 1 |
extensions.objectFormat |
unset (or sha1) |
sha256 |
| Empty tree | 4b825dc… |
6ef19b4… |
| Push/fetch with the other format | refused | refused |
| Default today (2.56) | yes | --object-format=sha256 |
| Default in Git 3.0 (planned) | no | yes, for new repositories |
SHA-1 is not being removed: BreakingChanges states there is no plan to deprecate the sha1 format. Existing repositories keep working. What changes is what git init creates when you do not say.
Worked examples
All cases were run on Debian 13 with Git 2.56.0 built from the release tarball, plus two extra builds of the same source: one with WITH_BREAKING_CHANGES=YesPlease (a preview of Git 3.0 defaults) and one with Rust enabled (needed for Case 6). Commands assume git is 2.56; the other two builds are called by full path.
git --versiongit version 2.56.0
To make the object names below reproducible on your machine, fix identity and dates first:
export GIT_AUTHOR_NAME=Ada GIT_AUTHOR_EMAIL=ada@example.com
export GIT_COMMITTER_NAME=Ada GIT_COMMITTER_EMAIL=ada@example.com
export GIT_AUTHOR_DATE='2026-10-08T09:00:00+05:30' GIT_COMMITTER_DATE='2026-10-08T09:00:00+05:30'Case 1: hash an object by hand
A blob is "blob <size>\0" followed by the file contents. Hash those bytes with the ordinary coreutils tools and compare with what Git computes in a repository of each format:
printf 'hello\n' > hello.txt
printf 'blob 6\0hello\n' | sha1sum
printf 'blob 6\0hello\n' | sha256sum
git init -q s1
git init -q --object-format=sha256 s256
for r in s1 s256; do (cd $r && cp ../hello.txt . && echo "$r: $(git hash-object hello.txt)"); donece013625030ba8dba906f756967f9e9ca394464a -
2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4 -
s1: ce013625030ba8dba906f756967f9e9ca394464a
s256: 2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4
Same bytes, same algorithm, same name, with no Git involved. That is the whole trick, and it is also the whole problem: the SHA-256 name of hello shares nothing with its SHA-1 name.
Case 2: what a SHA-256 repository is
for r in s1 s256; do
(cd $r && echo "== $r" &&
git rev-parse --show-object-format --show-ref-format &&
git config --get core.repositoryformatversion; git config --get extensions.objectformat)
done== s1
sha1
files
0
== s256
sha256
files
1
sha256
The format is one line of repository config plus a format-version bump to 1, which makes Git versions that predate repository extensions refuse to open the repository rather than misread it. Never edit extensions.objectFormat by hand on a repository with objects in it: the existing objects keep their old names, and Git will not find them.
Commits look the same, just with longer names:
git init -q -b main --object-format=sha256 app256 && cd app256
echo 'hello' > README && git add README && git commit -q -m 'first commit'
git log --format='%H %s'
git rev-parse --short HEAD
git cat-file -p HEAD
git log --format=%H | awk '{print length($0)}'
cd ..7c77260fad0cad11b0ae2ca24098d6d36ee56d96ad221d0d6f485f0911ac3c33 first commit
7c77260
tree cacbd5aeac8aafd15f1acb153e9a189e0b607b7ee7d874aee3534d1babdfcb21
author Ada <ada@example.com> 1791430200 +0530
committer Ada <ada@example.com> 1791430200 +0530
first commit
64
Abbreviated names stay seven characters by default, so everyday output barely changes. Scripts that consume full names are a different story (Case 4).
Case 3: what does not cross between formats
git init -q --bare -b main remote-sha1.git
(cd app256 && git push ../remote-sha1.git main)
git init -q -b main app1 && (cd app1 && echo x > f && git add f && git commit -q -m one)
(cd app256 && git fetch ../app1 main)
git clone -q app256 clone256 && git -C clone256 rev-parse --show-object-formatfatal: the receiving end does not support this repository's hash algorithm
fatal: the remote end hung up unexpectedly
fatal: mismatched algorithms: client sha256; server sha1
sha256
- Push and fetch across formats are refused, in both directions. No silent conversion happens.
- Clone adopts the remote’s format. A SHA-256 server produces SHA-256 clones; you do not choose.
- The consequence for teams: a repository’s format is a property of the project, decided once at creation, and every mirror, fork, CI cache, and forge that touches it must speak that format.
BreakingChangesmakes the Git 3.0 default switch conditional on exactly that ecosystem readiness: libraries, applications, and forges. Check your hosting platform, CI runners, and Git libraries before you create SHA-256 repositories that others must use.
Case 4: tooling that hard-codes SHA-1
Two assumptions break first: “an object name is 40 hex characters” and magic constants like the empty-tree ID.
for r in app1 app256; do
echo "== $r ($(git -C $r rev-parse --show-object-format))"
git -C $r hash-object -t tree /dev/null
git -C $r rev-parse HEAD | grep -Exq '[0-9a-f]{40}' && echo "40-hex check: pass" || echo "40-hex check: FAIL"
git -C $r rev-parse HEAD | grep -Exq '[0-9a-f]{40}([0-9a-f]{24})?' && echo "40-or-64 check: pass"
done== app1 (sha1)
4b825dc642cb6eb9a060e54bf8d69288fbee4904
40-hex check: pass
40-or-64 check: pass
== app256 (sha256)
6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321
40-hex check: FAIL
40-or-64 check: pass
Audit hooks, CI scripts, and database columns for:
| Assumption | Where it hides | Replace with |
|---|---|---|
[0-9a-f]{40} |
Hook validation, log parsers, webhook handlers | Accept 40 or 64, or ask git rev-parse --show-object-format |
CHAR(40) / fixed-width columns |
Deploy trackers, review tools | Width 64 |
4b825dc… empty tree |
“diff against nothing” scripts | git hash-object -t tree /dev/null |
0000…0000 (40 zeros) for created/deleted refs |
pre-receive / update hooks |
Compare against a string of zeros of the received length |
| “short SHA is unique enough” in URLs | Dashboards | Unchanged; abbreviation rules are the same |
Case 5: previewing Git 3.0 defaults, and pinning yours
Git’s source can be built with the planned Git 3.0 behaviour switched on:
make prefix=/opt/git-3preview NO_TCLTK=1 NO_RUST=1 WITH_BREAKING_CHANGES=YesPlease all
sudo make prefix=/opt/git-3preview NO_TCLTK=1 NO_RUST=1 WITH_BREAKING_CHANGES=YesPlease install
/opt/git-3preview/bin/git version --build-options | grep defaultdefault-ref-format: reftable
default-hash: sha256
/opt/git-3preview/bin/git init g3
cd g3 && /opt/git-3preview/bin/git rev-parse --show-object-format --show-ref-format && /opt/git-3preview/bin/git symbolic-ref HEADhint: Using 'main' as the name for the initial branch since Git 3.0.
hint: If you expected Git to create 'master', the just-created
hint: branch can be renamed via this command:
hint:
hint: git branch -m master
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /tmp/vcs/g3/.git/
sha1
reftable
refs/heads/main
main and reftable flipped as documented. The object format did not: this 2.56.0 preview build reports default-hash: sha256 in its build options, but in our runs git init without other settings (also tried with an empty environment) still created a SHA-1 repository. The --object-format flag, init.defaultObjectFormat, and GIT_DEFAULT_HASH all produced SHA-256 as expected. Reading the source shows why tests may not notice: t/test-lib.sh exports GIT_DEFAULT_HASH from the build’s default-hash. The lesson is the boring one: a preview build is a preview. Do not infer the future default from it; state the format you want.
Stating it is cheap and works on today’s Git, on the preview, and on Git 3.0:
git -c init.defaultObjectFormat=sha256 init -q -b main k1 && git -C k1 rev-parse --show-object-format
GIT_DEFAULT_HASH=sha256 git init -q -b main k2 && git -C k2 rev-parse --show-object-format
/opt/git-3preview/bin/git -c init.defaultRefFormat=files -c init.defaultBranch=master init -q k3
/opt/git-3preview/bin/git -C k3 rev-parse --show-object-format --show-ref-format
/opt/git-3preview/bin/git -C k3 symbolic-ref HEADsha256
sha256
sha1
files
refs/heads/master
For a fleet, put the choices in system or global config (or your provisioning tooling) so the Git 3.0 upgrade changes nothing you did not decide:
# /etc/gitconfig (or ~/.gitconfig) — make today's defaults explicit
[init]
defaultObjectFormat = sha1 # or sha256, once your forge and tools are ready
defaultRefFormat = files
defaultBranch = mainCase 6: the interoperability work (compatObjectFormat)
Git 2.52’s release notes mark “the beginning of SHA1-SHA256 interoperability work”: a SHA-256 repository that also maintains a SHA-1 name for every object, so that it can one day talk to SHA-1 peers. In 2.56 the switch exists but needs a Rust-enabled build. The default NO_RUST=1 build refuses:
git init -q -b main --object-format=sha256 compat && cd compat
git config extensions.compatObjectFormat sha1
echo hi > a && git add afatal: compatibility hash algorithm support requires Rust
With Git 2.56.0 built with Rust enabled (rust: enabled in --build-options, rustc 1.99), the same steps work. Then build the same content in a plain SHA-1 repository and compare:
G=/opt/git-2.56-rust/bin/git
$G init -q -b main --object-format=sha256 compat && cd compat
$G config extensions.compatObjectFormat sha1
echo hi > a && $G add a && $G commit -q -m c1
$G rev-parse HEAD
$G rev-parse --output-object-format=sha1 HEAD
ls .git/objects
cd .. && $G init -q -b main plain1 && cd plain1
echo hi > a && $G add a && $G commit -q -m c1 && $G rev-parse HEAD
cd ../compat
$G rev-parse --output-object-format=sha1 'HEAD^{tree}'
$G -C ../plain1 rev-parse 'HEAD^{tree}'
$G cat-file -t 15307cff4ea19f61d3bb0b98bc73102e2454352c
$G push ../r1.git main # r1.git: a bare SHA-1 repository13c18aac72ade018e9d88a6f80239f88397fdf997c3437bce30b870bf87e720a
15307cff4ea19f61d3bb0b98bc73102e2454352c
00
13
96
info
loose-object-idx
pack
15307cff4ea19f61d3bb0b98bc73102e2454352c
c49897f29f9819a0ab6850d7e22443508a1a29d5
c49897f29f9819a0ab6850d7e22443508a1a29d5
commit
fatal: the receiving end does not support this repository's hash algorithm
fatal: the remote end hung up unexpectedly
The SHA-256 repository computed the same SHA-1 commit and tree names as the native SHA-1 repository: it re-serialises each object the way a SHA-1 repository would and hashes that, recording the pairs in loose-object-idx. cat-file can already look objects up by their SHA-1 name. The network protocol cannot use them yet, so the push is still refused. This is the foundation for a future where a project migrates to SHA-256 while contributors on SHA-1 keep pushing. It is groundwork, not something to deploy.
Case 7: migrating an existing repository today
There is no in-place conversion. The supported route is to replay history into a new repository, which gives every commit a new name:
git init -q -b main --object-format=sha256 conv256
(cd app1 && git fast-export --all) | (cd conv256 && git fast-import --quiet)
git -C app1 log --format='%H %s'
git -C conv256 log --format='%H %s'aaec54ba980d0b7c3e8f4afe2fd0e72e8fa15d5d one
9e2c3721012ae0d829ab2717b83446230a71ab4fc873830ae5c98b7ee39aa75b one
Everything that refers to old names breaks: commit hashes quoted in messages and issues, signed commits and tags (the signature covered the old bytes), submodule pointers, CI caches, and every clone. Treat it like a history rewrite of the whole project: announce it, freeze pushes, convert, re-sign tags if you rely on signatures, and have everyone re-clone.
Cheat sheet
| Task | Command |
|---|---|
| Which format is this repo? | git rev-parse --show-object-format |
| Which ref backend? | git rev-parse --show-ref-format |
| What does this Git build default to? | git version --build-options \| grep default |
| New SHA-256 repo | git init --object-format=sha256 |
| Pin the default | git config --global init.defaultObjectFormat sha1\|sha256 |
| One-off override | GIT_DEFAULT_HASH=sha256 git init … |
| Hash a file for this repo’s format | git hash-object <file> |
| Empty-tree name for this repo | git hash-object -t tree /dev/null |
| Convert history (new names) | git fast-export --all \| git -C new fast-import |
Safety
- Changing
extensions.objectFormatin an existing repository does not convert anything. It just makes the repository unreadable. - Push and fetch refuse to cross formats, so you cannot corrupt a remote by accident. You can strand yourself by creating SHA-256 repositories your forge or CI cannot host.
- A converted repository is a different project as far as names go. Keep the SHA-1 original read-only for as long as anything references its commit IDs.
compatObjectFormatis experimental and Rust-only in 2.56. Do not enable it on repositories other people depend on.
Hands-on checklist
- Reproduce Case 1 and confirm
sha256sumof"blob 6\0hello\n"matchesgit hash-objectin a SHA-256 repository. - Run
git version --build-optionson your workstation, CI image, and server. Write downdefault-hashanddefault-ref-formatfor each. - Grep your hooks and CI for
{40},CHAR(40), and4b825dc. Fix what you find. - Add
init.defaultObjectFormat,init.defaultRefFormat, andinit.defaultBranchto the config you provision, set to what you use today. - Create a throwaway SHA-256 repository on your forge (if it allows that) and push to it from CI. That is the real readiness test.
Pitfalls
| Pitfall | Symptom |
|---|---|
| Assuming 40 hex everywhere | Hooks reject valid pushes; parsers truncate names |
| Hard-coded empty-tree ID | fatal: bad object 4b825dc… in a SHA-256 repo |
| Creating SHA-256 repos before the forge supports them | Push refused or repository unusable on the host |
| Mixing formats in mirrors or forks | mismatched algorithms on fetch |
| Trusting a preview build’s defaults | Repos created with a format you did not intend (Case 5) |
| Converting without a plan | Broken signatures, dead commit links, confused clones |
Sources
- Git — BreakingChanges (Git 3.0: SHA-256 default, reftable,
main, Rust): https://git-scm.com/docs/BreakingChanges
- Git — hash-function-transition (design, compat object format, translation tables): https://git-scm.com/docs/hash-function-transition
- Git —
git init(--object-format,--ref-format,init.defaultObjectFormat): https://git-scm.com/docs/git-init
- Git —
git rev-parse(--show-object-format,--output-object-format): https://git-scm.com/docs/git-rev-parse
- Git — repository-version / extensions (
extensions.objectFormat,extensions.compatObjectFormat): https://git-scm.com/docs/repository-version and https://git-scm.com/docs/git-config#Documentation/git-config.txt-extensionsobjectFormat
- Git v2.52.0 release notes (“The beginning of SHA1-SHA256 interoperability work”): https://github.com/git/git/blob/master/Documentation/RelNotes/2.52.0.adoc
- Git source,
hash.h(GIT_HASH_DEFAULTunderWITH_BREAKING_CHANGES) andt/test-lib.sh: https://github.com/git/git
- GitHub Blog — Highlights from Git 2.54: https://github.blog/open-source/git/highlights-from-git-2-54/