Skip to content

Plugin userConfig options are not expanded in http hook headers (${CLAUDE_PLUGIN_OPTION_*} resolves empty) #81742

Description

@T0mSIlver

Summary

Plugin userConfig options are not available to ${VAR} expansion in http hook headers. A header like Authorization: Bearer ${CLAUDE_PLUGIN_OPTION_TOKEN} (with allowedEnvVars: ["CLAUDE_PLUGIN_OPTION_TOKEN"]) expands to an empty string even when the option is set, so the request is sent as Authorization: Bearer. Expansion draws only from the process environment — plugin option values are never injected for http hooks.

The docs say plugin option values are "exported to hook processes as CLAUDE_PLUGIN_OPTION_<KEY> environment variables" (plugins reference). That holds for command hooks (subprocesses), but http hooks are POSTed by Claude Code itself, and there the variables don't exist. This makes it impossible for a plugin to declare an authenticated http hook — which is otherwise the perfect shape for a "no binary on this machine" integration.

Environment

  • Claude Code 2.1.220, Linux (also reproduced on 2.1.2xx earlier builds)
  • Plugin installed with claude plugin install name@marketplace --config 'token=<value>'; value confirmed stored under pluginSecrets in ~/.claude/.credentials.json (the option is sensitive: true)

Minimal reproduction

.claude-plugin/plugin.json:

{
  "name": "authtest",
  "version": "1.0.0",
  "description": "repro",
  "userConfig": {
    "token": { "type": "string", "sensitive": true, "title": "t", "description": "t", "default": "" }
  }
}

hooks/hooks.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "http://127.0.0.1:8474/v1/hook/UserPromptSubmit",
            "headers": { "Authorization": "Bearer ${CLAUDE_PLUGIN_OPTION_TOKEN}" },
            "allowedEnvVars": ["CLAUDE_PLUGIN_OPTION_TOKEN"],
            "timeout": 3
          }
        ]
      }
    ]
  }
}

Capture server: any local HTTP server on 8474 that logs the Authorization header.

claude plugin install authtest@testmkt --config 'token=capture-test-secret-42'
claude -p "reply with just: ok"

Observed

The capture server receives:

Authorization: Bearer

The configured option value is never sent. Control experiment — putting the variable in the actual process environment DOES expand:

CLAUDE_PLUGIN_OPTION_TOKEN=from-process-env claude -p "reply with just: ok"
# capture server receives: Authorization: Bearer from-process-env

So allowedEnvVars + header expansion work as documented for real environment variables; the missing piece is that plugin userConfig values are not part of that namespace for http hooks.

Expected

One of:

  1. CLAUDE_PLUGIN_OPTION_<KEY> (and/or ${user_config.KEY}) is resolvable in a plugin-declared http hook's headers, consistent with the plugins-reference wording that option values are exported to hook processes; or
  2. The hooks/plugins docs explicitly state that http hooks cannot access plugin options, so plugin authors don't ship auth designs that silently send empty credentials.

Option 1 is strongly preferred: header interpolation is not shell evaluation, so the injection concern that motivated rejecting ${user_config.*} in shell-form fields (see #76567 and the v2.1.207 change) doesn't apply — the value lands in an HTTP header byte-for-byte.

Real-world impact

Our plugin (localvoxtral-remote, localvoxtral) publishes session context from an SSH remote host to a loopback listener through a RemoteForward tunnel, authenticated by a per-host bearer token issued at enrollment. Declarative http hooks meant nothing to install on the remote host beyond the plugin itself. Every hook fires with an empty bearer and is rejected 401; users see UserPromptSubmit hook error: HTTP 401 on every prompt. We're converting to a command hook with curl as a workaround, which reintroduces a runtime dependency the http hook type exists to avoid.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions