Safeguard
Supply Chain Security•

Git Release History 2005 to 3.0: Protocol v2, SHA-256, Signing

Git went from a 2005 first commit to 2.56 in 2026. Git 3.0 has not shipped; it will default to SHA-256, reftable and main. What changed for security.

Safeguard Security Team
41 min read

Git began with Linus Torvalds's first commit on 7 April 2005, reached 1.0 in December 2005 under maintainer Junio C Hamano, broke compatibility on purpose at 1.6.0 (2008) and 2.0 (2014), and as of 25 October 2026 the newest release is 2.56.0. Git 3.0 has not shipped and has no release date; it is planned to default new repositories to SHA-256, reftable and a branch called main.

For security teams the interesting part of that history is not the version numbers. It is a run of default changes that each closed a class of mistake: safer push behaviour, collision detection after SHAttered, a cleaner wire protocol, an ownership check called safe.directory, and commit signing that no longer needs a separate GnuPG keyring. This post walks the whole line from 2005 to the 3.0 plan, using the Git project's own release notes and documentation as the record.

Key takeaways

  • First commit 7 April 2005; every release tag checked from v1.0.0 (21 December 2005) on was made by Junio C Hamano.
  • Past deliberate breaking releases: 1.6.0 (August 2008) and 2.0 (May 2014). Git 3.0 is next and has "no planned release date" yet; the development notes call 2.98 the "antepenultimate" release before it.
  • Planned 3.0 defaults: SHA-256 and reftable for new repositories, main, mandatory Rust, and safe.bareRepository=explicit. SHA-1 repositories are not being deprecated.
  • Collision detection has been the default since 2.13 (May 2017); SHA-256 repositories since 2.29 (October 2020); interoperability is still being built.
  • Protocol v2 became the default in 2.26, was demoted in 2.27 and restored in 2.29.
  • safe.directory arrived in the March 2022 security releases and broke many CI jobs. SSH signing arrived in 2.34 (November 2021). A signed commit proves which key made it, not that anyone reviewed it.
  • The practical work: know which Git every machine and build image runs, keep it on a maintained track, verify signatures in CI, and keep safe.directory narrow.

For data on how ready forges and tools are for SHA-256, read Git SHA-256 transition: 2026 readiness data; this post does not repeat it.

How did Git begin, and how did Junio Hamano become maintainer?

April 2005: a replacement for a revoked licence

The Linux kernel had used a proprietary distributed version control system, BitKeeper, since 2002 under a free of charge licence. In 2005 the relationship between the kernel community and the company behind BitKeeper broke down and the free status was revoked. The Pro Git book on git-scm.com, which the Git project hosts, describes this as the trigger: the kernel community, and Torvalds in particular, decided to build their own tool using the lessons learned from BitKeeper. The design goals listed there are speed, a simple design, strong support for non linear development with thousands of parallel branches, full distribution, and the ability to handle a project the size of the kernel efficiently.

The first commit in the git/git repository is e83c5163316f89bfbde7d9ab23ca2e25604af290, authored by Linus Torvalds and dated 7 April 2005. Its message is "Initial revision of "git", the information manager from hell". Every clone of git.git still carries it as a root commit, under the same object name.

Summer 2005: the handover

Before the end of 2005, release duties had passed from Torvalds to Junio C Hamano. The tag objects in git.git are the cleanest primary record of who cut each release: v1.0.0, tagged on 21 December 2005, carries Junio C Hamano as tagger, and so does every major release tag checked for this post, from v1.5.0 through v2.45.0 in April 2024. The same maintainer has cut Git releases for more than twenty years, through a public mailing list, which makes the release notes an unusually consistent record.

Why the design choices of 2005 still matter

Three early design decisions shape everything that follows.

  1. Objects are named by a hash of their content. Blobs, trees, commits and tags are all stored under the SHA-1 of their contents. This gives integrity for free: if any byte changes, the name changes. It also ties Git's security to SHA-1.
  2. Repositories carry configuration and hooks. A .git directory holds a config file and a hooks directory that Git reads and runs. That became a trust boundary problem once repositories lived on shared machines, network drives and CI runners, and it is the root of safe.directory.
  3. Everything is local and distributed. Every clone is a full copy. There is no central server that can enforce policy at commit time, so signing and verification have to be things clients and CI do, not things the protocol guarantees.

Git release timeline from 1.0 to 2.56

The table below lists the releases that changed security relevant behaviour. Dates are tag dates from git.git tag objects or the release history shown on git-scm.com. Descriptions summarise the Git release notes for that version.

VersionDateSecurity relevant change (from the release notes)
First commit7 Apr 2005Content addressed object store using SHA-1
1.0.021 Dec 2005First 1.0 tag, made by Junio C Hamano
1.5.014 Feb 2007Large usability release that set the shape of modern Git
1.6.017 Aug 2008First deliberate breaking release; git-xyzzy dashed commands moved out of $PATH
1.7.012 Feb 2010Pushing into the checked out branch of a non bare repository is refused by default
2.0.028 May 2014Second breaking release; push.default becomes simple, so a bare git push only pushes the current branch
2.13.09 May 2017SHA-1 collision detection (the sha1dc implementation) integrated and made the default
2.18.021 Jun 2018Transport protocol v2 work lands
2.19.010 Sep 2018X.509 signing through gpg.format=x509 and gpgsm
2.26.022 Mar 2020Protocol v2 becomes the default
2.27.01 Jun 2020Protocol v2 demoted from the default because of remaining rough edges
2.29.019 Oct 2020Protocol v2 the default again; SHA-256 repositories available, marked experimental, with no interoperability with SHA-1 yet
2.34.015 Nov 2021SSH keys can sign commits, tags and push certificates
2.30.3 to 2.35.224 Mar 2022Ownership check for repositories (safe.directory), fixing CVE-2022-24765
2.36.018 Apr 2022safe.directory documented in the feature release; * trusts everything
2.42.021 Aug 2023Warning about SHA-256 repositories toned down; no further breaking changes to the format planned
2.45.029 Apr 2024Work starts on repositories that use both SHA-1 and SHA-256
2.48.010 Jan 2025Policy for introducing breaking changes documented
2.51.018 Aug 2025Release notes state SHA-256 and reftable will become defaults at Git 3.0
2.52.017 Nov 2025main announced as the 3.0 initial branch; "the beginning of SHA1-SHA256 interoperability work"
2.54.020 Apr 2026BreakingChanges updated
2.55.029 Jun 2026Rust support enabled by default in both build systems
2.56.028 Sep 2026Latest source release listed on git-scm.com at the time of writing
2.98 (in development)not releasedRelease notes on master describe it as the antepenultimate release before 3.0
3.0not releasedNo planned release date

A few of these deserve more than a line.

1.6.0 and 1.7.0: the first safety defaults

Git 1.6.0 in August 2008 is listed in the BreakingChanges document as the first major breaking release. Most of its changes were about command layout: the git-xyzzy hardlinks for built in commands stopped being installed on $PATH, so scripts had to say git xyzzy. It set the pattern of announcing a change well ahead, then flipping it at a version boundary.

Git 1.7.0 in February 2010 made pushing into the checked out branch of a non bare repository refused by default; the notes call these "safety features", which receive.denyCurrentBranch and receive.denyDeleteCurrent can relax. The theme recurs through every later section: the safe behaviour becomes the default, and turning it off becomes an explicit choice.

1.7.9: credential helpers and signed commits

Two features that every developer now relies on arrived together in the 1.7.9 release notes. First, the HTTP code used by git push and git fetch "learned to talk "credential API" to external programs to cache or store them, to allow integration with platform native keychain mechanisms." That is the origin of credential.helper. Second, git commit learned -S to sign a commit with GnuPG, with git log --show-signature to display it, and git pull learned to merge a signed tag and record its signature in the merge commit "for later auditing".

Which helper an image or laptop uses is a security decision: a keychain backed helper and one that writes tokens to a plain file carry different risks, and CI images inherit whatever was baked in.

2.0: push.default simple

Git 2.0 in May 2014 was the second breaking release. The change most teams noticed was push.default. Under the old matching behaviour, a bare git push pushed every local branch that had a same named branch on the remote. That made it easy to push a stale or experimental branch over someone else's work by accident. From 2.0 the default is simple: only the current branch, only to the branch of the same name, and only when it is set to integrate with that remote branch. Anyone who still wants the old behaviour has to set it explicitly. It is an integrity default more than a security control.

2.13 to 2.56 in brief

The later rows are covered in their own sections below, so here is only the thread that connects them.

  • Hashing. Collision detection became the default in 2.13.0 on 9 May 2017. The release notes credit the "detect attempt to create collisions" variant of SHA-1 by Marc Stevens (CWI) and Dan Shumow (Microsoft). SHA-256 repositories followed in 2.29, a stability statement in 2.42, interoperability work from 2.45 and 2.52, and the 3.0 default plan in 2.51. See the hash function section.
  • Transport. Protocol v2 was built during 2018, made the default in 2.26, pulled back in 2.27 because of "remaining rough edges", and restored in 2.29: "The transport protocol v2 has become the default again." Even a careful project ships a default, measures the fallout and reverses it. Pin Git in CI so you meet such changes on purpose.
  • Signing. SSH signing in 2.34.0 came with a warning that ssh-keygen from OpenSSH 8.7 has broken support for it and that you should use 8.8. A cross project version dependency like that is easy to miss in a base image.
  • Ownership. The safe.directory check shipped in six maintenance releases on 24 March 2022 and was restated in 2.36.0 a month later, with * available "to declare that you trust anything".
  • Process. Git 2.48.0 in January 2025 says simply "Describe the policy to introduce breaking changes." Since then each release has nudged toward 3.0: 2.52 warns about the main default, 2.55 turned Rust on by default in both build systems, and the 2.56 notes say Rust is "default, but still optional, and will become mandatory in Git 3.0".

What is planned for Git 3.0?

Status: not released, no date

As of 25 October 2026, Git 3.0 has not shipped. The latest source release on git-scm.com is 2.56.0, dated 28 September 2026. The BreakingChanges document, last updated in 2.55.0, says plainly: "There is no planned release date for this breaking version yet." The release notes file for the next version on the master branch is numbered 2.98 and opens by calling it the "antepenultimate release to prepare for the long anticipated Git 3.0", which reads as two more 2.x releases (2.98 and 2.99) before 3.0. Treat that as the maintainer's stated intent, not a schedule.

How Git manages breaking changes now

The BreakingChanges page says minor releases will not break backwards compatibility "unless there is a very strong reason to do so, like for example a security vulnerability". Breaking releases come irregularly, "typically measured in multiple years". The procedural rule is the interesting one for anyone running Git at scale: every planned breaking change must be guarded by a compile time switch, WITH_BREAKING_CHANGES. A Git built with that switch behaves as if 3.0 had already arrived, and the project runs a CI job against that build. You can do the same: build Git with WITH_BREAKING_CHANGES in a test image and run your tooling against it long before 3.0 is tagged.

Planned changes

AreaToday (2.56)Planned at 3.0Why it matters for security or operations
Hash for new repositoriessha1sha256SHA-1 has practical collision attacks; SHA-1 repositories are not being deprecated
Reference storagefilesreftableAtomic multi ref updates, no case or Unicode collisions in ref names
Initial branchmaster with a hintmainScripts that assume master break
RustDefault on, can be disabledMandatoryBuild images need a Rust toolchain; distribution impact will be evaluated
safe.bareRepositoryallexplicitCloses the embedded bare repository hook trick
Last 2.x releasenormal supportLong term supportBug fixes for at least four cycles, security fixes for six

The SHA-256 entry cites the attack history: the SHAppening (2015), SHAttered (2017), a birthday near collision with chosen prefix (2019) and Shambles (2020). It says the change depends on "the ecosystem" being ready, including popular Git libraries, applications and forges, and that "There is no plan to deprecate the "sha1" object format at this point in time."

The safe.bareRepository entry is the most directly security relevant. The document explains that it is easy to trick someone into cloning a repository that contains an embedded bare repository with hooks configured. If the user changes into that subdirectory, Git discovers the bare repository and its hooks can run, and many shell prompts run git status in the background, so merely changing directory can be enough. With explicit, Git refuses to use bare repositories found by walking up the directory tree; bare repositories named with --git-dir or GIT_DIR still work.

The long term support promise is new for Git. The last version before 3.0 will be declared a long term support release, receiving important bug fixes for at least four release cycles and security fixes for six, after which maintainership can pass to distributors. If your fleet cannot move to 3.0 quickly, that LTS line is the track to plan around.

Planned removals

The removals are mostly cleanup: grafts (replaced by git replace), git pack-redundant, the .git/branches/ and .git/remotes/ shorthand directories, name-rev --stdin, git whatchanged, core.commentString=auto, and writing symbolic refs as symbolic links (core.preferSymlinkRefs=true). The document also records things that will not be removed: git checkout stays alongside git switch and git restore.

For a supply chain team, the removals matter mainly as a checklist for internal scripts. A release pipeline that still calls git whatchanged or relies on grafts will break at 3.0.

How did the hash function transition progress?

The hash-function-transition design document on git-scm.com is the reference. In outline:

Why SHA-1 had to go

The document's objective is one line: "Migrate Git from SHA-1 to a stronger hash function." Its background section explains why. Git is "a content addressable filesystem". Using a hash to name content makes integrity checking easy, because a bit flip changes the name. Using a cryptographically secure hash adds two more properties: object names can be signed, so "third parties can trust the hash to address the signed object and all objects it references", and people can pass short strings around that reliably address stored content.

That is exactly why collisions matter. A signed tag signs a commit by name; the commit names its tree by hash; the tree names blobs by hash. If an attacker could produce two different objects with the same name, a signature over one would also cover the other. The document notes that SHAttered demonstrated a practical SHA-1 collision on 23 February 2017, that Git 2.13.0 moved to a hardened SHA-1 implementation that "isn't vulnerable to the SHAttered attack, but SHA-1 is still weak", and that "There's no guarantee that future attacks on SHA-1 won't be published in the future, and those attacks may not have viable mitigations."

Step 1: harden SHA-1 (2017)

Collision detection in 2.13 was the quick mitigation. The hardened implementation is not vulnerable to the SHAttered attack, in the transition document's words, but it does not make SHA-1 a strong hash again.

Step 2: SHA-256 repositories (2020)

Git 2.29.0, tagged on 19 October 2020, delivered what its notes call "the final leg of SHA-256 transition plus doc updates", marked experimental, with the warning that "there is no interoperability between SHA-1 and SHA-256 repositories yet". git init --object-format=sha256 creates such a repository, and object names become 64 hex characters instead of 40. At that point a SHA-256 repository could not exchange objects with a SHA-1 repository at all.

Step 3: stability (2023)

Git 2.42.0 in August 2023 toned down the warning on SHA-256 repositories "being an experimental curiosity", saying the project did not plan breaking changes to them. That is the point at which new projects could reasonably choose SHA-256, if their forge and tools supported it.

Step 4: interoperability (2024 onward)

The transition plan describes a repository that stores objects in SHA-256 but keeps a bidirectional mapping between SHA-1 and SHA-256 names in its index files, which the document calls a translation table. In that design, "Fetches from a SHA-1 based server convert the fetched objects into SHA-256 form and record the mapping", and pushes to a SHA-1 server convert objects into SHA-1 form "so the server does not have to be aware of the hash function the client is using." The local SHA-1 compatibility metadata can later be removed once it is no longer needed. The release notes record the start of this work in 2.45 (April 2024) and "the beginning of SHA1-SHA256 interoperability work" in 2.52 (November 2025). As of 2.56 it is not complete, and this post does not claim it is.

Step 5: default flip (planned for 3.0)

New repositories default to SHA-256 at 3.0. Existing SHA-1 repositories keep working.

Our SHA-256 readiness post covers which forges and libraries were ready in 2026. If you rewrite history to move a repository across formats, read Git ancestry lies after a history rewrite first, because every commit name changes.

What is protocol v2 and why did it matter?

Git has several transports (local, SSH, HTTP smart protocol, and the old git:// daemon), and over all of them it speaks a pack protocol. In the original protocol, the server begins by advertising every reference it has. For a repository with a very large number of refs, that advertisement is expensive even when the client wants one branch.

The protocol-v2 documentation on git-scm.com, first published with 2.18.0 on 21 June 2018, lists what v2 improves on v1:

  • One service that accepts multiple commands, instead of multiple service names.
  • Capabilities moved into their own section of the protocol, "no longer being hidden behind a NUL byte and limited by the size of a pkt-line".
  • The reference advertisement "will be omitted unless explicitly requested", with an ls-refs command to ask for specific refs.
  • A design built "with http and stateless-rpc in mind".

The client asks for v2 through a side channel that depends on the transport: a Git-Protocol: version=2 header on HTTP, the GIT_PROTOCOL environment variable on SSH and local file transports, and an extra version=2 parameter on the git:// transport. In every case the server answers with a capability advertisement. Communication from there is command oriented, and a client can reuse the connection for further commands. ls-refs takes arguments, such as a ref prefix, that "can be used to limit the refs sent from the server", so a client that wants one branch no longer receives every ref.

One operational detail from that document is easy to miss: over SSH, "The server may need to be configured to allow this environment variable to pass." If your self hosted SSH server does not accept GIT_PROTOCOL, clients quietly speak the older protocol.

The security relevance is indirect but real:

  • Less to parse on every fetch. A client that asks only for the refs it needs receives less untrusted input from the server, and servers spend less work on every request from automated clients such as CI.
  • A command structure that can be extended cleanly. Features such as partial clone filters and server side ref filtering were added through capabilities rather than by overloading the old format.
  • Parsing changes have consequences. The 2.27 notes include a fix where the v2 server side was "not prepared to see a delim packets at unexpected places, which led to a crash". New protocols mean new parser code, which deserves the same patching discipline as any other network facing code.

You can see what your client speaks with GIT_TRACE_PACKET=1 git ls-remote origin and look for version 2 in the trace. You can force it on older clients with git config --global protocol.version 2.

What does safe.directory protect against, and why did it break CI?

The problem

A Git repository's .git/config can set values that cause Git to run programs. core.fsmonitor names a program Git runs to learn which files changed. core.pager, core.editor, core.sshCommand, diff and merge drivers and hooks in .git/hooks all do something similar. That is fine when the repository is yours. It is a problem when the repository belongs to someone else on a shared machine.

The 2.30.3 release notes describe the case behind CVE-2022-24765 precisely: on multi user machines, a user might find themselves "unexpectedly in a Git worktree", for example when another user created a repository at C:\.git, on a mounted network drive or in a scratch space. Simply having a Git aware shell prompt that runs git status, or opening the directory in an editor that runs Git, could run commands defined by that other user. No cloning, no clicking: just being in a directory under someone else's .git.

The fix

From March 2022 Git checks who owns the repository. In the words of the 2.36.0 notes, Git "has been taught not to recognise repositories owned by other users, in order to avoid getting affected by their config files and hooks."

The git-config documentation adds the details that matter for safe configuration:

  • By default Git "will refuse to even parse a Git config of a repository owned by someone else, let alone run its hooks".
  • safe.directory is "only respected in protected configuration", meaning system and global config and the command line. A value inside a repository's own .git/config is ignored. Otherwise the untrusted repository could simply trust itself.
  • It is multi valued. Adding an entry with an empty value resets the list, which lets you clear an overly broad system wide entry before listing the paths you actually trust.
  • * opts out of the check entirely. A path with /* appended trusts every repository under that directory.
  • Values are interpolated, so ~/path expands relative to the home directory.
  • When Git runs as root under sudo, it also trusts repositories owned by the user recorded in SUDO_UID, so that make && sudo make install keeps working.

The 2024 security releases extended the same ownership idea to local clones. The 2.39.4 release notes say the ownership checks introduced in 2.30.3 "are now extended to cover cloning local repositories".

Why CI broke

CI runners are the multi user case without humans. Typical patterns:

  • The repository is checked out by one user (often root, or a runner service account) and the build steps run as another, for example inside a container with a different UID.
  • A workspace directory is mounted into a container where the UID inside does not match the owner on the host.
  • A cache restores a .git directory created by a different user on a previous run.

From 24 March 2022 onward, any of those made Git exit with "detected dubious ownership in repository". Scripts that called git describe, git rev-parse HEAD or git log failed. Many teams fixed it by adding git config --global --add safe.directory '*', and many of those lines are still there.

That fix trades a loud failure for a silent risk. A wildcard in the global config of a shared runner means any repository any job places on that machine is trusted, including ones from forks and pull requests. The narrow fix is to trust only the workspace path the job actually uses, or to make the UIDs match so no exception is needed at all.

What comes next: safe.bareRepository

safe.directory addresses ownership. It does not address a repository you cloned yourself, which you do own, that contains an embedded bare repository with hooks. That is what safe.bareRepository covers. It already exists, with default all for compatibility, and Git 3.0 plans to change the default to explicit. You can set safe.bareRepository=explicit in your global config today; only workflows that rely on Git discovering a bare repository by walking up from the current directory are affected.

How are Git security releases coordinated?

The release notes show a consistent pattern.

  1. Fixes for every maintained track at once. The March 2022 fix shipped as 2.30.3, 2.31.2, 2.32.1, 2.33.2, 2.34.2 and 2.35.2. The 2.35.2 notes say it "merges up the fixes that appear in" the older tracks. The 2024 clone hardening followed the same shape: the 2.45.1 notes say it merges up the fix from 2.39.4, 2.40.2, 2.41.1, 2.42.2, 2.43.4 and 2.44.1. Six or seven tracks receive the same fix together.
  2. The oldest track carries the full write up. In 2022 that was 2.30.3 and in 2024 2.39.4; newer tracks point back to it.
  3. Release notes that name the CVEs. Each security point release lists the CVE identifiers and a short description of the problem class, with credit to the reporter and the author of the fix.
  4. Published advisories. The git/git repository on GitHub has a security advisories page, which is the easiest place to subscribe.
  5. Downstream builds follow. Most machines do not run a Git built from the upstream tarball. They run a package from an operating system distribution, an installer, or a base image. Each of those has to pick up the release, and distributions often backport the fix to the version they already ship.

Operationally, a fixed version usually exists on the track you already run, so you rarely need to jump feature versions. You do need to know which track every machine and image is on: "we updated Git" on laptops says nothing about the Git inside an old CI base image.

Which signing options does Git support?

Git can sign commits, tags and push certificates. Since 2.19 the backend is chosen with gpg.format, which accepts openpgp (the default), x509 and, since 2.34, ssh.

OpenPGP (gpg.format=openpgp)SSH (gpg.format=ssh)X.509 (gpg.format=x509)
Commit signing since1.7.9 (git commit -S)2.34 (November 2021)2.19 (September 2018)
Program Git calls by defaultgpg (gpg.program is a legacy synonym for gpg.openpgp.program)ssh-keygen; the 2.34 notes require OpenSSH 8.8, not 8.7gpgsm
Key managementOpenPGP keyrings with their own expiry and revocationOrdinary SSH key pairs; an SSH CA key with cert-authority is also acceptedCertificates issued by an organisational certificate authority
What Git trusts when verifyingThe keyring and its trust levels; gpg.minTrustLevel sets a floorKeys listed in gpg.ssh.allowedSignersFile; SSH has no trust levels, so listed means trustedThe certificate chain as gpgsm evaluates it
Revoking a keyOpenPGP revocationgpg.ssh.revocationFile (a KRL or list of revoked keys)Through your certificate infrastructure
Key rotation without breaking historyKey expiry in OpenPGPvalid-after and valid-before on allowed signers entries (OpenSSH 8.8 or later)Certificate validity periods
Typical fitProjects with existing OpenPGP practiceTeams that already manage SSH keys and want the shortest pathOrganisations with existing smart card or certificate infrastructure
Hosting displayDepends on the forge; most need the public key registered on the accountDepends on the forge; most need the key registered as a signing keyDepends on the forge; check its documentation

Two points about signing are routinely misunderstood.

  • A signature proves possession of a key, not intent or review. A signed commit says the holder of a given key produced this commit object. It says nothing about whether anyone read it, whether tests ran, or whether the key was stolen.
  • Verification only means something against a trust list you control. For SSH signing, that is the allowed signers file. The git-config documentation describes three ways to run it: every developer keeps their own file outside the repository; a central server generates it from the SSH keys that have push access; or a repository that only allows signed commits stores it in the tree, so that "only committers with an already valid key can add or change keys". The third model only holds if verification reads the file from the trusted base, not from the change being verified. If CI reads it from a pull request's own tree, the pull request decides who is trusted.

If you are choosing SSH signing, our post on SSH security keys and ed25519-sk covers hardware backed keys, which keep the private key off the laptop. For policy over which signatures a repository accepts across branches and tags, gittuf's OpenSSF incubating status describes one approach that layers on top of Git.

Step by step: audit Git versions, sign with SSH, verify in CI, narrow safe.directory

This walkthrough uses commands documented in the Git manual. Adjust paths and image names to your environment.

Step 1: find which Git every machine and image runs

On a developer machine:

git --version
git config --show-origin --get-all safe.directory
git config --show-origin --get protocol.version
git config --show-origin --get gpg.format

--show-origin tells you which file set each value, which is how you find a safe.directory=* that someone put in /etc/gitconfig years ago.

Inside each CI base image:

for img in ghcr.io/example/build-base:2024 ghcr.io/example/build-base:2026 node:20-bookworm python:3.12-slim; do
  printf '%s: ' "$img"
  docker run --rm --entrypoint git "$img" --version 2>/dev/null || echo "no git in image"
done

Compare the results with the release history on git-scm.com and rebuild anything on an unmaintained track first. For distribution packages, the distribution's own security tracker says whether a fix was backported; the upstream version string alone does not.

Step 2: pin and update Git in the images you build

In a Dockerfile, install Git explicitly rather than relying on whatever the base image happens to include, and rebuild on a schedule:

FROM debian:bookworm-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends git openssh-client ca-certificates \
 && rm -rf /var/lib/apt/lists/*
RUN git --version

The RUN git --version line puts the version in your build log, which is useful evidence later. If you build Git from the upstream source instead, record that, because your vulnerability data must then track the upstream version.

Step 3: enable SSH commit and tag signing

# Use SSH signing (Git 2.34 or later, OpenSSH 8.8 or later)
git config --global gpg.format ssh

# The public key that matches the private key you sign with
git config --global user.signingkey ~/.ssh/id_ed25519_signing.pub

# Sign every commit and annotated tag by default
git config --global commit.gpgsign true
git config --global tag.gpgsign true

Use a dedicated signing key rather than your login key, and prefer a hardware backed ed25519-sk key if your machines support it. Register the public key with your forge as a signing key so its interface can show the verification state.

Step 4: create an allowed signers file and verify locally

The allowed signers file maps identities to public keys. Per the git-config documentation, it holds "one or more lines of principals followed by an ssh public key", and the full syntax is in the ALLOWED SIGNERS section of the ssh-keygen manual:

alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...alice
bob@example.com,bob@users.example.org sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t...bob

The principal "is only used to identify the key". Trust comes from the key being in the file at all: Git sets the trust level to fully when the key is present, and otherwise git verify-commit and git verify-tag fail. With OpenSSH 8.8 or later you can add valid-after and valid-before options so a rotated key keeps old signatures valid.

Point Git at it and check:

git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
git commit --allow-empty -m "signing test"
git log --show-signature -1
git verify-commit HEAD

git verify-commit exits non zero if the signature is missing or does not verify against the allowed signers file. git verify-tag v1.2.3 does the same for tags.

Step 5: verify signatures in CI

The pattern: fetch enough history to see the commits in the change, load the allowed signers file from a protected location, and fail if any commit in the range is unsigned or unverifiable. In GitHub Actions:

name: verify-signatures
on: pull_request
permissions:
  contents: read
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Load allowed signers from the base branch, not the PR
        run: |
          git show "origin/${{ github.base_ref }}:.github/allowed_signers" > "$RUNNER_TEMP/allowed_signers"
          git config gpg.ssh.allowedSignersFile "$RUNNER_TEMP/allowed_signers"
      - name: Verify every commit in the pull request
        run: |
          set -e
          base="origin/${{ github.base_ref }}"
          for c in $(git rev-list "$base..HEAD"); do
            echo "checking $c"
            git verify-commit "$c"
          done

Two things make this check meaningful. Reading the allowed signers file from the base branch means a pull request cannot add its own key. And running it as a required status check, alongside required review, means a signed but unreviewed commit still cannot merge. Squash and rebase merges create new commits, so decide whether you verify the contributor's commits, the result on the protected branch, or both.

Step 6: set safe.directory narrowly

First, try not to need it. If the checkout and the build run as the same user, there is no ownership mismatch. In a container you can often pass the host UID:

docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" -w /src build-image:2026 git describe --tags

When you do need an exception, trust only the path the job uses, at the narrowest config level that works:

# Trust one workspace for this command only
git -c safe.directory="$PWD" describe --tags

# Or trust one path for this user on this runner
git config --global --add safe.directory /home/runner/work/app/app

If a job checks out several repositories under one parent directory, the documented /* suffix trusts every repository under that directory and nothing else, which is still far narrower than a bare wildcard:

git config --global --add safe.directory '/home/runner/work/*'

Avoid git config --system --add safe.directory '*' on shared runners. If an old image already has a wildcard in its system config, a user can clear the list with an empty value in their own global config and then add back only what they need:

git config --global --add safe.directory ""
git config --global --add safe.directory /home/runner/work/app/app

Step 7: opt in to the 3.0 safety default now

git config --global safe.bareRepository explicit

Then run your normal workflows. If something relies on implicit discovery of a bare repository, pass --git-dir explicitly instead.

Step 8: test against the 3.0 behaviour

In a throwaway image, build Git from source with WITH_BREAKING_CHANGES set, as the BreakingChanges document describes, and run your release scripts, hooks and tooling against it. Look for anything that assumes 40 character object names, master, the files ref backend, or removed commands like git whatchanged.

Common mistakes

safe.directory=* everywhere. The wildcard was the fastest way to get pipelines green in March 2022, and it was copied widely into Dockerfiles and runner setup scripts. On a single user laptop it is mostly harmless. On a shared runner, a multi tenant build host or an image used by many jobs, it removes the exact protection the fix added. Search your images and runner configs for it and replace it with specific paths or matching UIDs.

Old Git in base images. Developer laptops update Git through the operating system or a package manager. CI images often do not, because nobody rebuilds a base image that still works. A build image from 2022 can still carry a Git from before the 2024 clone hardening. Treat Git like any other dependency in your images: inventory it, pin it, rebuild on a schedule.

Treating a signed commit as reviewed code. Signing tells you whose key produced a commit. It is a useful input to provenance, and it makes some forgery harder, but it is not review. Keep required review, required checks and branch protection, and treat signature verification as one more required check.

Ignoring hooks and config in cloned repositories. Hooks in .git/hooks are not cloned, but repositories can ship scripts and documentation that tell you to install them, tooling can set core.hooksPath to a directory inside the repository, and embedded bare repositories can carry their own hooks. Review what a repository asks you to install, set safe.bareRepository=explicit, and be careful with tooling that runs Git automatically in every directory you open.

Assuming 3.0 changes existing repositories. The SHA-256 and reftable changes apply to newly created repositories. Existing SHA-1 repositories are not being deprecated. Plan for new repositories created by automation, such as test fixtures and template generators, to change format.

Why Git's history matters for software supply chain security

Provenance records, SBOMs that cite a commit, version stamps from git describe and signed release tags all assume Git behaves as documented. The history above shows the project steadily moving risky behaviour from the default to an explicit choice.

The gap for most organisations is knowing where old behaviour still lives: base images, runner scripts, a system wide gitconfig. That is an inventory problem.

Safeguard's fit here is mostly around the software you build with Git rather than Git itself. Safeguard scans repositories, container images and SBOM files for open source components and matches them against vulnerability data, so the Git package inside a build image shows up alongside its other packages, and the platform shows which projects carry a given component and version. Policies and gates evaluated in the platform can Block, Warn or Notify when a project carries a component version you have ruled out, and a team that needs to ship anyway goes through an approved exception rather than deleting the rule. It does not manage developer laptops, does not configure your runners, and does not verify commit signatures for you; those stay with your Git configuration and CI.

The Gold block below applies to the open source dependencies in the projects your Git repositories hold. For Git itself, the fix almost always comes from the Git project or your distribution, and the work is getting it into every image.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The first demo is a real Safeguard CLI session in a repository. It reads a finding, edits a manifest, and runs install and tests, asking before any command that changes something and pushing nothing. The same flow applies when you want to bump a dependency in a repository whose history you sign: the CLI edits files, and you make and sign the commit.

Click through it at your own pace, or open it full screen.

The second demo sets up the Safeguard runner on your own machine, approves it with a device code, and scans from the terminal. That is the shape of an audit of your build images: run the scan where the images live and see every component, including the Git package, in one place.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude may use. Once connected, you can ask which projects carry a given component version, which is how you would find every image still on an old Git track.

Click through it at your own pace, or open it full screen.

To try it on your own repositories, download Safeguard and follow the CLI docs.

FAQ

Has Git 3.0 been released?

No. As of 25 October 2026 the latest release is 2.56.0, and the BreakingChanges document says there is no planned release date for 3.0. Development notes describe 2.98 as the antepenultimate release before it.

Will Git 3.0 break my existing SHA-1 repositories?

The plan is to change the default for new repositories to SHA-256. The BreakingChanges document says there is no plan to deprecate the SHA-1 object format.

What does "detected dubious ownership in repository" mean?

Git found a repository owned by a different user than the one running the command. Since March 2022 Git refuses to use such repositories unless the path is listed in safe.directory in system, global or command line config. Fix the ownership or add the specific path.

Is safe.directory '*' safe?

On a single user machine the risk is small. On shared CI runners or build hosts it removes the protection entirely, because it trusts any repository any job places on the machine. Use specific paths, or make the checkout and build users match.

What Git version do I need for SSH commit signing?

Git 2.34 or later, with OpenSSH 8.8 or later. The 2.34 release notes warn that ssh-keygen from OpenSSH 8.7 has broken support for this feature.

Does a signed commit mean the code is safe?

No. It shows which key created the commit. It does not show that anyone reviewed it, that tests passed, or that the key was not stolen. Use signature verification together with required review and checks.

Is protocol v2 enabled by default?

Yes. It was made the default in Git 2.26, demoted in 2.27 and made the default again in 2.29. Check with GIT_TRACE_PACKET=1 git ls-remote origin.

How do I get notified about Git security releases?

Watch the security advisories on the git/git repository on GitHub, follow your distribution's security announcements for its git package, and check the release notes, which name the CVEs fixed in each point release.

Quick check

Which two Git releases does the project list as its past major breaking releases?

Git 1.6.0 (August 2008) and Git 2.0 (May 2014). Git 3.0 will be the next one, and it has not been released.

Why is safe.directory ignored when it is set in a repository's own .git/config?

Because the setting exists to decide whether to trust a repository owned by someone else. If that repository could list itself in its own config, the check would be meaningless, so Git only reads it from system, global and command line config.

Sources

Last reviewed: 25 October 2026. Git 3.0 had not been released at the time of writing; check git-scm.com for the current release.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.