[BUG] sparsePaths git marketplaces clone empty since 2.1.274 (GIT_NO_LAZY_FETCH=1 on a blob:none clone; git ≤2.34 exits 0) and silently replace healthy clones — 2.1.278 retry never fires

Status Fixed / completed
Reported on v2.1.274
Maintainer reply None cached
Activity 0 comments · opened Sep 21, 2026 · closed Sep 25, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Since 2.1.274, every git marketplace declared with sparsePaths is cloned empty, and the refresh loop then destroys existing healthy clones by swapping the empty staging clone into place before validating it. All plugins/skills shipped through such a marketplace disappear for every developer whose client auto-updates. 2.1.273 is fine.

Root cause: since 2.1.274 the post-clone git sparse-checkout set / git checkout HEAD run under the hardened git environment with GIT_NO_LAZY_FETCH=1 (and GIT_ALLOW_PROTOCOL=none). The clone was made with --filter=blob:none --no-checkout, so the checkout must lazy-fetch blobs to write any file. With lazy fetch disabled it writes nothing:

warning: lazy fetching disabled; some objects may not be available
error: unable to read sha1 file of .claude-plugin/marketplace.json (<sha>)
error: unable to read sha1 file of .claude/… (…)

The trap: git 2.34.1 (Ubuntu 22.04 default) exits 0 here. The index lists 97 files, the working tree has 0. Claude Code trusts the exit code, moves the empty clone into ~/.claude/plugins/marketplaces/<name>, and only then fails validation:

✘ Failed to update marketplace(s): Failed to refresh marketplace 'my-marketplace':
  The marketplace.json file is no longer present in this repository.
  This marketplace may have been deprecated or moved to a new location.

The file is present in the repository; only the local clone is empty.

Why it is destructive, not just a failed refresh

  1. Healthy clones are destroyed. On marketplace update the existing (good) clone is renamed aside, the empty staging clone is renamed into place, the backup is deleted, and only then marketplace.json is validated. Verified on 2.1.278 with a clean CLAUDE_CONFIG_DIR: a healthy clone (98 files) that was 5 commits behind → after claude plugin marketplace update: 0 files, marketplace.json gone, error above. This is the second half of the 2.1.275 fix "Fixed claude plugin marketplace update deleting a GitHub marketplace's local copy when the fetch failed" — validation still happens after the swap.
  2. It runs on its own. The re-clone happens at session start and in the background refresh, and retries in a loop (18 clones of 4 marketplaces in 3 minutes observed). Nothing the user does triggers it.
  3. Cross-client damage. The VS Code extension auto-updated to 2.1.274 while the CLI was still 2.1.273. The 2.1.274 sessions kept overwriting the correct clones the 2.1.273 CLI had just made, so even users who never updated the CLI lost their plugins.
  4. No knob to turn it off. No setting, env var or feature flag in 2.1.274–2.1.278 disables the hardened env for marketplace clones. CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE does not apply — the clone is reported successful.

2.1.278's retry does not fix it

2.1.278 added a retry ("<dir> is a partial clone: retrying with a constrained lazy fetch", GIT_NO_LAZY_FETCH=0) that is clearly aimed at this bug — but it only fires when the first git command exits non-zero. With git 2.34.1, checkout HEAD exits 0 with an empty tree, so the retry never runs. Re-tested on 2.1.278, clean config dir:

| action on 2.1.278 | result |
|---|---|
| claude plugin marketplace add <url> --sparse .claude-plugin .claude | empty clone, "marketplace.json … no longer present", clone discarded |
| claude plugin marketplace update of a healthy clone 5 commits behind | healthy clone replaced by an empty one, same error |

What Should Happen?

A sparsePaths marketplace clones and refreshes exactly as in 2.1.273 — or at minimum the failure is detected before the existing clone is touched: validate the staging clone, keep the previous one on failure.

Error Messages/Logs

Steps to Reproduce

Marketplace declaration (large monorepo on self-hosted GitLab over SSH; sparsePaths exists precisely because a full clone takes ~1 min vs ~5 s sparse):

{
  "extraKnownMarketplaces": {
    "my-marketplace": {
      "source": {
        "source": "git",
        "url": "git@git.example.com:group/R.git",
        "ref": "master",
        "sparsePaths": [".claude-plugin", ".claude"]
      },
      "autoUpdate": true
    }
  },
  "enabledPlugins": { "my-plugin@my-marketplace": true }
}

What Claude Code runs for a sparsePaths marketplace (from the host's exec log), in a staging directory:

git clone --depth 1 --filter=blob:none --no-checkout --branch master -- <url> <staging>
git sparse-checkout set --cone -- .claude-plugin .claude
git checkout HEAD

Up to 2.1.273 the two post-clone commands ran with the normal environment and a lazy git fetch … --filter=blob:none --stdin is visible right after checkout. Since 2.1.274 they run with GIT_NO_LAZY_FETCH=1.

Minimal reproduction (no Claude Code needed)

git clone --depth 1 --filter=blob:none --no-checkout --branch master <any-repo> t
cd t && git sparse-checkout set --cone -- <dir1> <dir2>

GIT_NO_LAZY_FETCH=1 git checkout HEAD; echo "exit=$?"
find . -path ./.git -prune -o -type f -print | wc -l   # 0 files
git ls-files -t | grep -c '^H'                          # N files expected on disk

| env for git checkout HEAD | exit | files on disk | index entries |
|---|---|---|---|
| default | 0 | 97 | 97 |
| GIT_NO_LAZY_FETCH=1 | 0 | 0 | 97 |

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.273

Claude Code Version

2.1.278

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

VS Code integrated terminal

Additional Information

Suggested Fixes

  1. Do not set GIT_NO_LAZY_FETCH=1 for the checkout of a clone Claude Code itself made with --filter=blob:none — or fetch the needed blobs explicitly (git fetch --filter=blob:none --stdin), or run git checkout under the constrained lazy-fetch env from the start rather than only on retry.
  2. Treat "exit 0 but the sparse paths are empty / marketplace.json missing in the staging clone" as a failure — don't rely on git's exit code for partial clones (same lesson as #66579's {code:0} on a missing directory).
  3. Validate marketplace.json in the staging directory before swapping it in and deleting the previous clone; on failure keep the existing clone.

Workarounds we found

  • Org-wide requiredMaximumVersion: "2.1.273" in managed settings + VS Code extension rolled back to 2.1.273 with auto-update off. Current state; blocks the whole org from every newer release.
  • Repairing a broken cache by hand: git -C ~/.claude/plugins/marketplaces/<name> restore -- . — works until the next 2.1.274+ session re-clones.
  • Dropping sparsePaths (full clone, no lazy fetch needed) — works, but costs a full clone of a large monorepo for every developer, which is exactly what sparsePaths exists to avoid.

Environment

  • Claude Code 2.1.274, 2.1.275, 2.1.278 (CLI native install and VS Code extension bundled binary); 2.1.273 works
  • Ubuntu 22.04.5 LTS, git 2.34.1 (distro default)
  • Marketplace source: git over SSH to a self-hosted GitLab; monorepo, marketplace under .claude-plugin/ + .claude/plugins/**, declared with sparsePaths

Related issues (different bugs, same area)

  • #46469 / #45653 — empty marketplace clones, pre-2.1.274 lifecycle bugs (different mechanism)
  • #66579 — sparse-checkout reconcile returns {code:0} for a missing directory and the guard trusts it; second known case of trusting git exit codes where they can't be trusted
  • #94516 — marketplace refresh silently replacing clone content without signal
  • 2.1.275 changelog: "Fixed claude plugin marketplace update deleting a GitHub marketplace's local copy when the fetch failed" — this issue is the remaining half of that fix (validate before swap)

View original on GitHub ↗