· 5 min read ·

Claude Code's Hook System Has a Permission Gap That Lets Plugins Read Your Prompts

Source: hackernews

A post by Akshay Chugh documented something uncomfortable about Vercel’s Claude Code plugin: it was capturing user prompts through the hooks system and forwarding that data to Vercel’s telemetry infrastructure. The post landed on Hacker News with enough traction to prompt a response. Vercel updated the plugin, framed the collection as opt-out telemetry, and the specific incident resolved fairly quickly, but the mechanism that enabled it is unchanged.

How Claude Code’s Hook System Works

Claude Code’s hooks let you register shell commands that fire at specific points in the agent lifecycle. The official documentation lists five event types:

  • PreToolUse: fires before a tool call, receives tool name and inputs
  • PostToolUse: fires after a tool completes, receives the result
  • Notification: fires on user-visible messages
  • Stop: fires when the agent finishes a turn
  • UserPromptSubmit: fires when the user submits a message

Hooks are configured in .claude/settings.json or the global ~/.claude/settings.json. Here is what a basic hook configuration looks like:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Each hook runs as a subprocess with context passed via environment variables: $CLAUDE_TOOL_INPUT_COMMAND for shell commands, $CLAUDE_TOOL_INPUT_FILE_PATH for file operations, and similar variables depending on the event type. The subprocess inherits the user’s full environment and runs with their OS permissions.

PreToolUse hooks have one additional capability: if the process exits with code 2, Claude Code cancels the pending tool call. A PreToolUse hook can block a destructive git push or a rm -rf on a protected path, and the agent cannot override it by rephrasing the request. This is the enforcement mechanism that makes hooks qualitatively different from CLAUDE.md instructions, which are advisory.

The UserPromptSubmit Hook Specifically

UserPromptSubmit fires before the user’s message reaches Anthropic’s API. The hook subprocess receives the prompt text, and it can do anything with that text that a subprocess can do: write to a file, call a remote API, pass it through a filter, or forward it elsewhere. The hook’s stdout gets injected into the agent’s context, which is how prompt modification or augmentation works in legitimate use cases.

The Vercel plugin registered a UserPromptSubmit hook as part of its installation. That hook captured the prompt text and sent it to Vercel’s telemetry endpoints. The plugin’s primary purpose, providing Vercel deployment tools inside Claude Code, is legitimate. The telemetry collection happened alongside it, without clear disclosure at install time.

Where the Permission Model Ends

Claude Code’s permission system handles tool invocations through allow and deny rules in settings.json:

{
  "permissions": {
    "allow": ["Bash(git *)", "Read", "Write"],
    "deny": ["Bash(curl *)", "Bash(wget *)"]
  }
}

These rules constrain what Claude’s agent process can invoke. An agent with Bash(curl *) denied cannot use curl in a tool call; Claude Code intercepts the attempt and blocks it.

Hook processes are not the agent process. They are subprocesses that Claude Code spawns, and once spawned, Claude Code has no further control over what they do. A deny rule on Bash(curl *) has no authority over what a UserPromptSubmit hook does internally. If the hook calls curl to send prompt data to a remote server, that call runs with the user’s full OS permissions, entirely outside Claude Code’s permission enforcement. The permission model that makes the agent’s tool use relatively auditable simply does not reach hook behavior.

This is a design boundary, not an implementation gap. Hooks exist specifically to give integrations capabilities that the agent itself cannot or should not exercise directly. The tradeoff is that those capabilities are unrestricted by design.

The Installation Vector

The Vercel plugin distributes via npm. Whether run directly with npx or added as a project dependency, the package includes a postinstall script that modifies .claude/settings.json to register the MCP server and hook configuration. The npm postinstall lifecycle event runs automatically on installation, with the user’s permissions, before any manual review of what the script does.

This is not a new surface. The event-stream incident in 2018 and ua-parser-js in 2021 both used postinstall scripts to execute behavior the user would not have authorized explicitly. The Vercel plugin is categorically different from those incidents, since the telemetry is collected by the package’s own publisher rather than by a supply chain attacker who compromised the package. The installation mechanism is structurally the same.

There is a practical complication for teams. The Claude Code documentation recommends committing .claude/settings.json to the repository for sharing project-level configuration. An install done by one developer propagates the hook configuration, including any telemetry hooks, to every team member who pulls the repo. A team member who never ran the Vercel plugin install still has the hook registered in their settings if the file is committed.

Why Prompt Data Is Particularly Sensitive

The data UserPromptSubmit captures is distinct from what other hooks collect. PreToolUse and PostToolUse hooks access tool invocations and results, which are visible in Claude Code’s interface and represent actions the agent is taking against files, the shell, or external services. These events correspond to observable, loggable operations.

UserPromptSubmit hooks get the verbatim text of every message the user sends, before it appears in any output log. Claude Code sessions contain a wide range of sensitive material in practice: proprietary code being debugged, architectural questions about internal systems, queries that include error messages with internal service names and stack traces, and occasionally prompts that inadvertently include credentials or tokens. The hook receives all of it, for every message, with no indication in the interface that capture is occurring.

Vercel’s stated rationale is usage analytics for improving their integration. That goal is standard across developer tooling. The disclosure gap is that users installing a plugin for Vercel deployment commands have no reason to expect that prompt capture is part of the arrangement.

What This Means for the Plugin Ecosystem

The Vercel case is significant not because of Vercel specifically, but because the mechanism is generic. Any package that can modify .claude/settings.json can register a UserPromptSubmit hook. Any UserPromptSubmit hook receives every prompt the user types. The permission model does not constrain what the hook does with that data. This is a structural property of the current architecture, not a configuration mistake that individual users or vendors can resolve on their own.

A few approaches would help close the gap. Displaying registered hooks in a user-visible audit surface inside Claude Code, separate from the raw settings.json file, would at least make prompt capture visible without requiring users to parse JSON. Treating outbound network requests from hook subprocesses as requiring explicit authorization, analogous to how browser extensions declare manifest permissions before accessing sensitive APIs, would bring hook network behavior into the same consent model as tool invocations. Neither of these changes would eliminate the risk entirely, but they would make prompt capture legible rather than silent.

The hooks system is genuinely useful for teams building development workflows on top of Claude Code. Enforcing test runs after file edits, injecting dynamic context at the prompt layer, auditing shell commands before execution: these patterns have real leverage. Extending the permission model to cover hook behavior would preserve that utility while making the data exposure visible before it becomes an incident.

The Vercel plugin situation resolved quickly, which is the best-case outcome for this kind of disclosure. The hook architecture it exposed is still there, available to every plugin that touches .claude/settings.json.

Was this interesting?