Third-party Claude Code plugins (pinned in .claude-plugin/marketplace.json)
occasionally ship bugs we've fixed but can't land upstream synchronously. The
plugin install lives in a cache at
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ that Claude Code
regenerates from the pinned source on every (re)install — so a hand-edit to
the cache is lost the next time pnpm run install-claude-plugins runs.
The durable fix: keep the change as a checked-in patch in
scripts/fleet/plugin-patches/, and have install-claude-plugins.mts reapply it over
the freshly-installed cache as a post-reconcile pass (reapplyPluginPatches()).
🚨 Keep the diff itself as small as possible. When a fix needs more than a few
lines of new logic, move that logic into a standalone file and let the diff
import it + swap the call sites, rather than inlining a 30-line function
body as + lines. A thin diff (an import + a call-site swap) re-anchors cleanly
across upstream version bumps; a fat inlined diff breaks on the first nearby
edit and is painful to review.
Mechanism: a patch named <x>.patch may ship a companion <x>.files/
directory whose tree mirrors the plugin cache root. reapplyPluginPatches()
copies it into the cache (overwrite) before applying the diff, so the thin
diff's import of a sidecar module resolves. Example — the codex stdin fix
ships codex-1.0.1-stdin-eagain.files/scripts/lib/read-stdin-sync.mjs (the
30-line readStdinSync body) and the .patch is a 6-line diff that imports it
in three files.
This is doable for node-smol-shaped patches (we own the consuming source) and
for plugin-cache patches (we copy the sidecar in). It does NOT apply where the
patch target can't import a sibling we control (e.g. some pnpm patch
scenarios that rewrite a published package's internals) — there, inline.
A # @key: value provenance header above a plain diff -u body — never a
git diff (git injects index <hash> / new file mode markers that bare
patch doesn't expect). The reapply step strips everything before the first
--- line and pipes the diff to patch -p1. Sidecar modules (the
smallest-footprint mechanism above) live in the companion <x>.files/ dir, not
in the diff.
# @plugin: codex
# @plugin-version: 1.0.1
# @sha: 9cb4fe4099195b2587c402117a3efce6ab5aac78
# @upstream: https://github.com/openai/codex-plugin-cc
# @description: One-line summary of what the patch fixes
#
# Optional multi-line detail. Each non-blank line begins with #.
#
--- a/scripts/lib/fs.mjs
+++ b/scripts/lib/fs.mjs
@@ -32,9 +32,39 @@
context
-old
+new
context
Required header keys: @plugin, @plugin-version, @sha, @description.
@upstream is recommended. Paths in the diff are plugin-root-relative
(a/scripts/…, b/scripts/…) so patch -p1 resolves them inside the cache
dir. No timestamps on the ---/+++ lines (diff -u adds them; strip with
grep -v $'^[-+]\\{3\\}.*\\t').
<plugin>-<version>-<slug>.patch — e.g. codex-1.0.1-stdin-eagain.patch. The
<plugin> + <version> prefix maps to the cache dir; the version is dotted
semver (1.0.1), the slug is freeform lowercase-kebab. parsePatchFileName
(in install-claude-plugins.mts) parses it; a name that doesn't match is
skipped with a warning.
reapplyPluginPatches() runs after the plugin reconcile:
- Parse the filename →
{ plugin, version }; resolve the cache dir (skip if the plugin isn't installed on this machine). - Strip the
#header; feed the diff topatch -p1 --forward --silentvia stdin. - Idempotency: a forward
--dry-runthat fails while a reverse--dry-runsucceeds means the fix is already present → skip. A patch that applies neither way (the plugin bumped, the patch went stale) warns, doesn't abort — a stale patch must not wedge the whole reconcile.
- Upstream fixes the bug → bump the SHA pin in
marketplace.json(+ the README row) and delete the patch + itsmanifest.mtsentry. The reapply step no-ops cleanly when no patch matches an installed plugin. - Upstream drifts but the bug persists → regenerate the patch against the
new pinned source via the
regenerating-patchesskill, rename to the new version, update the manifest entry.
.claude-plugin/is Claude Code's convention dir (it readsmarketplace.json/plugin.jsonfrom there). Putting our own files inside it risks a future strict validator and conflates ownership.<root>/patches/is pnpm's convention forpnpm patchnpm-dependency patches (wired viapnpm-workspace.yamlpatchedDependencies). A plugin-cache patch there would imply pnpm owns it.
scripts/fleet/plugin-patches/ is plainly ours, next to its only consumer
(install-claude-plugins.mts).