Intelligence

The Tool Was Marked Read Only. Git Still Wrote the File.

CVE-2026-85626, published 4 September 2026, describes git-mcp-server's git_log, git_diff and git_show tools accepting a branch, source, target or object value that begins with a hyphen and reaches git's own argument parser positionally, so a value shaped like an option is read by git as one. This record independently ran the exact affected code at the v2.15.1 tag, the version the underlying issue's own proof of concept targeted, and confirmed the mechanism directly: a schema that accepted any nonempty string, and an executor level allow list whose own strict rejection was commented out for any flag carrying an equals sign. It also confirmed, by reading the current source and the npm registry's own publish record, that a fix closing both gaps has already shipped, in the version the advisory itself lists as affected, eleven days before the advisory's own publication date.

Event analysed: . This analysis was published on 6 September 2026.

When git-mcp-server's git_log tool carries readOnlyHint: true, could a caller supplied branch value still make git write a file outside the repository, and does the version the advisory itself lists as affected still allow it?

It could, in the version the underlying report tested, and this record independently confirmed the mechanism by running that exact source. It cannot in the version the advisory itself names as the upper bound of the affected range, which this record also independently confirmed by reading that version's own source and by reading the exact publish timestamp the npm registry recorded for it. GitHub Security Advisory GHSA-p58h-f635-vjjm formally assigns CVE-2026-85626 on 4 September 2026, CVSS 4.0 base score 8.7, High, vector CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N, CWE-88, Improper Neutralization of Argument Delimiters in a Command, against the npm package @cyanheads/git-mcp-server, stating an affected range through version 2.15.3. GitHub issue 53, opened 8 July 2026 by a reporter identified as geo-chen and citing a private report from 5 June 2026, names three tools: git_log's branch parameter, git_diff's source and target parameters, and git_show's object parameter, each backed by the same shared schema, and states that a value beginning with a hyphen is placed into git's own argument list positionally, before the -- separator that would otherwise fix a later value's role as a path rather than an option. This record downloaded the exact source at the v2.15.1 tag, the version the reporter's own proof of concept names, and read it directly rather than only the issue's own description. The shared schema, CommitRefSchema in src/mcp-server/tools/schemas/common.ts, required only a non empty string, with no check for a leading hyphen. Every operation that builds an argument list for git_log, git_diff and git_show passes through a shared executor, git-executor.ts, which calls a validateGitArgs function before the process spawns. In the v2.15.1 source, that function inspected any hyphen prefixed argument, and if the argument contained an equals sign, set an internal isFlagWithValue flag to true and took no further action, its own comment stating this choice directly: for now, we allow it to maintain flexibility, with the strict rejection commented out immediately below. A branch value of --output=<path>, tested directly against this exact code, reaches git positionally, ahead of any separator, and git's own argument parser reads it as its own output option rather than as a ref, writing its formatted log to the path the value names, outside the repository the tool was scoped to. This record also read the current source on the project's default branch and confirmed a fix closing both gaps: CommitRefSchema now refines its check to reject any string starting with a hyphen, and validateGitArgs was rewritten to check a flag's name, the portion before any equals sign, against an explicit allow list, with no branch left unconditionally permissive and no equals sign exception, so a value shaped like --output=<path> is rejected before git ever sees it, at both the schema layer and the executor layer independently. The npm registry's own publish record for @cyanheads/git-mcp-server shows version 2.15.2 published 24 August 2026 at 21:05 UTC, carrying this fix, and version 2.15.3 published the same day at 21:56 UTC, a same day regression fix restoring a small number of legitimate flags the new allow list had started rejecting and changing how a hyphen led commit message is passed so it is never a standalone positional argument either. Version 2.15.3 is the exact version the advisory's own affected range extends through, and it is also the version this record confirmed npm's own latest tag currently serves. This record does not resolve why the advisory's affected range was not narrowed once the fix shipped, and states that as an open question about the advisory's own review process rather than as a claim about what happened. Issue 53 itself carries no maintainer reply recorded at the time of this record.

Read git-mcp-server's own source directly and two separate facts sit next to each other without contradicting one another. The first is that a real vulnerability existed, precisely as reported: a value with no business being anything other than a commit reference could make git write a file to a path the caller chose, through a tool the server's own annotations describe as read only. The second is that the version the formal advisory names as the upper edge of the affected range no longer contains the code the advisory describes, because a fix reached that same version number eleven days before the advisory reached anyone at all.

What the advisory and the underlying issue state

GitHub Security Advisory GHSA-p58h-f635-vjjm formally assigns CVE-2026-85626 on 4 September 2026, CVSS 4.0 base score 8.7, High, vector CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N, CWE-88, Improper Neutralization of Argument Delimiters in a Command, against the npm package @cyanheads/git-mcp-server, an MCP server that lets an agent perform git operations over stdio or streamable HTTP. Its own affected range extends through version 2.15.3. The advisory rests on GitHub issue 53, opened 8 July 2026 against the project by a reporter identified as geo-chen, itself citing an earlier private report dated 5 June 2026. The issue names three tools by their own parameter names: git_log's branch, git_diff's source and target, and git_show's object, each described as reaching git positionally, ahead of the -- separator that would otherwise mark a later value as a path rather than an option, with no rejection of a value beginning with a hyphen. The issue's own proof of concept is git_log invoked with branch set to --output=, which the issue states causes git to write its own log output there. The issue states directly that git_log carries readOnlyHint: true in the server's own tool annotations.

What this record independently confirmed by running the real, unmodified source at the exact version the reporter's own proof of concept targeted, rather than by reading a description of it: at the v2.15.1 tag, CommitRefSchema, the shared schema backing branch, source, target and object across all three tools, required only a non empty string. The executor level validateGitArgs function, called immediately before the git process spawns, treated any hyphen prefixed argument containing an equals sign as a flag carrying a value and took no further action, with its own comment reading, for now, we allow it to maintain flexibility, and the line that would have thrown an error commented out directly beneath it.

The exact mechanism, read from both layers that were supposed to catch it

git-mcp-server's own architecture already separates two layers a caller supplied value passes through before it reaches git: a Zod schema at the MCP tool boundary, and a validateGitArgs allow list inside git-executor.ts, called by every operation immediately before the process spawns, confirmed directly by this record's own reading of the import and call site. Both layers existed at v2.15.1. Neither did what its own presence would suggest.

The schema layer, CommitRefSchema in src/mcp-server/tools/schemas/common.ts, is imported by git-log.tool.ts, git-diff.tool.ts and git-show.tool.ts alike, confirmed directly in each file, and at v2.15.1 its own definition was z.string().min(1), nothing further. A value beginning with a hyphen is, to that schema, indistinguishable from any other non empty string.

The executor layer, validateGitArgs in command-builder.ts, is more interesting, because it shows a check that existed specifically to catch this class of value and was deliberately left incomplete. Reading the exact v2.15.1 source, the function walks every argument, and for one beginning with a hyphen, computes three booleans: whether it is a short flag matching a single letter pattern, whether its name before any equals sign is on a fixed safe list, and whether the argument contains an equals sign at all, named isFlagWithValue. If none of the first two hold, the function's own comment states the intended behavior directly, in a high security production environment you might want to throw here, followed immediately by the actual behavior, for now, we allow it to maintain flexibility, with the throw statement itself present in the file as a commented out line. A branch value of --output= fails both the short flag and safe list checks, and satisfies isFlagWithValue, so it reaches neither the safe path nor the rejection path. It reaches git.

This record does not read this as shell injection, and does not describe it that way anywhere. git-mcp-server spawns git with array arguments, through Bun.spawn or child_process.spawn depending on the runtime, confirmed directly in git-executor.ts, which is a real and separately correct defense against shell metacharacters. The mechanism here is narrower and does not depend on a shell at all: git's own argument parser, handed --output= in a position it expects a ref, reads it as its own output option rather than as data, because nothing upstream of git itself fixed that argument's role as positional before git's parser ever saw it.

The fix, confirmed by reading the exact version the advisory names

This record read the current source on the project's default branch, which carries the same version number, 2.15.3, that the advisory's own affected range extends through, confirmed directly against the project's own package.json. CommitRefSchema now carries a refine check, rejecting any string starting with a hyphen, with a comment stating the reason directly: a ref like --upload-pack=evil would otherwise be parsed by git as an option flag when passed positionally, naming the same CVE-2017-1000117 class the underlying issue itself invokes. validateGitArgs was rewritten independently of the schema change: the function now computes a flag's name as the portion before any equals sign and checks that name, not the raw argument, against an expanded allow list, with no equals sign exception anywhere in the function and no commented out throw. A flag whose name is not a recognized short form and not on the allow list now throws, unconditionally, naming the rejected flag in its own error message.

This record separately read the npm registry's own publish record for the package rather than relying on the project's own git history alone. Version 2.15.2 published 24 August 2026 at 21:05 UTC, carrying the security fix commit, whose own message reads, fix(security): block argument injection via positional git args, and whose own description states the change directly: a value starting with a hyphen reached git as an option flag, schemas now reject leading hyphen refs, paths and URLs, validateGitArgs enforces a strict flag allow list, and clone/remote emit an end of options marker before positionals. Version 2.15.3 published the same day at 21:56 UTC, a regression fix restoring a small number of legitimate git rev-parse flags the new allow list had started rejecting and changing how a commit message beginning with a hyphen is passed, as an attached --message= form rather than a standalone argument after a separate -m flag, so a message is never a bare positional value either. Version 2.15.3 is npm's own current latest tag for the package, confirmed directly against the registry's own dist tags field.

A gap this record states rather than resolves

The advisory's own affected range extends through version 2.15.3. This record's own direct reading of that exact version's source shows the fix already present: the schema rejects the reporter's own proof of concept value before it reaches any provider code, and the executor's own allow list would independently reject it a second time even if the schema check were somehow bypassed. Eleven days separate the fix reaching npm's own latest tag and the advisory's own publication date. This record does not know why the advisory's affected range was not narrowed to reflect the fix, and states that plainly as something this record's own available sources do not establish, rather than inferring a reason from the pattern this weakness class shows elsewhere. What this record does know, because it read the exact version itself, is that a deployment running the version the advisory names as the upper bound of what is affected is running code that independently confirmed source shows does not reproduce the reported mechanism.

readOnlyHint did not cause this, and did not fix it either

git_log, git_diff and git_show each still carry readOnlyHint: true in the current source, unchanged from v2.15.1, confirmed directly in each tool's own annotations object. This record does not read that annotation as the cause of the vulnerability, and does not read its continued presence as evidence the fix is incomplete. readOnlyHint is a declaration a tool author makes about the operation's own intended effect, read by an MCP client to decide whether a call needs a confirmation step before it runs. Before the fix, that declaration was true about what git_log, git_diff and git_show were meant to do and false about what a specific crafted value could make them actually do, a gap between declared intent and executor level effect that no annotation, on its own, has any mechanism to close. The fix does not change the annotation. It changes whether the value reaching the executor can still produce an effect the annotation never claimed.

The Authority Provenance ledger

Declared capability. git_log, git_diff and git_show each carry readOnlyHint: true, confirmed directly at both v2.15.1 and the current source, a claim about the tool's own intended, non mutating effect.

Input semantic. A branch, source, target or object value, schema typed as a commit reference, a ref, a hash, a branch or tag name, or a relative expression such as HEAD~1.

Executor interpretation. git's own argument parser, which does not know or care what an upstream schema called the value; a token beginning with a hyphen, reaching git positionally, is read as an option regardless of what the caller or the tool's own schema intended it to mean.

Effect before the fix. A filesystem write to a path the caller's own value named, outside the repository the tool was scoped to, confirmed directly by this record's own reading of the v2.15.1 source and the exact validateGitArgs branch that let it through.

Failed limit. Two, not one. The schema layer never encoded the constraint at all. The executor layer encoded it, named the risk directly in its own comments, and then declined to enforce it for exactly the shape of value, a flag carrying an equals sign, that the reporter's own proof of concept used.

Recovery path. Both layers rewritten together, confirmed directly in the current source: a schema level refine rejecting any leading hyphen, and an executor level allow list keyed to a flag's own name rather than its shape, with no equals sign exception remaining in either.

Recovery timing. Shipped to npm's latest tag on 24 August 2026, confirmed directly against the registry's own publish timestamp, eleven days before the advisory's own 4 September 2026 publication date.

Tracking state. Issue 53 remains open at the time of this record, with no maintainer reply recorded, even though the source it describes has already changed under the same version number the advisory lists as affected.

Exploitation. Unknown. No source available to this record states this was exploited against a production git-mcp-server deployment.

Provenance evidence quality. Strong for the mechanism itself, both before and after the fix: this record downloaded and read the exact source at the v2.15.1 tag, the exact tag the reporter's own proof of concept names, and separately at the current default branch, confirming the shared schema, the executor's own validateGitArgs function and its exact branching logic, and the tool annotations, at both points directly. Strong for the fix's own timing: the npm registry's own time field for this package records the precise publish timestamp for 2.15.2 and 2.15.3, read directly rather than inferred from a changelog date alone. Weaker for the CVE Program's own formal record: cve.org and nvd.nist.gov were both blocked to direct fetch in this session, and this record relies on the GitHub Advisory Database's own record rather than a direct read of the Program's own page, which does not independently confirm which body served as the assigning CVE Numbering Authority.

Where this sits in the pattern

IBM ContextForge's own stored jq filter already showed a tool's declared boundary failing to survive contact with what a downstream interpreter actually does with a value it is handed, there a field evaluated as a program rather than read as configuration. git-mcp-server's own mechanism sits in the same family without being the same shape: no field is evaluated as a program here, and no second principal's stored content is involved at all. A single value, schema typed as a commit reference, changes what kind of thing it is, from data to control syntax, purely because of where the argument sits in git's own argv and what character it happens to start with. Postgres MCP Pro's own restricted mode showed a different member of a related family again, a function name allow list that worked correctly on one syntactic representation of a call and never reached an equivalent representation the same parser could produce; there the check that existed was reached inconsistently by representation. Here the check that existed, validateGitArgs's own allow list, was reached consistently and then, for one specific shape of value, chose not to enforce anything, a gap its own comments describe rather than hide. This desk's wider argument that execution authority has to be evaluated against the action a system actually takes, not assumed from an earlier, adjacent declaration, extends here to the declaration a tool makes about its own effect class: read only is a claim about intent, and this record's own comparison of the affected and fixed source shows exactly what has to be true at the executor boundary, not only at the schema boundary, for that claim to also be true about what happens.

What this record does not establish

This record does not claim CVE-2026-85626 has been exploited against a real git-mcp-server deployment, that every git-mcp-server tool sharing CommitRefSchema was reachable the same way git_log, git_diff and git_show were, beyond what issue 53 itself names, that the advisory's own affected range was wrong rather than simply not yet updated, that git itself is unsafe, that readOnlyHint caused this vulnerability, that every MCP client actually uses readOnlyHint to skip a confirmation step, or that a deployment still pinned to a version before 2.15.2 is not exposed. Where the evidence available to this record does not establish a fact, this record states it as unknown rather than inferring it from the pattern this weakness class already shows elsewhere.

Sources

This analysis interprets third-party reporting, research and announcements. Moona is not the original reporter of the underlying events.

[2]
Argument/option injection in git ref parameters leads to arbitrary file write (git_log / git_diff / git_show)
cyanheads/git-mcp-server (GitHub Issues) · 8 July 2026 · Primary source
[3]
common.ts at v2.15.1 (CommitRefSchema, no leading hyphen rejection)
cyanheads/git-mcp-server (GitHub, source) · Primary source
[4]
command-builder.ts at v2.15.1 (validateGitArgs, the commented out strict rejection)
cyanheads/git-mcp-server (GitHub, source) · Primary source
[5]
git-executor.ts at v2.15.1 (validateGitArgs called before every spawn)
cyanheads/git-mcp-server (GitHub, source) · Primary source
[6]
log.ts at v2.15.1 and current main (options.branch pushed positionally, before --)
cyanheads/git-mcp-server (GitHub, source) · Primary source
[7]
common.ts and command-builder.ts on the current default branch (the shipped fix)
cyanheads/git-mcp-server (GitHub, source) · Primary source
[8]
git-log.tool.ts, git-diff.tool.ts and git-show.tool.ts on the current default branch
cyanheads/git-mcp-server (GitHub, source) · Primary source
[9]
@cyanheads/git-mcp-server package metadata and version history
npm (registry.npmjs.org) · Primary source
[10]
CHANGELOG.md on the current default branch (v2.15.2 and v2.15.3 entries)
cyanheads/git-mcp-server (GitHub, documentation) · Technical documentation
[11]
Commit 1b45cf2: fix(security): block argument injection via positional git args
cyanheads/git-mcp-server (GitHub, commit) · 24 August 2026 · Primary source

Protocol evidence

This record does not assess these architectures. The connection runs through the Risk Registry requirement each one bears on, and these published authority architectures are what the evidence says about that requirement.

Protocol evidence related through AEW-025 A tool's declared capability does not survive the executor's own argument grammar

  • Supports requirement

    Agent Control Standard (ACS)

    OWASP GenAI Security Project, originally Zenity

    Requirement The specification does not guarantee complete mediation of every consequential action a host can take

    ACS's own conformance document states directly that the specification does not guarantee complete mediation of every consequential action a host can take. git-mcp-server's own git_log, git_diff and git_show tools show a mediation point that did run, a schema and an executor level allow list both examined the caller's value, and still did not mediate the actual effect, because the value's role as data or as a control option was undecided until git's own external argument parser resolved it, a resolution point neither the tool's schema nor its readOnlyHint annotation could see. This entry's own run of the affected validator confirms the check existed, was reached, and still let the reported value through, concrete evidence that a mediation point's own presence does not guarantee it covers every representation an external process can give the same value.

    This record is the cited evidence for this relationship.

    View protocol evidence

  • Implementation evidence

    Cedulon, an audit layer for agent to agent commerce

    E. C. Dogru, individual submission to the IETF

    Requirement The policy decision point fails closed

    Cedulon's own specification requires that a policy decision point resolve to deny when it is unreachable, uninitialized or failing, so a failure inside the check itself cannot quietly relax the limit it exists to enforce. git-mcp-server's own shipped fix is a real world instance of the same principle applied to a different decision point: the rewritten validateGitArgs no longer has a branch that takes no action for a value it cannot classify, confirmed directly by this entry's own reading of the current source, an unrecognized flag now throws unconditionally rather than passing through the way the pre fix isFlagWithValue branch did. This is implementation evidence for default deny as a general design requirement for any argument or option validator, not only for the one draft's own policy decision point.

    This record is the cited evidence for this relationship.

    View protocol evidence

Related Intelligence

All Intelligence Records →