HTTP Hooks and MCP Tool Hooks
- Configure an HTTP hook to POST tool call data to an external validation service and handle its response
- Secure HTTP hook headers using environment variable interpolation and the allowedHttpHookUrls allowlist
- Wire an MCP tool hook to call a server tool with template substitution in the input fields
When to Reach for HTTP and MCP Tool Hooks
Command hooks are the right starting point for most hook logic. But two situations push you toward HTTP hooks or MCP tool hooks instead.
The first is existing infrastructure. If your organization already has a security scanning service, a policy enforcement API, a centralized audit logging endpoint, or a compliance checker, duplicating that logic in shell scripts creates maintenance burden and drift. HTTP hooks let you call the existing service directly from the hook event, keeping the logic in one place.
The second is shared MCP capabilities. If a connected MCP server already has a tool that does what you need — a custom validator, a notification sender, a database lookup — there is no reason to shell out to an external process. MCP tool hooks call that tool directly.
Both handler types receive the same JSON payload as command hooks. The difference is in how they send it and how they receive the response.
HTTP Hooks: Request and Response Format
An HTTP hook sends the hook payload as a JSON POST to a URL. The POST body is exactly the same JSON object that a command hook would receive on stdin — the same common fields, the same tool-specific fields, the same structure. If you have a command hook that parses stdin JSON, you can move its logic to an HTTP service with minimal changes.
A minimal HTTP hook configuration:
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30
}
The service at that URL receives the POST, evaluates the payload, and returns a response. Claude Code handles the response based on HTTP status and content type.
A 2xx response with JSON body is treated as a structured decision, identical to command hook stdout on exit 0. The JSON can contain hookSpecificOutput with a permission decision, additionalContext for Stop hooks, or any other structured output the event supports.
A 2xx response with text body is treated as success. The text is added as context but does not contain a structured decision.
A 2xx response with empty body is treated as a no-op success. The hook ran, everything is fine, proceed.
A non-2xx response or a timeout is treated as a non-blocking error. The hook failed, but Claude Code does not block the action. This is intentional — a broken validation service should not bring down your entire development workflow. If you need failures to block, wrap your HTTP call in a command hook that exits 2 when the HTTP request fails.
Securing HTTP Hooks
HTTP hooks can include custom headers, which is how you pass authentication tokens to protected services. The header value can reference environment variables using $VAR_NAME syntax:
{
"type": "http",
"url": "https://security.internal/validate",
"headers": {
"Authorization": "Bearer $SECURITY_TOKEN",
"X-Project": "my-project"
},
"allowedEnvVars": ["SECURITY_TOKEN"]
}
The allowedEnvVars field on the hook declares which environment variables the hook is permitted to interpolate. Without this declaration, the variable reference is not expanded. This prevents hooks from silently leaking environment variables by referencing them in header values.
At the settings level, httpHookAllowedEnvVars provides an organization-wide allowlist. The effective set of allowed variables for any hook is the intersection of the hook's allowedEnvVars and the settings-level httpHookAllowedEnvVars (if the latter is set).
allowedHttpHookUrls in settings restricts which URLs HTTP hooks can target. An empty array blocks all HTTP hooks. A list of URL patterns with * as a wildcard restricts hooks to matching URLs. An undefined value places no restriction. Use this setting in managed configurations to prevent hooks from POSTing data to arbitrary external services.
MCP Tool Hooks: Calling Server Tools
An MCP tool hook calls a tool on a connected MCP server, using the server's tool protocol. The configuration requires three fields: server (the configured server name), tool (the tool name on that server), and optionally input (the arguments to pass).
{
"type": "mcp_tool",
"server": "security-scanner",
"tool": "scan_file",
"input": {
"file_path": "${tool_input.file_path}",
"project": "${CLAUDE_PROJECT_DIR}"
}
}
The input field supports template substitution using ${fieldName} syntax. You can reference any field from the hook's input payload — ${tool_input.command} for the bash command, ${tool_input.file_path} for the file being edited, ${cwd} for the working directory, and path placeholders like ${CLAUDE_PROJECT_DIR}.
The MCP tool's output is treated the same way as command hook stdout. If the tool returns valid JSON, Claude Code parses it for structured decisions. If it returns text, the text is added as context.
For plugin-scoped MCP servers, the server name uses the format plugin:pluginName:serverName. A tool on the database server within the my-analytics plugin would be addressed as server: "plugin:my-analytics:database".
A Local Validation Service Pattern
One practical use of HTTP hooks is running a local validation service that aggregates multiple checks. Rather than chaining many command hooks that each run independently, you run a single HTTP service that receives the hook payload and returns a consolidated decision.
A tiny Node.js service that validates Bash commands:
const http = require('http');
http.createServer((req, res) => {
let body = '';
req.on('data', d => body += d);
req.on('end', () => {
const data = JSON.parse(body);
const cmd = data.tool_input?.command || '';
if (/rms+-[a-z]*r[a-z]*f/i.test(cmd) || /curl.*|s*bash/.test(cmd)) {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: 'Command matches security policy block list.'
}
}));
} else {
res.writeHead(200); res.end();
}
});
}).listen(8080);
This service can be extended with database lookups, policy evaluations, rate limiting, or any logic that would be awkward in a shell script. Start it in a SessionStart hook so it is available throughout the session.
- HTTP hooks send the same JSON payload as command hooks — the POST body is identical to what command hooks receive on stdin, making the transition from command to HTTP straightforward
- Non-2xx responses are non-blocking by default — a failed HTTP hook does not stop the tool call; wrap in a command hook if you need failure to block
- Environment variable interpolation in headers uses $VAR_NAME syntax — combine with allowedEnvVars on the hook and httpHookAllowedEnvVars in settings for secure token passing
- MCP tool hooks use ${tool_input.field} substitution in their input — pass the tool arguments directly to the MCP tool without a shell script intermediary
- allowedHttpHookUrls is a security allowlist — an empty array blocks all HTTP hooks; undefined means no restriction; use pattern matching with * for flexible URL controls