Skip to content

fix(claudecode-plugin): follow upstream plugin hook and agent rules - #2571

Merged
dyoshikawa merged 3 commits into
mainfrom
resolve-issue-2495-plugin-gaps
Aug 4, 2026
Merged

fix(claudecode-plugin): follow upstream plugin hook and agent rules#2571
dyoshikawa merged 3 commits into
mainfrom
resolve-issue-2495-plugin-gaps

Conversation

@dyoshikawa

Copy link
Copy Markdown
Owner

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_DIR is the consumer's repository root. ClaudecodePluginHooks inherited projectDirVar: "$CLAUDE_PROJECT_DIR" from ClaudecodeHooks, 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.

ClaudecodeHooks now exposes its converter config through an overridable static, and ClaudecodePluginHooks overrides only projectDirVar (keeping prefixDotRelativeCommandsOnly: 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, and permissionMode are not supported for plugin-shipped agents" and "The only valid isolation value is \"worktree\"". ClaudecodePluginSubagent had no overrides, so an author writing permissionMode: acceptEdits got that field written into agents/*.md and believed the agent was constrained while Claude Code discarded it — a misleading security posture, not just dead metadata.

ClaudecodeSubagent gains a sanitizeFrontmatter hook (a pass-through by default, so non-plugin Claude Code output is unchanged), and the plugin subclass overrides it to:

  • drop hooks, mcpServers, and permissionMode with a logger.warn;
  • drop isolation unless it is worktree, with a warning;
  • reject a name containing :, which Claude Code reserves for plugin namespacing (v2.1.219).

To carry the warnings, ToolSubagentFromRulesyncSubagentParams gains an optional logger, passed by SubagentsProcessor.

Test plan

  • New unit tests: src/features/hooks/claudecode-plugin-hooks.test.ts (generate + both import forms) and src/features/subagents/claudecode-plugin-subagent.test.ts (each dropped field, the worktree carve-out, the : rejection, plus a guard that non-plugin claudecode output still emits permissionMode/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.md documents the three constraints; src/generated/docs-content.ts regenerated.

Closes #2495

🤖 Generated with Claude Code

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.
@dyoshikawa
dyoshikawa merged commit d10216f into main Aug 4, 2026
9 checks passed
@dyoshikawa
dyoshikawa deleted the resolve-issue-2495-plugin-gaps branch August 4, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow up Claude Code plugin upstream updates: ${CLAUDE_PLUGIN_ROOT} hook commands, forbidden plugin-agent frontmatter, plugin.json path overrides

2 participants