{"slug": "git-end-of-options", "title": "git --end-of-options", "summary": "Git's --end-of-options flag, introduced in version 2.24.0 in November 2019, provides a way to separate options from revisions in commands like git log, addressing argument injection vulnerabilities (CWE-88) that arise when untrusted strings are passed as arguments. The flag is distinct from --, which git repurposed to separate revisions from pathspecs, and support for it was added to subcommands incrementally, with git rev-parse receiving it in 2.30.0 and git checkout and git reset in 2.43.1 in February 2024.", "body_md": "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`\n\n. My first reaction was that some LLM had hallucinated it, but it’s [documented in gitcli(7)](https://git-scm.com/docs/gitcli), it was added in git 2.24.0 in November 2019, and it exists because git had already used\n\n`--`\n\nfor something else.In most Unix tools `--`\n\nmarks the end of option parsing, so `rm -- -f`\n\nremoves a file called `-f`\n\nrather than passing the force flag. Git had repurposed `--`\n\nearly on to separate revisions from pathspecs, because `git log foo`\n\non its own is ambiguous between a branch named `foo`\n\nand a file named `foo`\n\nand one of the two readings needed a marker: `git log main -- README.md`\n\nmeans commits on `main`\n\ntouching that file. That left the revision position with no terminator, so if a script runs `git log \"$rev\"`\n\nand `$rev`\n\nstarts with a dash, git parses it as an option.\n\nFrom the [commit that introduced](https://github.com/git/git/commit/19e8789b236dfe33667747d5523d6689bb59b5ef) `--end-of-options`\n\n:\n\nBut that doesn’t work for the revision parser, because\n\n`--`\n\nis already meaningful there: it separates revisions from pathspecs. So we need some other marker to separate options from revisions.\n\n`--`\n\nand `--end-of-options`\n\nare different things in git, and treating them as interchangeable is a mistake I’ve now seen in several places. Putting `--`\n\nbefore a URL in `git clone -- \"$url\"`\n\nworks, because `clone`\n\nfollows the POSIX convention. A trailing `--`\n\nafter a ref, as in `git checkout \"$ref\" --`\n\n, marks `$ref`\n\nas 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\"`\n\n, with both markers doing separate jobs.\n\nSupport for the new flag arrived per subcommand rather than all at once: `git rev-parse`\n\n[only got it in 2.30.0](https://github.com/git/git/commit/3a1f91cfd9), a year after the initial release, because it has its own hand-rolled argument parser, and `git checkout`\n\nand `git reset`\n\n[rejected it until 2.43.1](https://github.com/git/git/commit/9385174627) in February 2024 because they parse `--`\n\nthemselves and the initial implementation left `--end-of-options`\n\nin the argument list where their parsers rejected it.\n\n## Argument injection\n\nGit, hg, and ssh all ship options whose documented purpose is to run a command the caller names. `git clone`\n\naccepts `--upload-pack=<cmd>`\n\nto specify the server-side binary, and any `git`\n\ninvocation accepts `-c core.sshCommand=<cmd>`\n\nto override how it connects. Mercurial accepts `--config=alias.<subcmd>=!<shell>`\n\non any subcommand, which redefines the subcommand you’re running as an arbitrary shell script. `ssh`\n\naccepts `-oProxyCommand=<cmd>`\n\n. These are documented features that become attack primitives when a wrapping program passes an untrusted string into the argument list.\n\nThe failure mode has its own CWE, [CWE-88](https://cwe.mitre.org/data/definitions/88.html), 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`\n\ndirectly, exactly as every “don’t use `system()`\n\n” 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](https://staaldraad.github.io/post/2019-07-16-cve-2019-13139-docker-build/) in `docker build`\n\nis a clean example: Go’s `os/exec`\n\npackage, an argv array, no shell, and a git-context URL whose `#ref:dir`\n\nfragment reached `git fetch origin <ref>`\n\nas `--upload-pack=<cmd>`\n\n.\n\nThe pattern was demonstrated across four version control systems on the same day in August 2017, when [CVE-2017-1000117](https://nvd.nist.gov/vuln/detail/CVE-2017-1000117) (git), [CVE-2017-1000116](https://nvd.nist.gov/vuln/detail/CVE-2017-1000116) (Mercurial), [CVE-2017-9800](https://subversion.apache.org/security/CVE-2017-9800-advisory.txt) (Subversion), and [CVE-2017-12836](https://nvd.nist.gov/vuln/detail/CVE-2017-12836) (CVS) were disclosed together. Each passed a URL’s hostname to `ssh`\n\nas an argument, and a hostname starting with `-oProxyCommand=`\n\nbecame an ssh option. Phabricator’s [post-mortem](https://web.archive.org/web/20251216145944/https://secure.phabricator.com/T12961) on the disclosure noted that of the three actively maintained tools, only Subversion actually added `--`\n\nbefore the hostname in its fix; git and Mercurial validated the hostname format instead, partly because `--`\n\nisn’t supported by every ssh implementation. The same write-up called the `--`\n\nmechanism itself “unsafe by default”, since code without it looks correct and works fine right up until an argument starts with a dash.\n\n## Package managers\n\nPackage managers routinely take a git URL or ref as data and pass it to a subprocess: `gem 'foo', git: '...'`\n\nin a Gemfile, `github:user/repo#ref`\n\nin a `package.json`\n\n, and equivalents in `pyproject.toml`\n\n, `Cargo.toml`\n\n, `mix.exs`\n\n, `Package.swift`\n\n, `pubspec.yaml`\n\n, `conanfile.py`\n\n, and `go.mod`\n\n. The URL and ref arrive in a manifest, a lockfile, or a transitive dependency’s metadata.\n\nOf nineteen package managers I checked 1, seventeen fork the\n\n`git`\n\nbinary as their default or only path. The two that default to a library are Cargo, which uses [libgit2](https://libgit2.org/)with an opt-in\n\n`net.git-fetch-with-cli`\n\nsetting to fork instead, and Poetry, which switched to [dulwich](https://www.dulwich.io/)in\n\n[1.2.0](https://github.com/python-poetry/poetry/commit/ad1b0938)with a\n\n`system-git-client`\n\nsetting to fall back. Nix uses libgit2 for reading local repositories but forks `git`\n\nfor fetches, because [libgit2 lacks git-credential helper support](https://github.com/NixOS/nix/blob/3aff4dc5edf30998d64eec024de186ac2d6fb5ea/src/libfetchers/git-utils.cc#L639-L641).\n\nThe published CVEs against package managers in this class include [CVE-2021-43809](https://github.com/advisories/GHSA-fj7f-vq84-fh43) (Bundler), [CVE-2021-29472](https://github.com/composer/composer/security/advisories/GHSA-h5h8-pc6h-jvvx) and [CVE-2022-24828](https://github.com/composer/composer/security/advisories/GHSA-x7cr-6qr6-2hh6) (Composer), [CVE-2022-36069](https://github.com/advisories/GHSA-9xgj-fcgf-x6mw) (Poetry), [CVE-2023-5752](https://github.com/advisories/GHSA-mq26-g339-26xf) (pip), [CVE-2022-21223](https://github.com/advisories/GHSA-g397-v4w5-4m79) and [CVE-2022-24440](https://github.com/advisories/GHSA-7627-mp87-jf6q) (CocoaPods), and [CVE-2025-68119](https://pkg.go.dev/vuln/GO-2026-4338) (Go). The Snyk research that produced several of the 2022 entries is [written up here](https://snyk.io/blog/argument-injection-when-using-git-and-mercurial/), and Sonar maintains a [catalogue of the dangerous options](https://sonarsource.github.io/argument-injection-vectors/) per binary.\n\nOf the seventeen that fork git, exactly one uses `--end-of-options`\n\n: Go’s `cmd/go`\n\n. It [added --](https://github.com/golang/go/commit/55d31e16c1) before repository URLs in June 2019 as a general hardening pass. In January 2026 that turned out to be insufficient and\n\n`--end-of-options`\n\nwas [added across the board](https://github.com/golang/go/commit/94a1296a457387d1fd6eca1a9bcd44e89bdd9d55)as the fix for CVE-2025-68119, along with\n\n`HGPLAIN=+strictflags`\n\n, which has [restricted Mercurial’s early-option parsing since hg 4.4.2](https://wiki.mercurial-scm.org/WhatsNew/Archive#Mercurial_4.4.2_.282017-12-01.29)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.”\n\n## Minimum git versions\n\nThe other package managers that guard the argument list at all use `--`\n\nor 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 `--`\n\nbefore the URL in Bundler’s `git clone`\n\nis [the CVE-2021-43809 patch](https://github.com/ruby/rubygems/commit/90b1ed8b9f). The leading-dash rejection in cocoapods-downloader [landed in three commits](https://github.com/CocoaPods/cocoapods-downloader/commit/35340f4b) over ten days in March 2022, matching the CVE-2022-21223 disclosure. Poetry’s guard [arrived in September 2021](https://github.com/python-poetry/poetry-core/commit/cc84be6) with a CVE assigned a year later, and the switch to dulwich followed six months after that. vcpkg is the exception I found where `--`\n\nwas [present from the day](https://github.com/microsoft/vcpkg-tool/commit/8cde8a06) git registry support was written.\n\nComposer’s [advisory for CVE-2022-24828](https://blog.packagist.com/cve-2022-24828-composer-command-injection-vulnerability/) explains why almost none of these tools use `--end-of-options`\n\n: 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`\n\nis 2.7.0 on Linux, [set in 2018](https://github.com/Homebrew/brew/commit/7aa9956934).\n\nAmazon 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`\n\non `git fetch`\n\nbut old enough to reject it on `git rev-parse`\n\n. Relying on the flag means raising the minimum git to 2.24.0 for most subcommands, 2.30.0 for `rev-parse`\n\n, or 2.43.1 for `checkout`\n\nand `reset`\n\n, and losing anyone still on the distribution-packaged git.\n\n## Git libraries\n\nlibgit2, [gitoxide](https://github.com/GitoxideLabs/gitoxide), [go-git](https://github.com/go-git/go-git), [JGit](https://github.com/eclipse-jgit/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](https://github.com/jj-vcs/jj) 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](https://github.com/go-git/go-git/security/advisories/GHSA-v725-9546-7q7m), CVE-2025-21613, and it’s specifically on the `file://`\n\ntransport, the one code path in go-git that spawns the `git`\n\nbinary.\n\nI noted in the [package manager CWEs](/2026/05/04/package-manager-cwes.html) 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.\n\nWriting this prompted me to [open a PR against Homebrew](https://github.com/Homebrew/brew/pull/23223) raising its minimum git to 2.30.0 and adding `--end-of-options`\n\nbefore URLs in `clone`\n\n, `remote set-url`\n\n, and `ls-remote`\n\n, and before refs in `rev-parse`\n\n. The `checkout`\n\nand `reset`\n\ncalls 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.\n\n-\nBundler, 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.\n\n[↩](#fnref:survey)", "url": "https://wpnews.pro/news/git-end-of-options", "canonical_source": "https://nesbitt.io/2026/07/21/end-of-options.html", "published_at": "2026-07-21 12:31:15+00:00", "updated_at": "2026-07-21 13:15:35.778764+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["git", "git 2.24.0", "git 2.30.0", "git 2.43.1", "CWE-88", "CVE-2019-13139", "CVE-2017-1000117", "CVE-2017-1000116"], "alternates": {"html": "https://wpnews.pro/news/git-end-of-options", "markdown": "https://wpnews.pro/news/git-end-of-options.md", "text": "https://wpnews.pro/news/git-end-of-options.txt", "jsonld": "https://wpnews.pro/news/git-end-of-options.jsonld"}}