Skip to main content

Git 3.0 will change the default hash algorithm for newly created repositories from SHA-1 to SHA-256. You can run that future today with git init --object-format=sha256, which is exactly what we did. Then we pointed six widely used Git tools at the result.

Three of them broke. That part was expected. What was not expected is how they broke: of the five distinct failures we reproduced, precisely one error message mentioned the hash algorithm. The others reported a missing object, an unparseable reference, a missing commit, and a corrupt pack file. One of them passed git bundle verify first.

TL;DR

  • Git’s own BreakingChanges.adoc makes library, application and forge readiness an explicit precondition for the SHA-256 default, and states there is no planned release date for Git 3.0 yet.
  • We created a real SHA-256 repository with Git 2.43.0 and tested six tools. Working: the Git CLI, dulwich 1.2.15, simple-git 4.0.2. Broken: pygit2 1.20.1 (libgit2 1.9.7), GitPython 3.2.0, isomorphic-git 1.42.6.
  • Only pygit2 named the cause: unknown object format 'sha256'. isomorphic-git reported Invalid checksum in GitIndex buffer, which reads as repository corruption.
  • A SHA-256 git bundle passed git bundle verify with exit code 0, then failed the subsequent fetch with fatal: pack is corrupted (SHA1 mismatch). A SHA-1 control bundle passed both.
  • Neither init.defaultObjectFormat=sha1 nor GIT_DEFAULT_HASH=sha1 changes what git clone produces. An organisation-wide SHA-1 pin governs git init only.

What Git actually committed to

It is worth reading the primary source rather than the commentary. Git’s Documentation/BreakingChanges.adoc lists the hash change under Git 3.0, cites four published SHA-1 attacks as the motivation, then adds a sentence most coverage skips:

An important requirement for this change is that the ecosystem is ready to support the “sha256” object format. This includes popular Git libraries, applications and forges.

Separately: “There is no planned release date for this breaking version yet.” There is also no plan to deprecate SHA-1.

So the maintainers have made ecosystem readiness the gate, which reframes the question. Readiness is not a date to diary, it is a property of your toolchain you can measure this afternoon. We measured ours.

Six tools, one repository

The test repository was created with Git 2.43.0: one commit, two files, one tag, object format confirmed as sha256. Each library was installed fresh from its registry on 1 October 2026 and asked to do four ordinary things: resolve HEAD, walk the log, read the tree, read the commit message.

Working. The Git CLI, obviously. dulwich 1.2.15 passed every operation, returning the full 64-character object ID without complaint. simple-git 4.0.2 also passed everything, including revparse, log, status and diffSummary.

pygit2 1.20.1, linking libgit2 1.9.7, refused to open the repository at all: InvalidError: /tmp/sha256lab/repo256: unknown object format 'sha256'. This is the best failure in the set, because it is the only one that tells the truth. The cause is a compile-time flag. On libgit2’s maint/v1.9 branch, SHA-256 lives behind EXPERIMENTAL_SHA256=ON, which is off by default, so the wheel on PyPI cannot read the format. On libgit2 main that gate has since been removed, though the README still describes SHA256 support as experimental. The fix exists upstream and is not in anything you can install today.

isomorphic-git 1.42.6 failed every read with NotFoundError: Could not find 672ff97d…, and statusMatrix failed differently and much worse: Invalid checksum in GitIndex buffer: expected e1f1196e6ee3e55aab44075d1ee83168988b6826 but saw 352bef80af38b1e03d0a47ac74f5ce3d48d75cc9. Two 40-character hex strings and an accusation of corruption. Nothing in that sentence hints at an object format. In its shipped bundle, index.js parses protocol packet lines with line.slice(-41) and then rejects anything where oid.length !== 40.

The fault line is parsing, not reimplementation

The common framing is that libraries reimplementing Git break, while tools that shell out to the real git binary are safe. GitPython is the counter-example.

Its fork-exec surface works perfectly. g.git.rev_parse('HEAD'), g.git.log and g.git.cat_file all returned correct SHA-256 output. Its object model does not: g.head.commit, g.active_branch.commit and g.commit('HEAD') all raise ValueError: Failed to parse reference information from 'refs/heads/master'.

The cause is four characters. git/repo/base.py:140 defines re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$"), and git/refs/symbolic.py:300 uses it to decide whether a ref file contains a commit ID. A valid 64-character SHA-256 fails the match, so the parser concludes the ref file is malformed and blames the file. The same hardcoded 40 appears in git/refs/log.py:44, git/cmd.py:1912 and an outright assert len(hexsha) == 40 in git/objects/commit.py:592. Meanwhile git/repo/fun.py:87 already matches [0-9A-Fa-f]{64}|[0-9A-Fa-f]{40}.

That last detail is the one to take away. GitPython is partially migrated, and partial migration is harder to diagnose than none at all, because some code paths succeed and the failures look like data problems rather than compatibility problems.

The bundle that verified green

This is the result that should worry anyone running backups or air-gapped transfers.

We ran git bundle create /tmp/r256.bundle --all inside the SHA-256 repository, then switched to a SHA-1 repository and ran git bundle verify on it. It printed “The bundle records a complete history” and “The bundle uses this hash algorithm: sha256”, and it exited 0. Verified. Green.

The fetch from that same verified bundle then failed with fatal: pack is corrupted (SHA1 mismatch) and error: index-pack died.

We ran the control, because a single result like that is not evidence. A bundle created the same way from a SHA-1 repository verified with exit 0 and fetched with exit 0, creating the branch. The only variable was the source repository’s object format. A tool chain that verifies an archive and then restores it will therefore tell you, in sequence, that the archive is intact and that it is corrupt.

Cross-format fetching over a remote, by contrast, is handled honestly: fatal: mismatched algorithms: client sha1; server sha256. Adding a SHA-1 submodule to a SHA-256 superproject is not. The clone step prints “done.” and then the command dies with error: 'vendor/sha1lib/' does not have a commit checked out. The identical command against a SHA-1 superproject registered the submodule normally.

The format you cannot opt out of

The mitigation everyone reaches for is an organisation-wide default pinning new repositories to SHA-1. It works, and protects less than you would think.

We cloned the SHA-256 repository with git -c init.defaultObjectFormat=sha1 clone and the result was SHA-256. We cloned it with GIT_DEFAULT_HASH=sha1 and the result was SHA-256. That is correct behaviour, since a clone cannot rehash history, but the consequence is that your configuration governs repositories you create and says nothing about repositories you consume. The first SHA-256 repository in your estate will arrive as a dependency, a vendor handover or a contractor’s starter template.

And you will not spot it by eye. git log --format=%h returns seven characters in both formats. Every short hash in your logs, dashboards and commit messages looks exactly the same.

What to do about it, concretely

You cannot answer this question from a lockfile. pygit2’s behaviour was decided by a CMake flag in a transitive native dependency, and no manifest records it. The only reliable method is to run the repository and see what happens.

  1. Create the probe. git init --object-format=sha256 /tmp/probe, one commit, one tag. Ten seconds, and no upgrade required: Git has supported the flag for years.
  2. Point your actual toolchain at it, not a representative sample: release automation, changelog generators, hook runners, deployment scripts, anything that reads an object ID. Record the exact error text, because that text is what your on-call engineer will be reading at 2am.
  3. Grep your own code for the assumption, not just your dependencies: [0-9a-f]{40}, length === 40, len(sha) == 40, and any database column typed CHAR(40) or VARCHAR(40).
  4. Test restore, not just backup. The bundle result above only appears if you actually fetch from the artefact. A verification step that passes is not a restore that works.
  5. Do not budget for the conversion, budget for the diagnosis. Converting a repository is a known, scriptable job. Working out why a release pipeline is reporting a corrupt pack when nothing is corrupt is not.

None of this argues against SHA-256. Git’s maintainers have been careful: they gated the change on ecosystem readiness and have not set a date. The useful reading of “ecosystem readiness” is not “has every library shipped support” but “when one has not, does the error say so”. On today’s evidence, usually not.

At REPTILEHAUS this kind of measurement is part of our DevOps and platform work, because assumptions like a 40-character hash never appear in an audit until they break a deployment. If you want your own pipeline probed before Git 3.0 makes the question urgent, get in touch.


📷 Photo by Jan Antonin Kolar (@jankolar) on Unsplash