UrsaKit cover for the guide to migrating a Git repository from SHA-1 to SHA-256

Git hashes every object — blob, tree, commit — and stores it under that hash. Since 2005 that hash has been SHA-1, and the collision attacks published against SHA-1 are the reason the project added a second object format. Git 3.0 is planned to make SHA-256 the default, which turns a theoretical question into a scheduling problem for anyone maintaining a large repository.

This guide does the conversion on a real repository, shows the exact error you hit if you try the obvious clone approach, and lists the hardcoded assumptions that break when a 40-character hash becomes 64 characters.

01What changes when the object format changes

Git is a content-addressed database: an object's name is the hash of its content, so the hash function is part of the repository's identity. Two consequences follow immediately.

  • Every object gets a new name, so every commit hash changes.
  • A SHA-256 repository and a SHA-1 repository cannot share objects directly, because the same content hashes differently in each.

That second point is the one people underestimate. You cannot graft a SHA-256 clone onto a SHA-1 history and push; the object graphs are not compatible.

Fixed in your plan: this is a rewrite, not a setting

Switching object format changes the identity of every commit. Anything pinned to a SHA-1 commit — CI actions, release tags, submodule commits, deployment records — points at an object that no longer exists in the converted repository.

02Check what your repositories actually use

Start by measuring, not guessing. Every command below is runnable against a local repository.

git --version
# git version 2.50.1 (Apple Git-155)

git rev-parse --show-object-format
# sha1

rev-parse --show-object-format prints the format of the repository you are standing in. Run it across your checkouts to find which ones are still SHA-1, then size the job by object count.

git count-objects -vH
git rev-list --all --count

For a quick correctness probe, hash the same bytes in a SHA-1 repository and a SHA-256 one. The digest length and the digest itself both change:

cd sha1-repo  && printf 'hello\n' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a

cd sha256-repo && printf 'hello\n' | git hash-object --stdin
# 2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4

Create a scratch SHA-256 repository to see the difference without touching your real work:

git init --object-format=sha256 scratch
cd scratch && git commit --allow-empty -m init
git rev-parse HEAD
# a7bd209de3540f82c9736ffd0422615b8d8ebb7009ee32a6d9303f93f2807342

The commit id is 64 hex characters, not 40. That single difference ripples through every script that validates or truncates a hash.

03The obvious approach fails — here is the error

Creating a SHA-256 repository and adding a SHA-1 repository as a remote does not work. Fetching gives a hard error:

$ git init --object-format=sha256 converted
$ git -C converted remote add origin ../legacy.git
$ git -C converted fetch origin
fatal: mismatched algorithms: client sha256; server sha1

This is not a permissions problem or a missing flag; the transports refuse to move objects between formats. It rules out the migration strategy most people reach for first.

Do not plan around a cross-format clone

git clone has no object-format flag, and fetch across formats fails on current Git. If a guide tells you to clone into SHA-256, it is describing a version or a patch that does not ship yet.

04Convert through fast-export and fast-import

The reliable path is to serialize history as text and replay it into a new repository, letting the new repository hash everything with SHA-256. This is exactly what fast-export and fast-import are for, and it is verified below on a two-commit repository.

  1. Create the destination repository with the SHA-256 format.
git init --object-format=sha256 converted
  1. Stream every ref from the old repository into the new one. Run this from the source checkout, pointing at the destination path.
git fast-export --all --signed-tags=verbatim | \
  (cd ../converted && git fast-import --quiet)
  1. Confirm the result. The history is intact and the format has changed.
cd ../converted
git rev-parse --show-object-format
# sha256
git log --format='%H %s'
# 6b7d4f0bd580fe7030a11aecad57f6cbea14965fb64ea169e3d898f27c8fdae8 two
# 31b16d360e5e845a0895d08eafbcad0df340fb72cc1af4030bd02bf2b5e75e97 one

The commit subjects and order survive; the hashes do not, because they cannot.

  1. Restore the branch and tag names. fast-import writes the refs it was given, but check that your default branch, tags and any release refs are present before declaring success.
git branch -a
git tag --list
git fsck --full

git fsck --full should report no missing objects. If it does, the export was incomplete — most often because --all did not include a ref type you rely on, such as notes or a stash you expected to keep.

05Plan a rehearsed cutover, not a big bang

Rewrite once, then rehearse on a mirror. A workable sequence for a shared repository:

  1. Prepare a maintenance window and some downtime.
  2. Freeze pushes and convert a mirror clone into SHA-256.
  3. Grep the whole codebase and CI config for 40-character hex assumptions while contributors are paused.
  4. Push the converted repository as the new origin and force-push all branches.
  5. Have every clone re-clone rather than pull, because the old objects no longer match the new hashes.

That last step is unavoidable. Existing clones of the SHA-1 repository cannot fetch from the converted one, and a merge between them would duplicate history.

06Find everything that assumes a 40-character hash

This is where migrations actually fail, hours after the conversion looks successful. The hash is not just a Git detail; it leaks into tooling.

WhereAssumptionFix
CI cache keys40 hex charactersKey on the branch plus the full hash
Pinned GitHub ActionsCommit SHA in uses:Re-pin after the rewrite; the old SHA is gone
SubmodulesParent repo pins child commitRe-add the submodule from the converted child
Release notesLinks to commit URLsRegenerate links from the new hashes
Scriptssubstr($sha, 0, 7)Seven characters still works, but dedupe risk grows
Signed commits and tagsSignature covers the objectRe-sign; signatures do not carry across the rewrite

Search for the pattern rather than hoping to remember every location. A regular expression that matches a 40-character lowercase hex string is enough to start, and the regex tester is a fast way to tune it before you run it across a monorepo.

07Keep both formats working side by side

During the transition you will have SHA-1 and SHA-256 checkouts on the same machine, and git rev-parse --show-object-format tells you which one you are in. Set the format explicitly when you create tooling so a script does not silently assume the current directory is SHA-1:

git rev-parse --show-object-format
git -C /path/to/repo rev-parse --show-object-format

If a downstream system cannot be updated yet, keep a read-only SHA-1 mirror of the old repository for reference, but do not let anyone commit to it — diverging histories are worse than a delayed migration.

08Common errors and fixes

ErrorCauseFix
mismatched algorithmsFetching between object formatsConvert with fast-export / fast-import
History missing after import--all did not cover the desired refsExport specific refs or include notes and tags
CI can no longer check out a pinned SHAPinned to a pre-migration commitRe-pin to the converted commit
Push rejected after cutoverExisting clone has SHA-1 objectsRe-clone into a fresh working copy
git fsck reports dangling refsTemporary refs from the importDelete stale refs, then re-run git fsck

The migration is not difficult because of the object format. It is difficult because a hash is an identity that leaks into caches, pins and integrations that never mention SHA-1. Convert the repository with a script, then treat every hardcoded hash as a bug to hunt before the window closes.

Frequently asked questions

Can I convert a repository in place?

Not with a single command. Git has no in-place object-format conversion; the supported path is to rebuild the repository into a new one, which rewrites every object hash.

Does migration break commit hashes?

Yes, and that is the point. Every commit, tree and blob gets a new identifier, so tags, signed commits, submodule pins and external references all change.

Can a SHA-256 repository fetch from a SHA-1 remote?

Not in current Git. It fails with a mismatched algorithms error, which is why you convert through fast-export and fast-import rather than cloning.

Will my CI break?

Only where something assumes a 40-character hex string: cache keys, artifact paths, commit-pinned actions and deployment markers. Grep for 40-character patterns before cutover.

Is SHA-256 faster or slower?

SHA-256 objects are larger because the identifiers are longer, and hashing cost differs, but the practical effect is small compared with the migration work itself.

Back to articles

Related articles

Related tools