Skip to content
Development
Hook

Hooks

What pagokit runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
pagokit
708 skills1 agent8 commands5 hooks

Claude Code

> /plugin marketplace add Hainrixz/agente-pagokit
> /plugin install pagokit@tododeia

What fires, and when

PreToolUse

  • MatchesWrite|Edit|MultiEditnode $CLAUDE_PLUGIN_ROOT/hooks/pagokit-validate.js pre

PostToolUse

  • MatchesWrite|Edit|MultiEditnode $CLAUDE_PLUGIN_ROOT/hooks/pagokit-validate.js post

Stop

  • node $CLAUDE_PLUGIN_ROOT/hooks/pagokit-validate.js stop

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • node $CLAUDE_PLUGIN_ROOT/hooks/pagokit-validate.js prompt

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • node $CLAUDE_PLUGIN_ROOT/hooks/session-start.js
Read hooks/hooks.json

Where it lives

  • hooks/pagokit-validate.jsRunsGitHub
    Read the script
    #!/usr/bin/env node
    // Copyright (c) 2026 Enrique Rocha (@soyenriquerocha)
    // SPDX-License-Identifier: MIT
    // id: er-fee52fa9
    'use strict';
    
    /**
     * PagoKit — consolidated validator dispatcher.
     *
     * Invoked by Claude Code's hook system on PreToolUse, PostToolUse, and Stop.
     * Reads tool invocation JSON from stdin, runs the relevant checks, and emits
     * structured results to stderr (one JSON object per line).
     *
     * Exit codes:
     *   0 — all checks passed (no warnings or only warnings).
     *   2 — at least one check denied (tool call is blocked).
     *
     * Argv:
     *   node pagokit-validate.js <pre|post|stop>
     *
     * Stdin (Claude Code hook payload):
     *   {
     *     "session_id": "...",
     *     "tool_name": "Write" | "Edit" | "MultiEdit",
     *     "tool_input": {
     *       "file_path": "...",
     *       "content": "..." | "new_string": "..." | "edits": [...]
     *     },
     *     "tool_response": {...}   // PostToolUse only
     *   }
     */
    
    const fs = require('node:fs');
    const path = require('node:path');
    const { isAllowlistedTestFile, isSourceCodeFile, relPath } = require('./lib/utils');
    
    const CHECKS_DIR = path.join(__dirname, 'checks');
    
    const PHASE_CHECKS = {
      // PreToolUse: things that must be true BEFORE the write lands.
      pre: ['existing-webhook-check', 'gitignore-check'],
    
      // PostToolUse: the file now exists in full, so content analysis is meaningful.
      post: [
        // --- v0.1 core -------------------------------------------------------
        'webhook-has-signature',
        'no-hardcoded-keys',
        'idempotency-canonical',
        'raw-body',
        'no-pii-logs',
        // --- money correctness -----------------------------------------------
        'minor-units',
        'no-client-amount-trust',
        'currency-mismatch',
        'no-refund-on-irreversible',
        // --- webhook robustness ----------------------------------------------
        'webhook-event-allowlist',
        'webhook-secret-encoding',
        'webhook-secret-not-api-key',
        'webhook-fast-ack',
        // --- secrets and crypto ----------------------------------------------
        'no-plaintext-secret-compare',
        'secret-in-client-bundle',
        // --- PCI scope and card data -----------------------------------------
        'pci-script-integrity',
        'no-pan-in-transit',
        // --- business-model conflicts ----------------------------------------
        'mor-capability-conflict',
        'agentic-token-scope',
        'einvoice-obligation',
      ],
    
      // Stop: the last chance to catch something that only becomes visible once the whole
      // change is on disk. Incremental edits can pass individually and still leave the file
      // insecure, and network-txn-id-persistence genuinely needs cross-file state.
      stop: [
        'webhook-has-signature',
        'no-hardcoded-keys',
        'raw-body',
        'network-txn-id-persistence',
      ],
    
      // UserPromptSubmit: runs against the prompt text, not a file. Catches a live credential
      // pasted into the conversation before it reaches the transcript.
      prompt: ['prompt-preflight'],
    };
    
    /** The prompt phase has a different input shape and a different output contract. */
    async function runPromptPhase(payload) {
      const prompt = payload.prompt || payload.user_input || payload.user_message || '';
      if (!prompt) process.exit(0);
    
      for (const checkName of PHASE_CHECKS.prompt) {
        const checkPath = path.join(CHECKS_DIR, `${checkName}.js`);
        if (!fs.existsSync(checkPath)) continue;
        let result;
        try {
          result = require(checkPath).run({ prompt, phase: 'prompt' });
        } catch {
          continue; // a broken check must never block the user's prompt
        }
        if (!result) continue;
    
        if (result.level === 'deny') {
          // Exit 2 blocks the prompt; stderr is shown to the user.
          process.stderr.write(`${result.message_en}\n\n${result.suggested_fix}\n`);
          process.exit(2);
        }
        // A warning becomes context for the model rather than a block.
        process.stdout.write(JSON.stringify({
          hookSpecificOutput: {
            hookEventName: 'UserPromptSubmit',
            additionalContext: `[PagoKit] ${result.message_en}`,
          },
        }));
        process.exit(0);
      }
      process.exit(0);
    }
    
    async function readStdin() {
      return new Promise((resolve) => {
        let data = '';
        if (process.stdin.isTTY) {
          // No stdin attached (manual invocation for debugging)
          resolve(null);
          return;
        }
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', (chunk) => {
          data += chunk;
        });
        process.stdin.on('end', () => {
          if (!data.trim()) {
            resolve(null);
            return;
          }
          try {
            resolve(JSON.parse(data));
          } catch {
            resolve(null);
          }
        });
        // Safety timeout — never block longer than 2 seconds
        setTimeout(() => resolve(null), 2000).unref?.();
      });
    }
    
    function extractFileAndContent(toolName, toolInput) {
      if (!toolInput) return { filePath: null, content: null };
    
      // Write: { file_path, content }
      if (toolName === 'Write') {
        return {
          filePath: toolInput.file_path || null,
          content: toolInput.content || '',
        };
      }
    
      // Edit: { file_path, old_string, new_string }
      if (toolName === 'Edit') {
        return {
          filePath: toolInput.file_path || null,
          content: toolInput.new_string || '',
        };
      }
    
      // MultiEdit: { file_path, edits: [{old_string, new_string}, ...] }
      if (toolName === 'MultiEdit') {
        const edits = toolInput.edits || [];
        return {
          filePath: toolInput.file_path || null,
          content: edits.map((e) => e.new_string || '').join('\n'),
        };
      }
    
      return { filePath: null, content: null };
    }
    
    async function main() {
      const phase = process.argv[2];
      if (!PHASE_CHECKS[phase]) {
        // Unknown phase — exit cleanly (don't break the user)
        process.exit(0);
      }
    
      const payload = await readStdin();
    
      // If we can't read the payload, exit clean — no input means nothing to check
      if (!payload) {
        process.exit(0);
      }
    
      if (phase === 'prompt') {
        await runPromptPhase(payload);
        return;
      }
    
      const toolName = payload.tool_name;
      
  • hooks/session-start.jsRunsGitHub
    Read the script
    #!/usr/bin/env node
    // Copyright (c) 2026 Enrique Rocha (@soyenriquerocha)
    // SPDX-License-Identifier: MIT
    // id: er-fee52fa9
    'use strict';
    
    /**
     * SessionStart hook.
     *
     * PagoKit's entire differentiator is a set of rules the model must not forget. Compaction
     * drops them: after a long session the model no longer remembers that a refund on Pix is
     * fiction, or that JPY has no minor unit. This re-injects the non-negotiable subset on
     * startup, resume, clear, compact and fork.
     *
     * Deliberately short. A wall of text gets skimmed; twelve lines that each prevent a specific
     * money-losing bug do not. The full rules stay one Read away.
     *
     * stdin:  { session_id, reason: 'startup'|'resume'|'clear'|'compact'|'fork', ... }
     * stdout: { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: '...' } }
     */
    
    const fs = require('node:fs');
    const path = require('node:path');
    
    const PLUGIN_ROOT = path.resolve(__dirname, '..');
    const DATA = path.join(PLUGIN_ROOT, 'skills', 'payment-advisor', 'data');
    
    function readStdin(timeoutMs = 1500) {
      return new Promise((resolve) => {
        let buf = '';
        const done = (v) => { clearTimeout(timer); resolve(v); };
        const timer = setTimeout(() => done(''), timeoutMs);
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', (c) => { buf += c; });
        process.stdin.on('end', () => done(buf));
        process.stdin.on('error', () => done(''));
      });
    }
    
    function catalogSummary() {
      try {
        const idx = JSON.parse(fs.readFileSync(path.join(DATA, 'providers.index.json'), 'utf8'));
        const methods = JSON.parse(fs.readFileSync(path.join(DATA, 'methods.json'), 'utf8'));
        const currencies = JSON.parse(fs.readFileSync(path.join(DATA, 'currencies.json'), 'utf8'));
        const zero = currencies.currencies.filter((c) => c.exponent === 0).map((c) => c.code);
        const three = currencies.currencies.filter((c) => c.exponent === 3).map((c) => c.code);
        const irreversible = methods.methods
          .filter((m) => m.reversibility === 'irreversible_no_refund')
          .map((m) => m.id);
        return {
          count: idx.count,
          levels: idx.by_level,
          verified: idx.providers.filter((p) => p.webhook_confidence === 'high').length,
          zero, three, irreversible,
        };
      } catch {
        return null;
      }
    }
    
    async function main() {
      const raw = await readStdin();
      let reason = 'startup';
      try { reason = (JSON.parse(raw || '{}').reason) || 'startup'; } catch { /* keep default */ }
    
      const c = catalogSummary();
      if (!c) { process.exit(0); } // no catalog, nothing useful to say
    
      const lines = [
        'PagoKit is loaded. Non-negotiable rules for any payment code in this session:',
        '',
        '1. A webhook handler MUST verify the signature before parsing, over the RAW body. No exceptions.',
        '2. Signature verification alone does not stop replay. Use the timestamp window when the scheme signs a timestamp, and event-id dedup when it does not.',
        '3. Idempotency keys come from crypto.randomUUID() or the language equivalent. Never Math.random() or Date.now().',
        '4. Never hardcode a live key. Never write .env before .gitignore covers it.',
        '5. Never accept or store a PAN, CVV or track data on the merchant server. That is PCI SAQ D.',
        `6. Amounts follow the CURRENCY's ISO 4217 exponent, not a reflexive x100. Zero-decimal: ${c.zero.join(' ')}. Three-decimal: ${c.three.join(' ')}.`,
        `7. "Refund the charge" does not exist on irreversible rails (${c.irreversible.slice(0, 8).join(', ')}${c.irreversible.length > 8 ? ', …' : ''}). Emit a payout instead, and say so.`,
        '8. Log event.id, event.type and event.created. Never the payload.',
        '9. Verify with the WEBHOOK SECRET, not the API key. They are different values and mixing them fails silently.',
        '10. Compare signatures with timingSafeEqual / compare_digest, never ===.',
        '',
        `Catalog: ${c.count} providers (${(c.levels.build || []).length} build, ${(c.levels.generic || []).length} generic, ${(c.levels.advise || []).length} advise), ${c.verified} with a high-confidence webhook scheme.`,
        'Always tell the user which integration_level applies. If a scheme is not high-confidence, do NOT emit a signature verifier — emit re-fetch, dedup and a TODO.',
        '',
        'The validators run as hooks and will block a write that breaks rules 1-5. That is expected behaviour, not a bug: read the error and fix the code.',
      ];
    
      process.stdout.write(JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'SessionStart',
          additionalContext: lines.join('\n'),
        },
        systemMessage: reason === 'compact'
          ? 'PagoKit security rules re-injected after compaction.'
          : undefined,
      }));
      process.exit(0);
    }
    
    main().catch(() => process.exit(0)); // never break the session
    

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

@hainrixz
Ships withpagokit

Claude Code plugin that picks and implements the right payment method for your app — from your terminal. 42 providers, 136 payment rails, and deterministic validators that block insecure payment code as it is written.

708 skills1 agent
Get the whole plugin

Stars on GitHub

Momentum

stars in total
70
Star history of Hainrixz/agente-pagokit on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in pagokit.

Which coding agents can use the hooks in pagokit?
The hooks in pagokit install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.