[DOCS] Plugin `user_config` documentation still advertises unsafe shell-form interpolation
Documentation Type
Incorrect/outdated documentation
Documentation Location
https://code.claude.com/docs/en/plugins-reference
Section/Topic
The plugin Monitors example, the userConfig substitution rules, and the cross-reference from hook command execution.
Current Documentation
The plugin reference currently gives this monitor example:
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/poll-deploy.sh ${user_config.api_endpoint}"
It also says:
“Thecommandvalue supports the same variable substitutions as MCP and LSP server configs:${CLAUDE_PLUGIN_ROOT},${CLAUDE_PLUGIN_DATA},${CLAUDE_PROJECT_DIR},${user_config.*}, and any${ENV_VAR}from the environment.”
The userConfig section says:
“Each value is available for substitution as${user_config.KEY}in MCP and LSP server configs, hook commands, and monitor commands. … All values are exported to plugin subprocesses asCLAUDE_PLUGIN_OPTION_<KEY>environment variables.”
The Hooks page likewise says:
“Plugin hooks additionally substitute ${user_config.*} values.”
What's Wrong or Missing?
Claude Code v2.1.207 rejects ${user_config.*} in shell-form plugin hook, monitor, and MCP headersHelper commands as a shell-injection fix. The current examples and substitution rules still tell plugin authors to place these values directly in command strings, so following the published example can make a plugin fail after upgrading or encourage unsafe interpolation.
The release also gives different safe migration paths by component: hooks can use exec form with an args array or read $CLAUDE_PLUGIN_OPTION_<KEY>, while monitors and headersHelper should read the value inside the script, such as from the plugin configuration file or the MCP server's env block. The current docs do not explain this distinction.
Suggested Improvement
Replace the monitor example with a safe form that passes no user-controlled value through a shell command string and shows how the monitor script reads the corresponding plugin option. Update the substitution rules to state that ${user_config.*} is not accepted in shell-form commands for plugin hooks, monitors, or MCP headersHelper commands.
In the hooks section, show an exec-form example with args or explicitly read CLAUDE_PLUGIN_OPTION_<KEY> in the script. In the MCP headersHelper section, document the supported configuration path for plugin options, including reading the value inside the helper from the server's env block where appropriate. Add a migration note keyed to Claude Code v2.1.207 so plugin authors can update existing manifests safely.
Impact
High - Prevents users from using a feature
Additional Context
Affected Pages:
| Page | Context |
|------|---------|
| https://code.claude.com/docs/en/plugins-reference | Monitor example and general userConfig substitution rules |
| https://code.claude.com/docs/en/hooks | Plugin hook substitution and exec-form/shell-form guidance |
| https://code.claude.com/docs/en/mcp | Dynamic headersHelper configuration and plugin-provided MCP servers |
Total scope: 3 pages affected
Source: Claude Code v2.1.207
Plugin hooks/monitors/MCP headersHelper:${user_config.*}in shell-form commands is now rejected (shell-injection fix). Hooks: use exec form (argsarray) or$CLAUDE_PLUGIN_OPTION_<KEY>; monitors and headersHelper: read the value inside the script (config file or the server'senvblock).