Git Security

`git --end-of-options`: The tiny flag that prevents “argument injection”

`git --end-of-options`: The tiny flag that prevents “argument injection”

Picture this: you’re reading a security fix for a package-manager CVE, hunting for the exact line where the vulnerability got stopped. Then you trip over a Git flag that feels like it came from an overconfident chatbot: --end-of-options.

It’s real. It’s documented in gitcli(7), and it exists because Git already uses the traditional -- marker for something else. Once you see why, the flag stops feeling weird and starts feeling like “the missing half” of safe command construction. (git-scm.com)

The two meanings of -- (and why that matters)

Most Unix command-line tools treat a bare -- as a delimiter: it tells the option parser, “stop interpreting the rest as flags.” That’s why you might see patterns like:

rm -- -f

Here, -f is treated as a literal filename, not as the rm “force” option.

Git, though, has a complication: in many commands it uses -- as part of its own internal syntax, separating revisions (things like branch names and commit IDs) from pathspecs (path arguments like README.md). So Git couldn’t adopt the standard “end of all option parsing” behavior for -- without breaking its own grammar.

That’s the core reason --end-of-options exists: Git needs a marker that ends option parsing without colliding with --’s revision-vs-path meaning in Git’s commands. (github.com)

So what does --end-of-options actually do?

In Git’s CLI conventions, --end-of-options is the explicit end-of-option-parsing marker.

A beginner-friendly way to think about it:
- Option parsing is the phase where a program decides which tokens are flags (things starting with -) and which tokens are positional arguments.
- --end-of-options instructs Git: “from here on, don’t reinterpret -anything as an option.”

Git even documents that --end-of-options is available for this purpose, and that it can work as an alias for -- for commands that don’t need the revision/path disambiguation. (git-scm.com)

Here’s the behavioral difference you start caring about in scripts:

The unsafe version (common in wrapper code)

Imagine a wrapper script accepts a revision string from somewhere untrusted (config, metadata, manifest, lockfile, user input), then runs:

git log "$rev"

What if $rev is --help or -n2 or --something-dangerous? Git may treat it as an option, not as a revision. The wrapper code “did everything right” (no shell injection, no eval), yet the subprocess still misinterprets the argument.

Why does a leading dash turn data into options? Because the program’s option parser makes that decision before it ever thinks about “what the token represents.”

The safer version (use the marker Git gives you)

git log --end-of-options "$rev"

Now Git is told that $rev is positional, even if it starts with -. (git-scm.com)

-- and --end-of-options are not interchangeable in Git

This is where many fixes (and plenty of real-world code) go off the rails.

In Git-land, -- has at least two jobs depending on the command:
1. Revision/path disambiguation: it separates the “ref-ish” part from the “path-ish” part.
2. End-of-options: for some commands, -- effectively plays that role too.

--end-of-options is about job #2 without risking job #1.

A robust pattern looks like this:

git log --end-of-options "$rev" -- "$path"
  • --end-of-options "$rev" prevents $rev from being parsed as options.
  • -- "$path" keeps $path on the “pathspec” side of Git’s command grammar.

Git’s own release notes around the feature explicitly call out that this avoids option injection, and they warn against assuming -- is doing the same job everywhere. (github.blog)

The security angle: argument injection without a shell

This is the part that tends to surprise teams.

A lot of developers associate command injection with shells. But argument injection is different. With argument injection:
- A program builds an argv array (an array of arguments),
- calls exec (or an equivalent) directly,
- and the called program interprets one of those arguments as a flag.

No shell is required. The vulnerability comes from the callee’s option parsing.

That’s exactly how CWE frames argument injection: improper neutralization of argument delimiters in a command. (cwe.mitre.org)

A canonical example is CVE-2019-13139 in docker build, where a crafted Git URL fragment ended up being treated as a git clone option. The payload didn’t need a shell to become dangerous; it only needed Git’s parsing rules. (nvd.nist.gov)

The same class of bug shows up anywhere package managers (or build tools, or CI helpers) take untrusted “data” and pass it into Git commands as raw argv elements.

Why Git didn’t fix everything at once

Because Git has multiple parsing styles across subcommands.

Git originally introduced --end-of-options in the revision parsing flow so scripts could safely pass revision names that start with dashes. The feature was described as a way to separate options from revisions without changing the already-meaningful -- grammar. (github.com)

But Git also has commands with their own bespoke argument parsing. That’s why support landed unevenly:
- git rev-parse picked up --end-of-options in the 2.30.0 timeframe, called out in release notes.
(fossies.org)
- For commands like git reset and git checkout, Git’s release notes later describe fixes to how --end-of-options interacts with the revision/path split (i.e., parsing behavior that previously failed in certain combinations). (sources.debian.org)

This uneven rollout is a practical lesson: relying on a single delimiter token can work for one command today and fail for another command tomorrow.

What “good wrapper code” looks like in practice

The recurring safe pattern is: treat untrusted values as positional arguments, and explicitly end option parsing at the boundary where the callee needs it.

In Git terms, that usually means:
- Use --end-of-options before revision-like arguments that might start with -.
- Use -- before pathspec-like arguments that must remain path-only.

When a tool constructs subprocess argv, that wrapper code becomes part of your security boundary. Even in a “no shell” world, Git’s option parser is still an attack surface.

You can see this mindset in the Go toolchain’s later hardening work for CVE-2025-68119: the cmd/go code updated how it invokes VCS tools, using safer syntax to separate flags from positional inputs, and it explicitly cites the problem of misinterpreting untrusted values. (github.com)

Takeaway: --end-of-options is not trivia

--end-of-options is one of those flags that feels optional until it isn’t.

  • -- is overloaded in Git because it already carries semantic meaning for revisions vs paths.
  • --end-of-options is the precise tool for ending option parsing without disturbing Git’s revision/path grammar.
  • Argument injection can happen even when you never invoke a shell, because the callee still performs parsing decisions.

Once you start building wrappers that call Git, git log --end-of-options "$rev" -- "$path" stops being a clever trick and becomes the mental model: “end parsing where the callee needs certainty.” (github.com)

ahsan

ahsan

Hello! I am Mr Ahsan, the writer of the Website. I am from Netherland. I like to write about technology and the news around it.

Comments (0)

No comments yet. Be the first to respond!

Leave a Comment

Your comment will be visible after review.