Upgrade Plugin Claude Code: Version Pinning dan Rollback

Foto oleh Dr. Marcus Gossler via Wikimedia Commons (CC BY-SA 3.0)
Tidak secara langsung. Command claude plugin install tidak menerima argumen versi, jadi versi yang Anda dapat adalah apa pun yang di-resolve katalog marketplace saat itu. Untuk mengendalikan versinya Anda harus mengendalikan katalognya: tambahkan sha pada source plugin itu di marketplace.json milik Anda sendiri agar terpaku pada satu commit yang persis.
Tidak ada command downgrade, jadi rollback harus disusun sendiri. Disable plugin itu untuk menghentikan perilakunya, jalankan ulang dengan --plugin-dir yang menunjuk direktori versi sebelumnya di plugin cache agar hari itu bisa diselesaikan, lalu geser sha di katalog Anda kembali ke commit terakhir yang sudah Anda evaluasi dan jalankan claude plugin update. Pastikan lewat claude plugin list dan sebuah reload.
Tergantung marketplace-nya. Marketplace resmi Anthropic punya auto-update aktif secara default, sedangkan marketplace pihak ketiga dan marketplace pengembangan lokal nonaktif secara default. Ketika aktif, Claude Code memeriksa update setelah session Anda mulai dengan jeda acak sampai sepuluh menit, lalu meminta Anda reload kalau ada yang berubah.
Untuk source berbasis git, commit SHA yang ter-resolve menjadi versinya. Artinya setiap commit yang publisher push ke ref yang Anda ikuti terhitung sebagai rilis baru bagi Anda, dan update datang setiap kali commit itu berubah. Ini default yang wajar untuk plugin internal yang sedang aktif dikembangkan dan buruk untuk apa pun yang diandalkan sebuah tim.
Pakai dua pemeriksaan. Diff claude plugin details sebelum dan sesudah update untuk melihat komponen yang muncul atau hilang dan pergeseran biaya token always-on. Lalu jalankan claude plugin eval terhadap id plugin yang terinstall dengan sebuah threshold, yang menambahkan baseline arm tanpa plugin dan keluar dengan exit 1 saat ada case di bawah batas yang Anda tetapkan.

Foto oleh Dr. Marcus Gossler via Wikimedia Commons (CC BY-SA 3.0)
Ringkasan Utama
Upgrade plugin Claude Code jarang merusak build; yang berubah adalah cara agent berperilaku. Pin versi yang persis, jadikan eval run dan diff claude plugin details sebagai gate setiap upgrade, dan simpan changelog efek perilaku, karena tidak ada command downgrade yang bisa Anda andalkan.
Minggu ketika sebuah upgrade plugin masuk adalah minggu ketika tidak ada yang menyalahkan plugin itu. Auto-update jalan di background beberapa menit setelah tiap session dimulai, versi di disk bergeser, tidak ada error, dan satu-satunya tanda yang terlihat hanyalah ajakan menjalankan reload-plugins. Tiga hari kemudian keluhannya adalah agent terasa lebih buruk minggu ini, bukan bahwa ada yang rusak.
Tulisan ini soal failure mode yang khas ketika Anda mengirimkan instruksi sebagai dependency, dan cara membuatnya bisa ditangani: mengapa pin versi yang persis adalah default dan bukan paranoia, cara mengetahui bahwa sebuah marketplace menggeser tag di bawah Anda, cara mengubah perilaku berubah menjadi angka, dan cara rollback sebuah plugin ketika CLI-nya tidak punya command downgrade. Semua command di sini dicek terhadap Claude Code 2.1.259 dan dokumentasi plugin.
Plugin adalah bundle berversi berisi skills, subagents, hooks, definisi MCP server dan instruksi, jadi muatannya adalah perilaku, bukan kode yang dikompilasi build Anda. Ketika upgrade library rusak, ada yang throw dan stack trace menyebut nama library itu. Ketika upgrade plugin rusak, agent tetap jalan dan diam-diam bekerja dengan cara lain, sehingga laporannya datang sebagai suasana hati, bukan sebagai bug.
| Kata tim Anda | Yang sebenarnya berubah | Di mana itu terlihat |
|---|---|---|
| Checklist migration kami berhenti dipakai | Description sebuah skill diperketat dan tidak lagi cocok dengan cara orang menuliskan permintaannya | Tidak di mana pun. Skill itu memang tidak pernah fire |
| Hari ini setiap edit terasa lambat | Ada hook PostToolUse baru yang menjalankan formatter atas file itu pada setiap write | Wall-clock time per turn, dan debug log |
| Test runner yang dipilih selalu salah | Satu kalimat di file instruksi ditulis ulang, sehingga default yang lain yang menang | Transcript, tetapi hanya kalau Anda sudah mencurigai plugin |
| Sebuah tool call gagal karena namanya | Satu MCP server di dalam bundle mengganti nama tool-nya | Transcript, langsung. Ini yang paling mudah |
Dua mekanisme membuat penelusurannya sulit. Claude Code memeriksa update marketplace dan plugin setelah session Anda mulai, dengan jeda acak sampai sepuluh menit, jadi versi saat Anda membuka session belum tentu versi saat Anda menutupnya, dan notifikasi yang Anda terima adalah ajakan reload, bukan ringkasan apa yang berubah. Lalu plugin yang update di tengah session membiarkan hooks, monitors, MCP server dan LSP server menunjuk path versi sebelumnya sampai Anda reload, jadi sebagian siang itu separuh plugin lama dan separuh baru.
Claude Code memakai versi plugin sebagai cache key yang menentukan ada tidaknya update, dan untuk semua source type kecuali command versi itu diambil dari yang pertama terisi di antara lima sumber: field version di plugin.json, lalu version di entry marketplace, lalu git commit SHA dari source untuk github, url, git-subdir dan source berpath relatif, lalu digest SHA-256 sebuah archive source yang dipotong dua belas karakter, dan terakhir unknown untuk npm source atau direktori lokal di luar git repository.
# The version IS the cache key. Claude Code computes it, compares it with what
# is installed, and skips the update when they match. For every source type
# except command, it takes the FIRST of these that is set:
# 1 the version field in the plugin's plugin.json
# 2 the version field in its entry in marketplace.json
# 3 the git commit SHA of the source (github, url, git-subdir, ./relative)
# 4 the SHA-256 digest of an archive source, first 12 characters
# 5 unknown, for an npm source or a local dir outside a git repo
# Rung 3 is the trap: no declared version does not mean no updates. It means
# every commit on the ref you track is a release to you.
$ claude plugin list --json | jq '.[] | {id, version, installedAt, lastUpdated}'
{
"id": "review-toolkit@acme-tools",
"version": "2.4.0",
"installedAt": "2026-06-29T13:02:26.195Z",
"lastUpdated": "2026-09-01T07:28:31.657Z"
}
# lastUpdated is the field nobody reads. In a week where nothing errored, it
# is the only record in the session that Tuesday happened at all.Anak tangga ketiga itulah yang mengejutkan banyak orang. Kalau publisher tidak mendeklarasikan field version di mana pun, commit SHA yang menjadi versinya, jadi setiap commit yang mereka push ke ref yang Anda ikuti adalah rilis untuk Anda. Dokumentasinya justru menyarankan pola itu untuk plugin internal yang sedang aktif dikembangkan, dan itu wajar bagi penulisnya tetapi buruk bagi tim berisi dua belas orang, karena penulisnya yang lalu menentukan ritme update Anda. Baca entry marketplace-nya sebelum install: plugin tanpa version yang dideklarasikan tidak punya rilis yang bisa Anda sebut namanya di sebuah incident review.
Default-nya tidak simetris, dan itu perlu diketahui sebelum Anda menganggap ada yang sudah beku. Marketplace resmi dan sebagian besar marketplace resmi Anthropic lain punya auto-update aktif secara default; marketplace pihak ketiga dan marketplace pengembangan lokal justru nonaktif secara default. Jadi katalog yang paling sedikit Anda periksa adalah yang paling diam, dan plugin yang paling Anda percayai adalah yang bergerak tanpa bertanya. Tidak ada default yang salah di sini, tetapi hanya satu yang cocok dengan apa yang tim Anda kira sedang terjadi.
// 1. Auto-update is per marketplace, and the defaults are NOT symmetric:
// claude-plugins-official and most other official Anthropic marketplaces
// default to true; third-party and local dev marketplaces default to false.
// .claude/settings.json
{
"extraKnownMarketplaces": {
"acme-tools": {
"source": { "source": "github", "repo": "acme-corp/claude-plugins" },
"autoUpdate": false
}
}
}
# 2. The machine-wide switch. Read the second line carefully before copying it:
export DISABLE_AUTOUPDATER=1 # stops background marketplace refreshes
export FORCE_AUTOUPDATE_PLUGINS=1 # RE-ENABLES plugin updates. Omit it to pin.
// 3. The only immovable pin: a sha on a PLUGIN source, in a catalogue you own.
// Marketplace sources take a ref and no sha. Plugin sources take both, and
// when both are set the sha wins: Claude Code checks that commit out directly.
// .claude-plugin/marketplace.json
{
"name": "acme-tools",
"owner": { "name": "Platform Team" },
"plugins": [
{
"name": "review-toolkit",
"description": "PR review agents, pinned at the last commit we evaluated",
"source": {
"source": "github",
"repo": "vendor/review-toolkit",
"ref": "v2.3.0",
"sha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
}
}
]
}Mematikan auto-update tidak sama dengan pinning. Yang berhenti adalah refresh di background, sedangkan install atau update yang Anda jalankan dengan menyebut nama marketplace tetap melakukan refresh marketplace itu lebih dulu, bahkan dengan auto-update mati dan DISABLE_AUTOUPDATER terpasang. Satu-satunya pin yang benar-benar tidak bergeser adalah sha pada sebuah plugin source di dalam katalog yang Anda kendalikan. Marketplace source menerima ref tanpa sha; plugin source menerima keduanya, dan ketika keduanya diisi, sha yang menang dan Claude Code langsung checkout commit itu.
Pin lalu lupa adalah bentuk gangguan tersendiri. Plugin yang dibekukan di satu commit tidak akan pernah menerima perbaikan upstream untuk hook yang menggantung di repository besar, dan Anda menemukannya kembali berbulan-bulan kemudian tanpa ingat kenapa pin itu ada. Tulis alasan dan tanggal reviewnya di commit yang sama dengan pin-nya, atau Anda hanya menukar upgrade yang mengejutkan dengan kegagalan upgrade yang mengejutkan.
Anda bisa menambahkan marketplace pada sebuah branch atau tag dengan menempelkan ref, dan itu terbaca seperti pin. Padahal bukan. Marketplace yang ditambahkan dengan ref berupa branch atau tag akan update ke commit terbaru dari ref itu, bukan ke default branch repository-nya, jadi publisher yang menghapus lalu membuat ulang v2.3.0 di commit yang lebih baru, hal yang wajar dilakukan tim kecil, menggeser Anda pada refresh berikutnya tanpa sepatah kata. Mengikuti sebuah branch adalah pengaturan yang sama tanpa kepura-puraan: Anda punya nama, bukan versi.
# This reads like a pin and is not one. A marketplace added with a branch or
# tag ref updates to the LATEST COMMIT OF THAT REF on every refresh, so a
# publisher who deletes and re-creates v2.3.0 moves you without a word.
$ claude plugin marketplace add https://github.com/vendor/plugins.git#v2.3.0
# installLocation is a real git clone on your disk. Ask it what it checked out.
$ claude plugin marketplace list --json | jq -r '.[].installLocation'
/Users/me/.claude/plugins/marketplaces/acme-tools
$ git -C ~/.claude/plugins/marketplaces/acme-tools rev-parse HEAD
7f3c9a1e4b2d8065c1af93e7d2b6045a8e91cf37
# Commit both snapshots and let CI diff them on every run. Two files, and the
# case you cannot see any other way shows up as a changed SHA next to an
# UNCHANGED version string:
$ claude plugin marketplace list --json > .claude/plugin-marketplaces.lock
$ claude plugin list --json > .claude/plugin-versions.lockDeteksinya hanya dua command dan tempatnya di repository Anda, bukan di ingatan seseorang. Listing marketplace memberi Anda path clone lokalnya, dan git di path itu memberi Anda commit yang sebenarnya di-checkout. Commit kedua snapshot itu dan biarkan CI mendiff keduanya setiap run, karena tag yang bergeser muncul sebagai SHA yang berubah di sebelah version string yang tidak berubah, dan pasangan itu satu-satunya tanda yang tidak bisa Anda lihat dengan cara lain.

Simpan snapshot claude plugin list --json ke dalam repository pada setiap rilis. Isinya version, scope, installPath, installedAt dan lastUpdated untuk tiap plugin, jadi pertanyaan kapan ini berubah berhenti menjadi percakapan dan menjadi git log.
Lakukan pemeriksaan yang murah dulu, karena hanya butuh beberapa detik dan tidak memanggil model. claude plugin details mencetak inventaris komponen plugin itu yang dikelompokkan sebagai Skills, Agents, Hooks, MCP server dan LSP server, beserta angka always-on untuk token yang ditambahkan teks listing-nya ke setiap session. Ambil snapshot sebelum update dan sesudahnya, lalu diff keduanya. Komponen yang muncul dan angka always-on yang bertambah keduanya adalah perubahan pada default agent, bukan pada pilihannya.
# Cheap check, seconds, no model calls. Snapshot the inventory, update, snapshot
# again, diff. claude plugin update warns that a restart is needed to apply.
$ claude plugin details review-toolkit@acme-tools > before.txt
$ claude plugin update review-toolkit@acme-tools
$ claude plugin details review-toolkit@acme-tools > after.txt
$ diff before.txt after.txt
# Illustrative shape, not my numbers — yours come from your own two runs:
- Skills (3) review-diff, check-migrations, write-tests
+ Skills (4) review-diff, check-migrations, write-tests, enforce-style
- Hooks (1) SessionStart
+ Hooks (2) SessionStart, PostToolUse
# A new always-on skill in a review plugin is a new DEFAULT, not a new option,
# and a new PostToolUse hook is wall-clock time on every single write.
# Measured check. The target may be an INSTALLED plugin's id, not just a path,
# and when a plugin resolves Claude Code adds a no-plugin baseline arm, so you
# read a delta rather than a bare score.
$ claude plugin eval review-toolkit@acme-tools \
--threshold 0.8 \
--runs 5 \
--json ./upgrade-2.4.0.json
# --threshold exits 1 if any case scores below it. That exit code is the whole
# difference between a gate and a report.Lalu pemeriksaan yang terukur. claude plugin eval menerima plugin yang sudah terinstall lewat id yang menggabungkan namanya dengan nama marketplace-nya, tidak hanya sebuah path, dan ketika sebuah plugin ter-resolve ia menambahkan baseline arm tanpa plugin secara default, sehingga run-nya melaporkan delta bukan skor tunggal. Flag threshold keluar dengan exit 1 kalau ada case yang di bawahnya, dan itu seluruh perbedaan antara sebuah gate dan sebuah laporan. Batas jujurnya: eval suite hanya mengukur apa yang Anda tulis case-nya, jadi case yang paling layak ditulis lebih dulu adalah prompt yang benar-benar diketik tim Anda.
CLI plugin punya details, disable, enable, eval, init, install, list, marketplace, prune, tag, uninstall, update dan validate. Tidak ada downgrade, dan install tidak menerima argumen versi, jadi install yang lama bukan sesuatu yang bisa Anda ketik. Karena itu rollback disusun dari empat langkah, dan hanya langkah ketiga yang tahan lama.
# claude plugin has: details disable enable eval init install list marketplace
# prune tag uninstall update validate. No downgrade. And install takes no
# version argument, so "install the old one" is not a thing you can type.
# 1. Stop the behaviour. Disable the PLUGIN, not the marketplace — removing a
# marketplace from its last scope uninstalls every plugin you got from it.
/plugin disable review-toolkit@acme-tools
# 2. Get today back. An update marks the previous version directory orphaned
# and a background sweep removes it roughly 14 days later, so it is still
# there. A --plugin-dir copy takes precedence over an installed plugin of
# the same name for that session.
$ ls ~/.claude/plugins/cache/acme-tools/review-toolkit/
2.3.0 2.4.0
$ claude --plugin-dir ~/.claude/plugins/cache/acme-tools/review-toolkit/2.3.0
# The docs call that directory ephemeral and tell you not to write state
# there. Use it to finish the afternoon, never as your pin.
# 3. Make it durable: move the sha in YOUR catalogue back to the last commit
# you evaluated, then re-resolve.
$ claude plugin marketplace update acme-tools
$ claude plugin update review-toolkit@acme-tools
# 4. Prove it, in this order.
$ claude plugin list --json | jq -r '.[] | select(.id == "review-toolkit@acme-tools") | .version'
2.3.0
/reload-plugins # hooks, MCP and LSP servers hold the OTHER version's path until this
# monitors only follow on a full session restart
$ claude plugin eval review-toolkit@acme-tools --case 'migration-*' --threshold 0.8Memastikan rollback berhasil butuh lebih dari sebuah version string. Versi di claude plugin list memberi tahu apa yang terinstall di disk; reload-plugins yang memindahkan hooks, MCP server dan LSP server dari path versi yang lain; monitors baru ikut setelah session direstart penuh; dan eval case yang tadi turun itulah yang memberi tahu bahwa perilakunya kembali, bukan hanya byte-nya. Periksa dalam urutan itu, karena satu langkah bisa lulus sementara langkah berikutnya masih gagal.

Semantic versioning mencatat maksud publisher-nya. Changelog Anda mencatat pengalaman tim Anda, dan keduanya dokumen yang berbeda: rilis patch yang menulis ulang satu instruksi adalah kenaikan sepele di hulu dan satu hari kerja penuh kebingungan di sini. Satu entry per upgrade yang Anda terima, lima isian, disimpan di repository tepat di sebelah pin-nya.
Hasilnya terasa pada insiden kedua. Yang pertama memakan tiga hari untuk tidak tahu harus melihat ke mana. Dengan file ini yang kedua memakan satu siang, karena seseorang melakukan grep atas nama skill-nya dan menemukan entry yang menyebut bahwa description ini diperketat di 2.4.0 dan berhenti fire pada susunan kata yang dipakai tim ini.
Perlakukan plugin sebagai dependency instruksi, bukan dependency kode, dan praktiknya menyusul sendiri: pin sebuah commit yang persis di katalog yang Anda kendalikan, jadikan diff details dan eval run sebagai gate setiap upgrade, simpan snapshot apa yang terinstall ke dalam repository agar tag yang bergeser jadi terlihat, dan catat efek perilakunya alih-alih nomor versinya. Upgrade yang tidak mengubah apa pun yang bisa Anda lihat justru yang paling layak diberi catatan.
Sumber dan bacaan lanjutan