Skip to content

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

Description

@dyoshikawa

Summary

Three plugin-specific divergences from the documented Claude Code plugin format: generated plugin hooks reference $CLAUDE_PROJECT_DIR instead of ${CLAUDE_PLUGIN_ROOT} (so bundled hook scripts never resolve for plugin consumers), plugin-shipped subagents emit frontmatter fields upstream forbids for security (permissionMode, hooks, mcpServers), and plugin.json component-path overrides for commands/agents are ignored, so generated files can land in directories Claude Code will not scan.

Upstream baseline: Claude Code v2.1.220 (CHANGELOG). The last ~3 months of plugin-related changelog entries are bug fixes (v2.1.216 plugin-skill name prefix, v2.1.210 plugin MCP teardown, v2.1.206/v2.1.217 plugin LSP handling) — these are standing divergences from the current documented format rather than a fresh release rulesync missed.

Recent Releases

  • v2.1.219 — agent names containing : are rejected (reserved for plugin namespacing) — CHANGELOG.
  • v2.1.140+claude plugin list warns when a manifest path override causes a default component directory to be ignored — plugins reference.

Gaps

1. hooks — plugin hook commands are rewritten with $CLAUDE_PROJECT_DIR instead of ${CLAUDE_PLUGIN_ROOT}

  • Upstream: plugin hook commands resolve against the plugin's own install directory, verbatim example: "command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/format-code.sh"plugins reference, Hooks section. ${CLAUDE_PLUGIN_ROOT} is the plugin install dir; ${CLAUDE_PROJECT_DIR} is the consumer's project root.
  • rulesync (project): src/features/hooks/claudecode-plugin-hooks.ts only overrides getSettablePaths() and inherits the converter config from ClaudecodeHooks, including projectDirVar: "$CLAUDE_PROJECT_DIR" (src/features/hooks/claudecode-hooks.ts). Verified end to end: a canonical hook with "command": "./scripts/fmt.sh" generated into a plugin root produces "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/fmt.sh". The script ships inside the plugin, so for every consumer this expands to a nonexistent path in their repo and the hook silently fails. ${CLAUDE_PLUGIN_ROOT} appears nowhere in src/.

2. subagents — plugin-shipped agents emit frontmatter upstream forbids for security

  • Upstream, verbatim: "Plugin agents support name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter fields. The only valid isolation value is \"worktree\". For security reasons, hooks, mcpServers, and permissionMode are not supported for plugin-shipped agents." — plugins reference, Agents section.
  • rulesync (project): ClaudecodePluginSubagent inherits fromRulesyncSubagent from ClaudecodeSubagent, whose schema permits permissionMode, hooks, and mcpServers (src/features/subagents/claudecode-subagent.ts). A subagent with claudecode: { permissionMode: acceptEdits, mcpServers: {} } generated into a plugin root emits exactly that frontmatter into agents/*.md. Upstream discards these fields, so the author believes the plugin agent is permission-constrained when it is not — a misleading security posture, not just dead metadata. isolation is also unvalidated against the plugin-only "worktree" restriction.

3. commands / subagents — plugin.json component-path overrides are ignored

  • Upstream path behavior rules, verbatim: "Replaces the default: commands, agents, workflows, outputStyles, experimental.themes, experimental.monitors. For example, when the manifest specifies commands, the default commands/ directory is not scanned." (skills adds to the default scan; hooks/mcpServers/lspServers merge, so only commands and agents are affected.) — plugins reference, Path behavior rules.
  • rulesync (project): output directories are hardcoded (CLAUDECODE_PLUGIN_COMMANDS_DIR = "commands", CLAUDECODE_PLUGIN_AGENTS_DIR = "agents" in src/constants/plugin-paths.ts) and rulesync never reads the user's existing .claude-plugin/plugin.json. Generating into a plugin whose manifest declares {"agents": "./custom/agents/"} still writes agents/*.md, which Claude Code will not scan.

Proposed Follow-up

  1. In ClaudecodePluginHooks, override the converter config with projectDirVar: "$CLAUDE_PLUGIN_ROOT" (keeping prefixDotRelativeCommandsOnly: true), and teach the import direction to recognize ${CLAUDE_PLUGIN_ROOT} so plugin hooks round-trip. Consider the same substitution for plugin MCP server command values, currently passed through verbatim.
  2. In ClaudecodePluginSubagent, strip hooks, mcpServers, and permissionMode on generate (with a warning), constrain isolation to "worktree", and validate that agent names contain no : (v2.1.219).
  3. When .claude-plugin/plugin.json exists, honor a single-directory commands/agents override as the output root — or at minimum warn loudly when a replacing override points somewhere rulesync is not writing. Import should follow the same paths.

Non-gaps (verified)

  • Hook event coverage is current — DirectoryAdded and InstructionsLoaded are both in src/types/hooks.ts.
  • rules and permissions are correctly unsupported: upstream states a plugin-root CLAUDE.md is not loaded as project context, and plugin settings.json accepts only agent and subagentStatusLine.
  • Newer plugin surfaces (outputStyles, experimental.themes/monitors, lspServers, bin/, userConfig, channels, dependencies, marketplace.json) have no corresponding rulesync feature dimension — following them would be new features, not divergence fixes.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    consideringenhancementNew feature or requestmaintainer-scrapRough notes for AI implementation. Not for human eyes.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions