ClaudeSuperPower
P

PreToolUse (mcp__(.*[Cc]arta.*|2827383e-1775-4df3-b6ff-04d5392f6d18|640916d9-5e0c-4d0b-830a-03500fa93e5e|1af3c326-9402-463c-ba5c-d62de59cf57b|8eb65db0-0cbd-4f50-833b-3057c150d41a|33b9b857-8443-4b2d-b191-2d9b6c50eb86)__.*)

Hook

Fires on

PreToolUsematchingmcp__(.*[Cc]arta.*|2827383e-1775-4df3-b6ff-04d5392f6d18|640916d9-5e0c-4d0b-830a-03500fa93e5e|1af3c326-9402-463c-ba5c-d62de59cf57b|8eb65db0-0cbd-4f50-833b-3057c150d41a|33b9b857-8443-4b2d-b191-2d9b6c50eb86)__.*

Immediately before a tool runs — this is where an action can be denied.

Where this sits in a session

  1. Session start
  2. Prompt submitted
  3. Prompt expansion
  4. Before tool use
  5. After tool use
  6. Notification
  7. Before compaction
  8. Subagent finished
  9. Turn finished
  10. Session end

Can blockThis hook is able to deny the action or send Claude back to work.

What it runs

Executed automatically when the event fires — no prompt, no confirmation.

command· timeout 5s
node "${CLAUDE_PLUGIN_ROOT}/scripts/hooks/inject-instrumentation.js"

Install it yourself

Paste into .claude/settings.json to run this hook without installing the whole plugin. Adjust the paths to point at your own copy of the scripts.

settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/inject-instrumentation.js\"",
            "timeout": 5
          }
        ],
        "matcher": "mcp__(.*[Cc]arta.*|2827383e-1775-4df3-b6ff-04d5392f6d18|640916d9-5e0c-4d0b-830a-03500fa93e5e|1af3c326-9402-463c-ba5c-d62de59cf57b|8eb65db0-0cbd-4f50-833b-3057c150d41a|33b9b857-8443-4b2d-b191-2d9b6c50eb86)__.*"
      }
    ]
  }
}

Source

Read this before installing — it runs on your machine with your permissions.

#!/usr/bin/env node
/**
 * PreToolUse hook: inject _instrumentation_v2 into Carta MCP tool calls.
 *
 * Every active Carta plugin fires this hook on the same tool call, and hooks'
 * updatedInput does not merge (last-writer-wins) — so with v1's per-plugin
 * _instrumentation key each plugin clobbered the others, dropping their signal.
 * v2 fixes this: each plugin writes its own record to a shared session registry,
 * then reads the whole registry and emits the fully-merged payload. Whichever
 * plugin's hook runs last still carries every plugin's data (KAF-2892).
 *
 * For tools that accept a params dict (fetch, mutate), the payload is nested in
 * params; other tools get it at the top level of tool_input. The MCP server
 * middleware extracts it for Kafka events and Datadog spans, then the gateway
 * strips it before command processing.
 *
 * Schema:
 *   _instrumentation_v2: {
 *     plugins:    { name: string, version: string }[]  — every active Carta plugin
 *     skills:     string[]  — union of loaded skills, namespaced "plugin:skill"
 *     session_id: string    — Claude Code session ID
 *     prompt_id:  string    — UUID of the user prompt currently being processed
 *     permission_mode: string — Claude Code's active permission mode
 *     effort:     string    — Claude Code's active reasoning effort level
 *     agent_id:   string    — subagent id; present only when the tool call originated inside a subagent
 *     model:      string    — Claude model id, captured at SessionStart (see capture-model.js)
 *     from_hook:  boolean   — always true; marks the payload as hook-emitted (vs the server's AI-generated fallback)
 *   }
 *
 * Part of the official Carta AI Agent Plugin.
 */

const fs = require('fs');
const os = require('os');
const path = require('path');

const PLUGIN = 'carta-crm';

// Read plugin.json for version
let pluginVersion = 'unknown';
try {
    const pluginJsonPath = path.resolve(__dirname, '../../.claude-plugin/plugin.json');
    const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
    pluginVersion = pluginJson.version || 'unknown';
} catch {}

// Session-scoped state written by track-active-skill.js (mirror its constant).
const STATE_DIR = process.env.CLAUDE_PLUGIN_DATA
    ? path.join(process.env.CLAUDE_PLUGIN_DATA, 'sessions')
    : '/tmp/claude-carta-crm';

// Sanitize a session id into a filesystem-safe path segment.
function sanitize(sessionId) {
    return String(sessionId || 'no-session').replace(/[^A-Za-z0-9._-]/g, '_');
}

// Read this plugin's loaded skills for the session (bare names, no prefix). Fail open to [].
function readSkills(sessionId) {
    if (!sessionId) return [];
    try {
        const p = path.join(STATE_DIR, `${sanitize(sessionId)}.json`);
        const s = JSON.parse(fs.readFileSync(p, 'utf8'));
        return Array.isArray(s.skills) ? s.skills : [];
    } catch { return []; }
}

// Cross-plugin merge: write this plugin's record to the shared session registry,
// then read every plugin's record and fold them into one v2 payload. Each hook
// emits the full union, so last-writer-wins never drops a plugin. Falls back to
// this plugin alone if the registry is unavailable.
function buildInstrumentationV2(sessionId, skills, promptId, permissionMode, effort, agentId) {
    const namespaced = skills.map(s => `${PLUGIN}:${s}`);
    const selfOnly = {
        plugins: [{ name: PLUGIN, version: pluginVersion }],
        skills: namespaced,
        session_id: sessionId || null,
        prompt_id: promptId || null,
        permission_mode: permissionMode || null,
        effort: effort || null,
        agent_id: agentId || null,
        model: null,
        from_hook: true,
    };
    try {
        const base = process.env.CARTA_INSTRUMENTATION_REGISTRY_DIR
            || path.join(os.tmpdir(), 'carta-instrumentation');
        const dir = path.join(base, sanitize(sessionId));
        fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(
            path.join(dir, `${PLUGIN}.json`),
            JSON.stringify({ plugin: PLUGIN, version: pluginVersion, skills: namespaced }),
        );

        const plugins = [];
        const mergedSkills = [];
        const seen = new Set();
        for (const f of fs.readdirSync(dir).sort()) {
            if (!f.endsWith('.json')) continue;
            try {
                const rec = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
                plugins.push({ name: rec.plugin || f.slice(0, -5), version: String(rec.version || '') });
                for (const s of Array.isArray(rec.skills) ? rec.skills : []) {
                    if (!seen.has(s)) { seen.add(s); mergedSkills.push(s); }
                }
            } catch {}
        }
        if (!plugins.length) return selfOnly;
        // Move the most-recently-invoked skill (the shared '.last-skill' marker written
        // cross-plugin by track-active-skill.js) to the end, so the server's positional
        // last_skill = skills[-1] signals recency, not alphabetical plugin order (KAF-2912).
        let last = '';
        try { last = fs.readFileSync(path.join(dir, '.last-skill'), 'utf8').trim(); } catch {}
        const i = last ? mergedSkills.indexOf(last) : -1;
        if (i > -1) mergedSkills.push(mergedSkills.splice(i, 1)[0]);
        // model is captured at SessionStart by capture-model.js (not on PreToolUse stdin).
        let model = null;
        try { model = fs.readFileSync(path.join(dir, '.model'), 'utf8').trim() || null; } catch {}
        return {
            plugins,
            skills: mergedSkills,
            session_id: sessionId || null,
            prompt_id: promptId || null,
            permission_mode: permissionMode || null,
            effort: effort || null,
            agent_id: agentId || null,
            model,
            from_hook: true,
        };
    } catch {
        return selfOnly;
    }
}

// Tools where _instrumentation_v2 goes inside the params dict (MCP gateway tools).
// fetch and mutate both accept a generic params dict; the Carta backend middleware
// extracts and strips _instrumentation_v2 before command processing.
// All other carta MCP tools receive it at the top level of tool_input.
const PARAMS_TOOLS = new Set(['fetch', 'mutate']);

let inputData = '';
process.stdin.on('data', chunk => (inputData += chunk));

process.stdin.on('end', () => {
    try {
        const input = JSON.parse(inputData);
        const { tool_name, tool_input, session_id, prompt_id, permission_mode, effort, agent_id } = input;

        // Extract the short tool name from mcp__<server>__<tool>
        const parts = (tool_name || '').split('__');
        const shortName = parts.length >= 3 ? parts[parts.length - 1] : tool_name;

        const instrumentation = buildInstrumentationV2(
            session_id, readSkills(session_id), prompt_id, permission_mode, effort, agent_id,
        );

        let updatedInput;

        if (PARAMS_TOOLS.has(shortName)) {
            // Gateway tools: inject inside params dict
            let params = tool_input.params;
            if (typeof params === 'string') {
                try {
                    params = JSON.parse(params);
                } catch {
                    params = {};
                }
            }
            params = params || {};
            params._instrumentation_v2 = instrumentation;
            updatedInput = { ...tool_input, params };
        } else {
            // Non-gateway tools (discover, welcome, list_accounts, etc.):
            // Fixed-signature — inject _instrumentation_v2 at the top level of tool_input
            // so the MCP framework middleware can capture skill/plugin/session context.
            updatedInput = { ...tool_input, _instrumentation_v2: instrumentation };
        }

        process.stdout.write(JSON.stringify({
            hookSpecificOutput: {
                hookEventName: 'PreToolUse',
                permissionDecision: 'allow',
                updatedInput,
            },
        }));
        process.exit(0);
    } catch (err) {
        // Never block a tool call due to instrumentation failure
        process.stderr.write(`inject-instrumentation error: ${err.message}\n`);
        allow();
    }
});

function allow() {
    process.stdout.write(JSON.stringify({
        hookSpecificOutput: {
            hookEventName: 'PreToolUse',
            permissionDecision: 'allow',
        },
    }));
    process.exit(0);
}

Shipped by 1 plugin

Installing any of these installs this hook.

Reviews

Log in to leave a review.

No reviews yet — be the first.

Explore related

Other things in this space — across every part of the ecosystem, not just hooks.