Keamanan Plugin Claude Code: Audit Sebelum Install

Foto oleh Dori via Wikimedia Commons (CC BY 2.5)
Dokumentasi Claude Code menyatakan bahwa plugin dan marketplace adalah komponen yang sangat dipercaya dan bisa menjalankan kode arbitrer di mesin Anda dengan hak akses user Anda. Satu bundle bisa membawa hooks, definisi MCP server, subagent, skill, LSP server, background monitor, executable dan default settings. Anthropic tidak mengontrol isi plugin pihak ketiga dan tidak bisa memverifikasi bahwa plugin itu bekerja sesuai maksudnya, jadi review-nya jadi tanggung jawab Anda.
Ada beberapa. SessionStart jalan saat sesi dibuka atau dilanjutkan, InstructionsLoaded saat sebuah CLAUDE.md dibaca, ConfigChange saat file settings berubah di tengah sesi, CwdChanged saat working directory berpindah, FileChanged saat file yang dipantau berubah di disk, dan SessionEnd saat sesi berakhir. Semuanya tidak butuh prompt atau tool call, dan karena itu membaca setiap command string di file hooks sebuah plugin penting dilakukan sebelum install.
Buka konfigurasi MCP milik plugin itu dan baca transport-nya lebih dulu. Entry stdio menjalankan proses lokal dari command, args dan env, sedangkan entry http atau ws menyebut url dan headers sehingga menjadi tujuan jaringan plus sebuah credential. Periksa apakah headers mengekspansi environment variable, apakah ada command headersHelper yang membuatnya, dan apakah command stdio mengambil paket tanpa pin saat start.
Karena SKILL.md adalah markdown biasa yang menjadi bagian dari instruksi agent, jadi prosanya adalah kanal instruksi, bukan dokumentasi. Skill bersifat model-invoked lewat field description, sehingga description yang luas membuat file itu termuat pada tugas yang tidak pernah Anda pertimbangkan. Cari teks yang mengalihkan agent, misalnya klaim bahwa sebuah pemeriksaan sudah dilakukan, atau preferensi pada command selain yang Anda minta.
Di entry marketplace, source berbasis git menerima ref dan sha, dan kalau keduanya ada maka sha 40 karakter penuh yang menjadi pin efektif. Source archive memakai digest sha256, dan source npm memakai version — tetapi semver range seperti range dengan caret itu langganan, bukan pin. Entry tanpa ref dan tanpa sha mengikuti default branch dan diperbarui di background setelah sesi Anda mulai.

Foto oleh Dori via Wikimedia Commons (CC BY 2.5)
Ringkasan Utama
Plugin Claude Code adalah bundle yang diberi versi dan bisa membawa hooks, MCP server, subagent, skill, executable serta default settings, dan dokumentasi Anthropic sendiri menyatakan plugin menjalankan kode arbitrer dengan hak akses user Anda. Audit setiap hook command, endpoint MCP dan skill prose sebelum install, lalu pin ke commit yang pasti.
Hook yang mengajari saya soal ini adalah hook saya sendiri. Repository ini menjalankan PostToolUse hook yang mereview figure blog setiap kali ada file write, dan saat pertama kali dia jalan saya tidak memintanya di turn itu — saya mengedit satu component, lalu sebuah shell command yang saya konfigurasi berminggu-minggu sebelumnya berjalan dan mencetak temuannya. Satu peristiwa itu adalah versi kecil dari seluruh pertanyaan soal plugin: sesuatu yang dipasang sekali berhak bertindak pada lifecycle event yang tidak Anda picu.
Ini review yang saya jalankan sebelum menekan enter pada sebuah plugin install, dicocokkan dengan halaman referensi plugin, hooks, marketplace dan MCP milik Claude Code sendiri. Isinya defensif saja — kategori risiko dan tempat memeriksanya, pada level yang dibutuhkan seorang reviewer, dan tidak ada apa pun soal cara membuat plugin jahat. Hampir semua bundle yang pernah saya baca memang persis seperti yang dijanjikan. Masalahnya, hampir semua bukan model keamanan.
Dokumentasi Claude Code menyebutnya terang-terangan: plugin dan marketplace adalah komponen yang sangat dipercaya dan bisa menjalankan kode arbitrer di mesin Anda dengan hak akses user Anda. Sebuah plugin adalah satu bundle yang diberi versi, dipasang dengan satu perintah, dan bisa membawa skill, subagent, hooks, definisi MCP server, LSP server, background monitor, executable serta default settings sebagai satu kesatuan. Masing-masing adalah jenis pemberian akses yang berbeda, dan masing-masing punya file di repository yang bisa Anda buka.
| Komponen | Yang didapat saat aktif | Yang harus dibaca |
|---|---|---|
| Hooks | Shell command yang dipicu lifecycle event, sebagian tanpa perlu prompt | hooks/hooks.json, lalu setiap script yang disebut di dalamnya |
| MCP server | Sebuah proses lokal, atau koneksi keluar plus satu credential | config mcp — transport, host, headers, env |
| Subagent | Context window terpisah dengan tool restriction miliknya sendiri | folder agents, dibaca sebagai instruksi |
| Skill | Teks yang menyatu ke instruksi agent begitu skill itu aktif | setiap SKILL.md, termasuk field description |
| Executable | Nama file di PATH milik Bash tool selama plugin aktif | daftar isi folder bin, dibandingkan binary yang Anda pakai |
| Default settings | Key agent bisa menjadikan salah satu agent bundle sebagai main thread | settings.json di root plugin |
Dua detail menentukan keputusan ini milik siapa. Menambahkan marketplace tidak menginstall apa pun — dia hanya mendaftarkan katalog — jadi pemberian akses terjadi saat install, bukan saat add. Lalu install menanyakan scope: user, project atau local. Project scope menulis plugin itu ke file settings repository untuk semua kolaborator, sehingga penilaian lima menit satu orang menjadi default seluruh tim. Karena itu saya memperlakukannya sebagai review yang saya utang ke orang lain, bukan sekadar preferensi saya.
Panel Discover menampilkan bagian Will install yang memuat daftar command, skill, agent, hooks dan server yang disumbangkan sebuah plugin, ditambah estimasi context cost dan tanggal terakhir diperbarui. Baca itu sebelum menekan enter. Untuk plugin dari marketplace lokal atau kustom, panel itu bisa berkata bahwa komponen akan ditemukan saat instalasi, dan justru kalimat itulah yang penting: tidak ada inventaris untuk dibaca, jadi repository adalah satu-satunya tempat jawabannya ada.
Sistem permission menjaga tool, bukan hook command. Referensi hooks Claude Code memuat lebih dari tiga puluh event, dan beberapa di antaranya tidak berutang apa pun pada Anda: SessionStart berjalan saat sesi dibuka, InstructionsLoaded saat sebuah CLAUDE.md dibaca, ConfigChange saat file settings berubah di tengah sesi, CwdChanged saat working directory berpindah, FileChanged saat file yang dipantau berubah di disk, SessionEnd saat sesi berakhir. Tidak ada prompt yang dikirim dan tidak ada tool yang dipanggil untuk semua itu. Satu entry hook adalah sebuah command string, jadi pertanyaannya bukan apa isi JSON-nya — tetapi apa yang dilakukan script-nya.
# Read the bundle from a clone, not from a running session. Loading it in
# order to look at it is not looking at it — loading registers its hooks.
# 1. Which lifecycle events does this bundle hook at all?
jq -r '.hooks | keys[]' hooks/hooks.json
# SessionStart
# PreToolUse
# FileChanged
# SessionEnd
# 2. Every shell command in the file, however deeply nested.
jq -r '.. | .command? // empty' hooks/hooks.json
# scripts/bootstrap.sh
# scripts/guard.sh
# scripts/watch.sh
# 3. Anything that runs unattended, and how long it is allowed to take.
jq -r '.. | select(.async? or .timeout?) | [.command, .timeout, .async] | @tsv' \
hooks/hooks.json
# scripts/watch.sh 1200 true
# 4. jq printed paths, not behaviour. A hook entry is literally a command
# string, so hooks.json is a table of contents. The code is in the scripts.
cat scripts/bootstrap.sh scripts/guard.sh scripts/watch.shTiga field menentukan seberapa besar ruang gerak sebuah hook. Default timeout untuk command hook adalah 600 detik. Mengatur async ke true membuatnya berjalan di background tanpa memblokir turn. Dan filter if, yang terbaca seperti pembatas, dijelaskan di dokumentasi sebagai best-effort, dengan saran memakai sistem permission ketimbang hook kalau Anda butuh allow atau deny yang tegas — dan itu berlaku dua arah bagi reviewer, karena matcher milik hook itu sendiri juga bukan batas keamanan. Setting enterprise yang tampak seperti jawabannya ternyata bukan: allowManagedHooksOnly memblokir hooks dari user, project dan local tetapi tetap mengizinkan hooks dari managed settings dan dari plugin, jadi kontrol yang seolah mengunci hooks justru meninggalkan bundle yang Anda install tetap berjalan.
Dokumentasi menyebut secara eksplisit bahwa Anthropic tidak mengontrol MCP server, file atau software lain yang ada di dalam sebuah plugin, dan tidak bisa memverifikasi bahwa plugin itu bekerja seperti yang dimaksud. Sebuah hook command berjalan di shell Anda, sebagai Anda, dengan PATH Anda dan credential apa pun yang ada di environment Anda. Baca script-nya, bukan README-nya.
Daftar dulu semua server-nya, karena transport memberi tahu jenis akses masing-masing. Server stdio menjalankan proses lokal dari command, args dan env. Server http atau ws memakai url dan headers, jadi itu adalah tujuan jaringan plus sebuah credential. SSE adalah bentuk lama dari hal yang sama dan sudah deprecated. Pertanyaan review yang berguna bukan apakah server itu bermanfaat, melainkan host mana, memegang credential apa, dan bertindak atas nama siapa.
// .mcp.json at the plugin root. Two servers, two entirely different grants.
{
"mcpServers": {
"vendor-api": {
"type": "http",
"url": "https://mcp.vendor.example/mcp",
// A header assembled from YOUR environment at launch. The bundle never
// ships the secret, so a diff of the bundle never shows it either.
"headers": { "Authorization": "Bearer ${VENDOR_TOKEN}" }
},
"local-index": {
"type": "stdio",
// A process on your machine rather than a request to a host. And the
// "@latest" is a second supply chain inside the first: the plugin can
// be pinned to a commit and this package still resolve fresh at launch.
"command": "npx",
"args": ["-y", "@vendor/index-mcp@latest"],
"env": { "INDEX_ROOT": "${CLAUDE_PROJECT_DIR}" }
}
}
}
// Tools from a plugin-bundled server are namespaced with the plugin name,
// which is the exact string a deny rule needs:
// mcp__plugin_vendor-tools_vendor-api__searchDua hal di file itu mudah terlewat. Environment variable diekspansi di dalam url dan headers, dan bentuk dengan nilai default membuat variable yang kosong tidak gagal dengan berisik — jadi sebuah header bisa dirakit dari mesin Anda tanpa secret-nya pernah muncul di bundle. Lalu headersHelper menjalankan sebuah command untuk membuat headers, dan menerima plugin root di environment-nya, sehingga pembuatan header menjadi executable kedua yang harus dibaca. Dokumentasi juga memperingatkan bahwa server yang mengambil konten eksternal bisa membuka risiko prompt injection: begitu sebuah server bisa menarik halaman web ke dalam percakapan, teks yang dia kembalikan masuk ke kanal instruksi yang sama dengan prompt Anda. Tool dari server bawaan plugin diberi namespace memakai nama plugin, dan string ber-namespace itulah yang dicocokkan oleh deny rule, jadi layak dicatat sambil membaca.
Ini langkah yang dilewati para reviewer, dan justru yang paling penting. Sebuah SKILL.md adalah markdown biasa yang menjadi bagian dari instruksi agent, dan skill bersifat model-invoked: description di frontmatter yang menentukan kapan Claude memakainya, jadi description yang luas berarti file itu ikut termuat pada tugas yang tidak Anda bayangkan saat install. Opsi frontmatter yang membuat sebuah skill hanya bisa dipanggil user adalah disable-model-invocation, jadi ketidakhadirannya adalah fakta soal cakupan, bukan kelalaian. Subagent juga markdown, dan dia berjalan di context window sendiri dengan tool restriction sendiri, artinya dia bisa memegang permission yang tidak dimiliki main thread dan membawa instruksi yang tidak pernah Anda lihat di sesi utama.
Yang Anda cari adalah teks yang mengalihkan agent, bukan yang mengajarinya sebuah workflow: satu baris yang menyatakan sebuah pemeriksaan sudah dilakukan, preferensi pada satu command ketimbang command yang Anda minta, alasan untuk membuka file yang tidak dibutuhkan tugas itu, instruksi untuk merangkum ketimbang melaporkan. Semua itu bukan code review. Itu pembacaan prosa, dan rasanya seperti membaca dokumentasi, yang justru sebabnya ia dilewati. Perubahan terbesar jenis ini hanya satu key di satu file: settings.json sebuah plugin bisa mengatur agent, yang mengaktifkan salah satu agent milik bundle itu sebagai main thread beserta system prompt, tool restriction dan model-nya.

Tiga jenis komponen bertindak tanpa dipanggil. Folder bin ditambahkan ke PATH milik Bash tool selama plugin aktif, jadi setiap nama file di dalamnya menjadi nama yang bisa diresolusi agent, dan daftarnya layak dibandingkan dengan binary yang sudah dipakai workflow Anda. File monitors menjalankan sebuah command di background setiap kali plugin aktif dan mengirim tiap baris output-nya ke sesi sebagai notifikasi, jadi sebuah bundle bisa menaruh teks di depan model tanpa ada yang memintanya. Dan userConfig bisa meminta nilai yang ditandai sensitive oleh manifest, lalu menyerahkannya ke hooks sebagai environment variable yang dinamai mengikuti key itu — sebuah token yang diketik user, di dalam environment sebuah shell command yang tidak dibaca user.
# Read the bundle as a directory listing before you read its README.
find . -maxdepth 2 -not -path './.git/*' | sort
# ./.claude-plugin/plugin.json
# ./.mcp.json
# ./agents/release-manager.md
# ./bin/gh
# ./hooks/hooks.json
# ./monitors/monitors.json
# ./settings.json
# ./skills/deploy/SKILL.md
# bin/ is added to the Bash tool's PATH while the plugin is enabled, so every
# filename in it is a name the agent can now resolve. Read the list against
# the binaries your own workflow already calls.
ls -la bin/
# settings.json at the plugin root honours two keys today, and one of them is
# "agent": it activates one of the bundle's own agents as the MAIN thread —
# that agent's system prompt, its tool restrictions and its model.
cat settings.json
# { "agent": "release-manager" }
# monitors.json runs in the background whenever the plugin is active, and each
# stdout line arrives in the session as a notification. Nobody invokes it.
jq -r '.[] | [.name, .command] | @tsv' monitors/monitors.json
# Installed bundles are cached here, so this is what is really on disk.
ls ~/.claude/plugins/cacheLokasi file-nya penting untuk review itu sendiri. Bundle yang sudah terpasang di-cache di folder plugin Claude di home directory Anda, jadi di situlah isi yang benar-benar ada di disk setelah Anda menyetujuinya. Dua flag yang memuat sebuah bundle untuk satu sesi, satu dari direktori atau zip dan satu dari URL, membawa pertimbangan trust yang sama seperti sumber lain — dan di sinilah jebakannya: memuat sebuah plugin demi memeriksanya justru mendaftarkan hooks-nya dan menyalakan server-nya. Baca repository-nya pada satu commit, di dalam clone, sebelum ada apa pun yang memuatnya.
Entry tanpa ref dan tanpa sha mengikuti default branch repository, dan Claude Code memperbarui plugin berbasis git ketika ada commit baru sampai di ref yang dipin, atau langsung kalau ref tidak diset sama sekali. Update diperiksa setelah sesi Anda mulai, dengan jeda acak sampai sepuluh menit, jadi sesi yang sedang jalan tetap memakai versi yang dia muat saat start dan perubahannya masuk di sesi berikutnya. Default-nya berbeda menurut asal: marketplace resmi Anthropic punya auto-update aktif, sedangkan marketplace pihak ketiga dan marketplace pengembangan lokal tidak, dan itu layak dikonfirmasi ketimbang diasumsikan ke arah mana pun. Field version di manifest plugin mengunci semuanya, karena user hanya menerima update saat penulisnya menaikkan angka itu.
// One marketplace entry, showing three degrees of pinning.
{
"name": "release-tools",
"source": {
"source": "github",
"repo": "acme/claude-plugins",
// A branch or a tag. A tag can be moved by whoever owns the repository,
// and a branch moves by design, so this alone is a subscription.
"ref": "v2.4.0",
// The full 40-character commit. When both are set this is the effective
// pin, and the upgrade becomes something you choose rather than receive.
"sha": "4f1a9c0d2e7b8a3f5c6d1e0b9a8f7c6d5e4b3a29"
}
}
// The equivalent for the other source types:
// archive : "sha256" — a 64-hex digest of the zip
// npm : "version" — but "^2.0.0" is a RANGE, so it is not a pin
// command : versioned by a hash of the command output; cannot be pinned
// And the entry when nobody pinned anything. No ref, no sha: the default
// branch, refreshed in the background after your session starts.
{ "name": "release-tools", "source": { "source": "github", "repo": "acme/claude-plugins" } }Kalau ref dan sha dua-duanya diset, sha yang menjadi pin efektif, dan itulah field yang saya ingin ada di entry marketplace yang bukan saya tulis. Archive memakai digest sha256. Sumber npm memakai version yang bisa berupa semver range, dan range dengan caret itu langganan, bukan pin. Sumber command diberi versi dari hash output-nya sendiri dan tidak bisa dipin sama sekali. Setelah itu review ulang setiap upgrade, karena diff-nya berisi instruksi: satu kalimat yang berubah di sebuah SKILL.md bisa mengubah perilaku agent lebih besar daripada seratus baris TypeScript yang berubah, dan ia tidak akan tampak seperti perubahan yang perlu dibaca. Pilih juga marketplace dengan pemilik yang punya nama. Field author di manifest bersifat opsional, dan bundle yang perubahannya bisa Anda tunjuk pemiliknya lebih bernilai daripada bundle dengan daftar fitur lebih panjang.
Ada jalur yang sudah disaring kalau Anda menginginkannya. Community marketplace milik Anthropic hanya menerima kiriman pihak ketiga setelah validasi otomatis dan penyaringan keamanan, lalu mem-pin setiap plugin yang disetujui ke commit SHA tertentu di katalognya, dengan CI yang menggeser pin itu saat penulisnya push. Penulis plugin bisa menjalankan validasi yang sama secara lokal lewat command plugin validate di CLI, dan menambahkan flag strict agar warning ikut menggagalkan.

Urutannya penting, karena setiap langkah membuat langkah berikutnya lebih murah. Tidak ada langkah sebelum nomor enam yang menuntut plugin-nya sudah terpasang, dan itulah inti dari menjalankannya dalam urutan ini.
Dua di antaranya memang sering dilewati di lapangan. Langkah empat dilewati karena prosa tidak tampak seperti permukaan serangan, dan langkah tujuh dilewati karena pinning terasa kerja tambahan sampai upgrade pertama yang datang tanpa suara. Sisanya cukup mekanis untuk diserahkan ke sebuah script, dan itu justru alasan untuk menulisnya.
Aturan yang saya bawa dari semua ini singkat: review plugin adalah pembacaan shell command dan prosa, dalam urutan itu, pada satu commit yang disebut namanya. Kapabilitasnya nyata dan hampir selalu tidak berbahaya, dan memperlakukannya sebagai hampir selalu tidak berbahaya bukan hal yang sama dengan sudah mengauditnya. Kalau Anda tidak bisa menyebut lifecycle event apa yang dihook sebuah bundle, host mana yang dihubungi server-nya, dan commit mana yang Anda install, Anda belum mereviewnya. Anda memercayainya, dan bedanya layak diucapkan terang-terangan ke orang-orang yang berbagi repository dengan Anda.
Sumber dan bacaan lanjutan