I was reading through the fix for a package manager CVE last week and ran into a git flag I’d somehow never noticed:
–end-of-options
. My first reaction was that some LLM had hallucinated it, but it’s
documented in
gitcli(7)
, it was added in git 2.24.0 in November 2019, and it exists because git had already used
—
for something else.
In most Unix tools
—
marks the end of option parsing, so
rm — -f
removes a file called
-f
rather than passing the force flag. Git had repurposed
—
early on to separate revisions from pathspecs, because
git log foo
on its own is ambiguous between a branch named
foo
and a file named
foo
and one of the two readings needed a marker:
git log main — README.md
means commits on
main
touching that file. That left the revision position with no terminator, so if a script runs
git log “$rev”
and
$rev
starts with a dash, git parses it as an option.
From the
commit that introduced
–end-of-options
:
But that doesn’t work for the revision parser, because
—
is already meaningful there: it separates revisions from pathspecs. So we need some other marker to separate options from revisions.
—
and
–end-of-options
are different things in git, and treating them as interchangeable is a mistake I’ve now seen in several places. Putting
—
before a URL in
git clone — “$url”
works, because
clone
follows the POSIX convention. A trailing
—
after a ref, as in
git checkout “$ref” —
, marks
$ref
as a revision rather than a filename but still lets it be read as an option first. Passing an untrusted revision safely means writing
git log –end-of-options “$rev” — “$path”
, with both markers doing separate jobs.
Support for the new flag arrived per subcommand rather than all at once:
git rev-parse
only got it in 2.30.0
, a year after the initial release, because it has its own hand-rolled argument parser, and
git checkout
and
git reset
rejected it until 2.43.1
in February 2024 because they parse
—
themselves and the initial implementation left
–end-of-options
in the argument list where their parsers rejected it.
Argument injection
Git, hg, and ssh all ship options whose documented purpose is to run a command the caller names.
git clone
accepts
–upload-pack=
to specify the server-side binary, and any
git
invocation accepts
-c core.sshCommand=
to override how it connects. Mercurial accepts
–config=alias.=!
on any subcommand, which redefines the subcommand you’re running as an arbitrary shell script.
ssh
accepts
-oProxyCommand=
. These are documented features that become attack primitives when a wrapping program passes an untrusted string into the argument list.
The failure mode has its own CWE,
CWE-88
, argument injection, and it’s distinct from command injection because there’s no shell involved: the wrapping program builds an argv array and calls
exec
directly, exactly as every “don’t use
system()
” guide recommends, the array reaches git intact, and git then parses one of the arguments as an option because it starts with a dash.
CVE-2019-13139
in
docker build
is a clean example: Go’s
os/exec
package, an argv array, no shell, and a git-context URL whose
#ref:dir
fragment reached
git fetch origin
as
–upload-pack=
.
The pattern was demonstrated across four version control systems on the same day in August 2017, when
CVE-2017-1000117
(git),
CVE-2017-1000116
(Mercurial),
CVE-2017-9800
(Subversion), and
CVE-2017-12836
(CVS) were disclosed together. Each passed a URL’s hostname to
ssh
as an argument, and a hostname starting with
-oProxyCommand=
became an ssh option. Phabricator’s
post-mortem
on the disclosure noted that of the three actively maintained tools, only Subversion actually added
—
before the hostname in its fix; git and Mercurial validated the hostname format instead, partly because
—
isn’t supported by every ssh implementation. The same write-up called the
—
mechanism itself “unsafe by default”, since code without it looks correct and works fine right up until an argument starts with a dash.
Package managers
Package managers routinely take a git URL or ref as data and pass it to a subprocess:
gem ‘foo’, git: ‘…’
in a Gemfile,
github:user/repo#ref
in a
package.json
, and equivalents in
pyproject.toml
,
Cargo.toml
,
mix.exs
,
Package.swift
,
pubspec.yaml
,
conanfile.py
, and
go.mod
. The URL and ref arrive in a manifest, a lockfile, or a transitive dependency’s metadata.
Of nineteen package managers I checked
1
, seventeen fork the
git
binary as their default or only path. The two that default to a library are Cargo, which uses
libgit2
with an opt-in
net.git-fetch-with-cli
setting to fork instead, and Poetry, which switched to
dulwich
in
1.2.0
with a
system-git-client
setting to fall back. Nix uses libgit2 for reading local repositories but forks
git
for fetches, because
libgit2 lacks git-credential helper support
.
The published CVEs against package managers in this class include
CVE-2021-43809
(Bundler),
CVE-2021-29472
and
CVE-2022-24828
(Composer),
CVE-2022-36069
(Poetry),
CVE-2023-5752
(pip),
CVE-2022-21223
and
CVE-2022-24440
(CocoaPods), and
CVE-2025-68119
(Go). The Snyk research that produced several of the 2022 entries is
written up here
, and Sonar maintains a
catalogue of the dangerous options
per binary.
Of the seventeen that fork git, exactly one uses
–end-of-options
: Go’s
cmd/go
. It
added
—
before repository URLs in June 2019 as a general hardening pass. In January 2026 that turned out to be insufficient and
–end-of-options
was
added across the board
as the fix for CVE-2025-68119, along with
HGPLAIN=+strictflags
, which has
restricted Mercurial’s early-option parsing since hg 4.4.2
in 2017. The commit message ends: “We should probably follow up with a more structured change to make it harder to accidentally re-introduce these issues in the future, but for now this addresses the issue at hand.”
Minimum git versions
The other package managers that guard the argument list at all use
—
or a leading-dash check on the input, and looking at when each guard was added, most arrived as the fix for a reported vulnerability rather than in the original implementation. The
—
before the URL in Bundler’s
git clone
is
the CVE-2021-43809 patch
. The leading-dash rejection in cocoapods-downloader
landed in three commits
over ten days in March 2022, matching the CVE-2022-21223 disclosure. Poetry’s guard
arrived in September 2021
with a CVE assigned a year later, and the switch to dulwich followed six months after that. vcpkg is the exception I found where
—
was
present from the day
git registry support was written.
Composer’s
advisory for CVE-2022-24828
explains why almost none of these tools use
–end-of-options
: it names the flag as the correct fix and then says Composer supports git versions that predate it, so the patch rejects leading-dash branch names instead. vcpkg’s git integration has a comment stating a floor of git 2.7.4. Homebrew’s
HOMEBREW_MINIMUM_GIT_VERSION
is 2.7.0 on Linux,
set in 2018
.
Amazon Linux 2, which packaged git 2.14.3, reached end of life last month, so the distributions those floors track are only now ageing out. Ubuntu 18.04 with git 2.17.0 is in extended support until 2028. Ubuntu 20.04, in extended support until 2030, packages 2.25.1, which is new enough to accept
–end-of-options
on
git fetch
but old enough to reject it on
git rev-parse
. Relying on the flag means raising the minimum git to 2.24.0 for most subcommands, 2.30.0 for
rev-parse
, or 2.43.1 for
checkout
and
reset
, and losing anyone still on the distribution-packaged git.
Git libraries
libgit2,
gitoxide
,
go-git
,
JGit
, and dulwich all implement enough of the git wire protocol to clone and fetch in-process, with no argv boundary and so no argument list to inject into.
Jujutsu
uses gitoxide for its git interop and has no published CVEs in the argument-injection class; its two advisories to date are a path traversal and a missing SHA-1 collision check inherited from the library. go-git has
one
, CVE-2025-21613, and it’s specifically on the
file://
transport, the one code path in go-git that spawns the
git
binary.
I noted in the
package manager CWEs
post that this trades one problem for another, since a bundled git implementation has to track every checkout-safety fix upstream git ships, and libgit2 and JGit have both had rounds of those. That’s a real cost, but it’s a stream of specific patches to apply rather than a check that has to be remembered at every call site forever.
Writing this prompted me to
open a PR against Homebrew
raising its minimum git to 2.30.0 and adding
–end-of-options
before URLs in
clone
,
remote set-url
, and
ls-remote
, and before refs in
rev-parse
. The
checkout
and
reset
calls are left alone, since covering those would need a floor of 2.43.1, released February 2024, and that’s recent enough to still be ahead of several supported distributions.
Bundler, Cargo, CocoaPods, Composer, Conan, Go, Helm, Homebrew, Mix, Nix, npm, pip, pnpm, Poetry, Pub, SwiftPM, uv, vcpkg, Yarn. All checked at HEAD in July 2026.
↩