fix(claudecode-plugin): follow upstream plugin hook and agent rules - #2571
Merged
Conversation
Plugin hook commands now resolve against $CLAUDE_PLUGIN_ROOT instead of $CLAUDE_PROJECT_DIR, so a script bundled in the plugin is found in the plugin install directory rather than in each consumer's own repository. Both directions use the same converter config, so generated commands round-trip back to their relative form on import. Plugin subagents no longer emit hooks, mcpServers, or permissionMode, which Claude Code discards for plugin-shipped agents; emitting them left the author believing the agent was permission-constrained when it was not. isolation is constrained to worktree, and a name containing the plugin namespace separator is now rejected instead of producing an agent Claude Code refuses to load.
Address review feedback. The colon restriction comes from Claude Code 2.1.219 and applies to all agent markdown files, not only plugin-shipped ones, so it moved to the base ClaudecodeSubagent and warns rather than throwing: a single subagent targeting "*" would otherwise abort an entire generate run over a name only the author can fix. Also derive the dropped plugin fields from PLUGIN_FORBIDDEN_FIELDS instead of a hand-written destructuring that could drift from it, cover the processor-level logger wiring, and cover the exec-form and variable-prefixed hook commands.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves gaps 1 and 2 of #2495. Gap 3 (plugin.json component-path overrides) is a design decision and was split into #2570 instead of being implemented here.
Gap 1 — plugin hook commands resolve against the plugin, not the consumer's project
Claude Code documents
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/format-code.sh"for plugin hooks:${CLAUDE_PLUGIN_ROOT}is the plugin install directory, while$CLAUDE_PROJECT_DIRis the consumer's repository root.ClaudecodePluginHooksinheritedprojectDirVar: "$CLAUDE_PROJECT_DIR"fromClaudecodeHooks, so a hook script bundled inside the plugin was rewritten to a path in each consumer's own repo, where it does not exist — the hook silently never ran.ClaudecodeHooksnow exposes its converter config through an overridable static, andClaudecodePluginHooksoverrides onlyprojectDirVar(keepingprefixDotRelativeCommandsOnly: true). Because both directions read the same config, import also recognizes"$CLAUDE_PLUGIN_ROOT"/…and the braced${CLAUDE_PLUGIN_ROOT}/…exec form and converts them back to the relative command, so plugin hooks round-trip.Gap 2 — plugin subagents emitted frontmatter upstream forbids
Upstream, verbatim: "For security reasons,
hooks,mcpServers, andpermissionModeare not supported for plugin-shipped agents" and "The only validisolationvalue is\"worktree\"".ClaudecodePluginSubagenthad no overrides, so an author writingpermissionMode: acceptEditsgot that field written intoagents/*.mdand believed the agent was constrained while Claude Code discarded it — a misleading security posture, not just dead metadata.ClaudecodeSubagentgains asanitizeFrontmatterhook (a pass-through by default, so non-plugin Claude Code output is unchanged), and the plugin subclass overrides it to:hooks,mcpServers, andpermissionModewith alogger.warn;isolationunless it isworktree, with a warning;:, which Claude Code reserves for plugin namespacing (v2.1.219).To carry the warnings,
ToolSubagentFromRulesyncSubagentParamsgains an optionallogger, passed bySubagentsProcessor.Test plan
src/features/hooks/claudecode-plugin-hooks.test.ts(generate + both import forms) andsrc/features/subagents/claudecode-plugin-subagent.test.ts(each dropped field, theworktreecarve-out, the:rejection, plus a guard that non-pluginclaudecodeoutput still emitspermissionMode/isolation).npx vitest run --config vitest.e2e.config.ts src/e2e/e2e-hooks.spec.ts src/e2e/e2e-subagents.spec.ts src/e2e/e2e-plugins.spec.ts— 183 passed.pnpm cicheck(full) passes.docs/guide/plugin-packaging.mddocuments the three constraints;src/generated/docs-content.tsregenerated.Closes #2495
🤖 Generated with Claude Code