# Claude Code checked out a plugin's pinned 40-character SHA but never compared HEAD to it, so a branch named like the SHA won

> Source: <https://dev.to/rulestack/claude-code-checked-out-a-plugins-pinned-40-character-sha-but-never-compared-head-to-it-so-a-2cen>
> Published: 2026-11-14 02:17:00+00:00

Pinning a dependency to a full commit SHA is supposed to mean this exact code, as reviewed. An October advisory for Claude Code (CVE-2026-86063, fixed in 2.1.179) showed a plugin install that ran `git checkout` on the pinned SHA and never checked where HEAD ended up, so a branch whose name was that same 40-character string could take its place. The fix is one comparison, and any script of yours that pins and checks out commits needs the same one.

The advisory is [GHSA-pq7j-f95f-qfcv, Plugin Commit SHA Pin Bypass Allows Installation of Attacker-Controlled Code](https://github.com/anthropics/claude-code/security/advisories/GHSA-pq7j-f95f-qfcv), rated High (CVSS 7.7). Affected versions are Claude Code before 2.1.179, and 2.1.179 is the patched version.

The description is two sentences long and worth reading in full. "The plugin install flow in Claude Code and Claude Desktop checked out a marketplace's pinned commit SHA using git checkout without verifying that the resulting HEAD matched the pinned SHA. An attacker controlling a plugin's upstream repository could create a branch named identically to the pinned 40-character SHA, causing the checkout to silently resolve to the attacker's branch tip instead of the reviewed commit, resulting in attacker-controlled code being installed bypassing the pinned hash."

The conditions are narrow, and the advisory lists all three: "Exploitation required a user to install a malicious plugin, pin to a safe hash, and for the plugin source to be hosted on a git host that permitted SHA-shaped branch names." It also says "Users on standard Claude Code auto-update have received this fix already." The reporter was Or Nevo Michrowski of Air Security.

The pin is written by whoever publishes a plugin marketplace, not by the person installing. Claude Code's [marketplace reference](https://code.claude.com/docs/en/plugins/marketplace-reference) describes plugin sources of type `github`, `url` and `git-subdir`, which share two fields: `ref` and `sha`. The `sha` field is "a full 40-character lowercase commit SHA", and when you set both `ref` and `sha`, the reference says Claude Code checks out the `sha`.

The reason to set `sha` is review. A marketplace maintainer reads a plugin at one commit and pins that commit, so that what users install is what was read, even if the plugin's author later pushes something else. The advisory's precondition, "pin to a safe hash", is exactly that workflow, and the attack is aimed at the moment the pin is supposed to pay off: the upstream repository is controlled by someone who wants to ship different code.

Git's own [documentation for `git checkout`](https://git-scm.com/docs/git-checkout) explains how one string can mean two things. For the `<branch>` argument it says: "if it refers to a branch (i.e., a name that, when prepended with "refs/heads/", is a valid ref), then that branch is checked out. Otherwise, if it refers to a valid commit, your HEAD becomes "detached"".

A 40-character hexadecimal string is normally a commit name. On a host that permits it, as the advisory puts it, a branch can carry that same string as its name. When both exist, `git checkout <string>` has a rule for which one it means, and the rule puts the branch first. The advisory does not walk through which step of the install flow hit this, and we will not guess. What it does say is the important part: the checkout "silently" resolved to the branch tip, and nothing afterwards noticed.

One tempting fix does not help. The same page describes `git checkout --detach [<branch>]` as "The same as git checkout , except that instead of pointing HEAD at the branch, it points HEAD at the commit ID." With `--detach`, a name that refers to a branch still lands on that branch's tip; HEAD is just detached there. Changing the flags on the checkout is not the fix. Checking the result is.

The patch is described as a verification that "the resulting HEAD matched the pinned SHA". The same check works anywhere you pin and check out commits yourself, in an install script, a CI step that vendors a tool, or a script that a coding agent wrote to fetch a dependency at a known commit:

```
pin="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
git checkout --quiet "$pin"
actual="$(git rev-parse HEAD)"
if [ "$actual" != "$pin" ]; then
  echo "checkout landed on $actual, expected $pin" >&2
  exit 1
fi
```

`git rev-parse HEAD` prints the full commit name of whatever is checked out, so the comparison does not depend on how the checkout interpreted its argument. If any rule, default or remote branch led somewhere else, the script stops before the code is used.

Two more habits make the comparison worth having:

`sha`, and your own scripts should do the same, so the comparison is exact.`claude --version` should report 2.1.179 or later. The advisory says standard auto-update delivered the fix; installations that do not auto-update, such as Homebrew and WinGet according to the `ref` gives it nothing.
It is easy to think of a 40-character SHA as unforgeable, and the commit it names is. What the advisory shows is that the string has to be interpreted by a command before it becomes a commit, and the command had more than one way to read it. The protection comes from asking Git what you actually got, after the fact.

Do you pin plugins, actions or tools to full SHAs, and does anything check that the checkout landed where the pin says? If you have found another place where a pinned name was interpreted as something else, share it in the comments below.
