DeepDojo.AIPublished 2026-11-13Updated 2026-11-13中文
Claude Code Plugin Upgrades You Can Roll Back
Every team plugin eventually gets an upgrade: the new version fixes bugs and adds features, and everyone updates as usual. This time, the new version's startup script quietly migrated the plugin's local cache to a new storage format.
Soon it's failing for several teammates. You roll the plugin back to the old version, expecting the workflow to recover, but the old logic hits the new-format cache file, fails to parse it, and crashes all the same.
The trouble is that "code version" and "persistent data" were treated as one thing. Plugin code can move back with the version number, but the caches, indexes, and state it produced stay where they are, already in the new format. Once a plugin is out in the world, a few things need managing on their own:
- Sensitive config: a token marked sensitive lands in secure storage, but a later script may still print it into a log.
- Runtime data: caches written into the install directory can be wiped out by the next upgrade.
- Upgrade and rollback: the code can go back to the old version; the data may not.
Design each of these as its own line. Follow a sensitive value from storage to every use. Write runtime data into the dedicated persistent directory. Before upgrading, record the old version, back up the data, and run your common calls, and confirm you can roll back before rolling the upgrade out to the team.
01 Sensitive config: stored safely, then check how it's used
A plugin can declare user configuration in its manifest. Mark sensitive items with sensitive: true. Per the Claude Code plugins reference, the input is masked and the value goes into secure storage rather than settings.json.
{
"name": "team-notify",
"version": "1.0.0",
"userConfig": {
"channel": {
"type": "string",
"title": "Notification channel",
"description": "Default notification channel"
},
"token": {
"type": "string",
"title": "Notification service credential",
"description": "Notification service token",
"sensitive": true
}
}
}Manage the values separately:
| Content | How to handle it |
|---|---|
| Channel, API endpoint, check toggles, team name, default environment | Ordinary userConfig |
| Private tokens and other secrets | Sensitive userConfig, environment variables, or system credential storage; never in templates, scripts, or .mcp.json |
After marking an item sensitive, follow the value forward: which script reads it, and could that script print it? The docs note that hook processes receive every user configuration value as an environment variable. Secure storage solves storage; it can't stop a later use from writing the value into a log. Find everything whose leak would cause harm, then check how the current version stores and actually uses it.
02 Runtime data goes in CLAUDE_PLUGIN_DATA
A plugin can keep its runtime data under ${CLAUDE_PLUGIN_DATA}, which suits caches, local indexes, plugin state, the last run's record, and installed dependencies. An example hook config:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/log-command.js\" --data-dir \"${CLAUDE_PLUGIN_DATA}\""
}
]
}
]
}
}The most common mistake in plugin scripts is creating a cache/ folder or an index.db right next to the source. What happens next? When a new version ships and users upgrade, the old install directory is replaced wholesale with new files. The local search index, session state, or cache you built for users disappears with it, and on the next start they scan the whole project from scratch.
So keep the two built-in variables' jobs apart:
CLAUDE_PLUGIN_ROOTholds only the static code, bundled templates, and read-only scripts that ship with each version; never write runtime data there.- Local caches, build indexes, and user preferences that must survive versions go into the persistent directory
CLAUDE_PLUGIN_DATApoints to.
Per the Claude Code plugin manifest reference, that directory is ~/.claude/plugins/data/<id>/, created on first reference and kept across plugin updates. Keep runtime data apart from static source, and users won't have their experience interrupted every few releases.
Uninstalling is another matter. The docs say that by default the data directory is deleted when the plugin is uninstalled from the last place it's installed. Work out which data can be rebuilt and which must be kept, then check the uninstall options and back up.
03 Before release, check along the real load path
Before release, load the plugin from a local path to test a real install:
claude --plugin-dir ./my-plugin
claude --plugin-dir ./my-plugin --plugin-dir ./another-pluginCheck along the actual load path:
| Layer | What you should see |
|---|---|
| Manifest and entries | plugin.json is valid; commands are listed and callable |
| Agents and methods | Agents are discovered; skills trigger as needed |
| Automation and services | Hooks fire at the right time, MCP connects, LSP doesn't block startup |
| Materials and config | Template paths are right, scripts are executable, userConfig is readable |
| Sensitive content | Marked, and kept out of source, logs, and version control |
Then run a static check:
claude plugin validate ./my-pluginThe docs treat claude plugin validate as the authoritative manifest check; it confirms each component path exists and stays inside the plugin directory. Once it passes, walk a low-risk command through the real flow. Can it read the template? Did the hooks you need actually fire? When an external service is unreachable, does the result say so clearly? Static checks catch configuration problems; a real call shows whether the components connect.
04 Upgrades: code and data are two separate lines
Prepare an upgrade in this order:
Record the old version and DATA path, confirm the old version is obtainable
→ Back up old data, or note which data can be rebuilt
→ Upgrade and run common calls, including ones with arguments
→ Compare fields, formats, argument parsing, and behavior
→ If incompatible, fix it first; don't roll it out to the team while unresolvedBefore pressing upgrade, walk through the worst case one step further: if the new version fails widely, will rolling the plugin code back restore the workflow? As in the opening example, probably not. The new startup script has already migrated the cache database in CLAUDE_PLUGIN_DATA to a new structure; the old code reads the new-format file and crashes anyway.
So a plugin upgrade plan treats "code version" and "persistent data" as two independent lines:
- Code: tag versions clearly and make sure the last stable package is always obtainable.
- Data: snapshot the persistent data directory before upgrading, or design from the start so the new version reads the old format and the old version recognizes and skips the new one.
Recording the version number isn't enough. Confirm the data can be rolled back before rolling a major version out to the team.
05 Treat external plugins as unknown code
A plugin bundles automatic execution, external connections, and writes, so check the whole chain of components:
- Source and documentation: a trusted source, dependencies and install notes, a version history.
- Execution scope: automatic hooks, agent tools, sensitive MCP, and any system or production writes.
- Configuration: sensitive
userConfigmarking and how values are really used. - Recovery: it can be disabled and uninstalled, and there's a rollback path.
Until its tools have been reviewed, any external plugin package should be treated as unknown code with local execution rights. Bundling files into a package doesn't create a sandbox around them. Plugin subagents do have some fields restricted (hooks, mcpServers, and permissionMode are ignored), which keeps a plugin agent from quietly gaining too much control once installed. The runtime environment, tool permissions, and external accounts still need tightening on their own.
06 Debug plugin problems layer by layer
When something goes wrong, check by symptom, one layer at a time.
If the plugin won't install, check:
- Whether this Claude Code version supports the plugin's capabilities.
- Whether
plugin.jsonis valid JSON. - Whether the plugin's directory structure is correct.
- Whether the current user has read permission.
- Whether the local path is correct.
If components don't load, check:
- Whether the paths declared in
plugin.jsonexist. - Whether commands are in the right directory.
- Whether agent frontmatter is valid.
- Whether each skill includes a
SKILL.md. - Whether hook event names are correct.
If commands aren't available after install, use /plugin to confirm the plugin is installed and enabled, and run /reload-plugins if needed. It rereads the manifest, commands, agents, skills, hooks, and MCP/LSP config, so you can verify changes in the current session.
If MCP fails to connect, check:
- Whether the MCP server is installed.
- Whether the environment variables exist.
- Whether the credentials are valid.
- Whether the endpoint is reachable.
- Whether the path in the plugin's
.mcp.jsonis correct.
If hooks misbehave, check:
- Whether the script has execute permission.
- Whether the hook event name is correct.
- Whether
matchermatches. - Whether the script can read JSON from stdin.
- Whether errors go to stderr.
For when to package a plugin and how to set its directories and version numbers, see when to package your Claude Code setup as a plugin. Commands and templates should keep real credentials out of files too; for setting boundaries on commands with side effects, see why a "read-only" Claude Code command still edits.
Code can switch back and forth with the version number, but data only moves forward. Give the data a way back in your upgrade plan, and a downgrade really does return the workflow to where it was.