Skip to content
Development
Hook

Hooks

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

From plugin
lumin-repo-lens
263 skills9 commands4 hooks
Install
> /plugin marketplace add annyeong844/lumin-repo-lens
> /plugin install lumin-repo-lens@annyeong844-marketplace

Ships with lumin-repo-lens. Installing the plugin gets these hooks.

What fires, and when

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/user-prompt-submit.mjs"

PreToolUse

  • Matches*node "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs"

PostToolBatch

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-batch.mjs"

Stop

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/stop.mjs"
Read hooks/hooks.json

Where it lives

  • hooks/_runner-utils.mjsGitHub
    Read the script
    import { existsSync, readFileSync, writeFileSync } from 'node:fs';
    import path from 'node:path';
    import { fileURLToPath, pathToFileURL } from 'node:url';
    
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    const PLUGIN_ROOT = path.resolve(__dirname, '..');
    
    export function readJsonFromStdin() {
      let raw = '';
      try {
        raw = readFileSync(0, 'utf8');
      } catch {
        return null;
      }
      if (raw.trim().length === 0) return null;
      try {
        return JSON.parse(raw);
      } catch {
        return null;
      }
    }
    
    export function emitHookOutput(output) {
      if (!output || typeof output !== 'object' || Array.isArray(output)) return;
      writeFileSync(1, `${JSON.stringify(output)}\n`);
    }
    
    export async function importEngineModule(name) {
      const candidates = [
        path.join(PLUGIN_ROOT, '_lib', name),
        path.join(PLUGIN_ROOT, 'skills', 'lumin-repo-lens', '_engine', 'lib', name),
      ];
      for (const file of candidates) {
        if (!existsSync(file)) continue;
        return import(pathToFileURL(file).href);
      }
      throw new Error(`engine module not found: ${name}`);
    }
    
    export async function runHookMain(fn) {
      try {
        await fn();
      } catch {
        // Hooks are advisory. Unexpected failures must not block the host action.
      } finally {
        process.exitCode = 0;
      }
    }
    
  • hooks/post-tool-batch.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    
    import {
      emitHookOutput,
      importEngineModule,
      readJsonFromStdin,
      runHookMain,
    } from './_runner-utils.mjs';
    
    await runHookMain(async () => {
      const payload = readJsonFromStdin();
      if (!payload) return;
    
      const { processPostWriteLite } = await importEngineModule('hook-post-write-lite.mjs');
      const result = processPostWriteLite(payload);
      emitHookOutput(result.output);
    });
    
  • hooks/pre-tool-use.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    
    import {
      emitHookOutput,
      importEngineModule,
      readJsonFromStdin,
      runHookMain,
    } from './_runner-utils.mjs';
    
    const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
    
    await runHookMain(async () => {
      const payload = readJsonFromStdin();
      if (!payload) return;
    
      const cwd = typeof payload.cwd === 'string' ? payload.cwd : process.cwd();
      const {
        getToolTargetPath,
        resolveAuditRoot,
        safeRepoPathForToolInput,
      } = await importEngineModule('hook-path-safety.mjs');
      const {
        safeSessionId,
        safeToolUseId,
      } = await importEngineModule('hook-id-safety.mjs');
      const { capturePreimage } = await importEngineModule('hook-preimage-store.mjs');
      const { drainDueEventReminders } = await importEngineModule('hook-event-drain.mjs');
    
      const auditRoot = resolveAuditRoot(cwd);
      if (!auditRoot) return;
      const sid = safeSessionId(payload);
    
      if (MUTATING_TOOLS.has(payload.tool_name)) {
        try {
          const targetPath = getToolTargetPath(payload.tool_name, payload.tool_input ?? {});
          const safe = safeRepoPathForToolInput(cwd, targetPath);
          if (safe.ok) {
            const tid = safeToolUseId({
              tool_use_id: payload.tool_use_id,
              tool_name: payload.tool_name,
              tool_input: payload.tool_input ?? {},
            });
            capturePreimage({ auditRoot, sid, tid, safe });
          }
        } catch {
          // Continue to drain reminders even when preimage capture cannot proceed.
        }
      }
    
      const drain = drainDueEventReminders(auditRoot, sid, {
        hookEventName: 'PreToolUse',
      });
      emitHookOutput(drain.output);
    });
    
  • hooks/stop.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    
    import {
      importEngineModule,
      readJsonFromStdin,
      runHookMain,
    } from './_runner-utils.mjs';
    
    await runHookMain(async () => {
      const payload = readJsonFromStdin();
      if (!payload) return;
    
      const cwd = typeof payload.cwd === 'string' ? payload.cwd : process.cwd();
      const { resolveAuditRoot } = await importEngineModule('hook-path-safety.mjs');
      const { safeSessionId } = await importEngineModule('hook-id-safety.mjs');
      const { observeStopAcknowledgements } = await importEngineModule('hook-ack-observer.mjs');
    
      const auditRoot = resolveAuditRoot(cwd);
      if (!auditRoot) return;
      observeStopAcknowledgements(auditRoot, safeSessionId(payload), payload);
    });
    
  • hooks/user-prompt-submit.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    
    import {
      emitHookOutput,
      importEngineModule,
      readJsonFromStdin,
      runHookMain,
    } from './_runner-utils.mjs';
    
    await runHookMain(async () => {
      const payload = readJsonFromStdin();
      if (!payload) return;
    
      const cwd = typeof payload.cwd === 'string' ? payload.cwd : process.cwd();
      const { resolveAuditRoot } = await importEngineModule('hook-path-safety.mjs');
      const { safeSessionId } = await importEngineModule('hook-id-safety.mjs');
      const { drainDueEventReminders } = await importEngineModule('hook-event-drain.mjs');
    
      const auditRoot = resolveAuditRoot(cwd);
      if (!auditRoot) return;
      const drain = drainDueEventReminders(auditRoot, safeSessionId(payload), {
        hookEventName: 'UserPromptSubmit',
      });
      emitHookOutput(drain.output);
    });
    

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.

Ships withlumin-repo-lens

🇰🇷 한국어로 읽으시려면 → README.ko.md  ·  🇬🇧 English continues below. The kind little buddy that says "this already exists" before you write it again. Your repo's companion for vibe-coding sessions.

Get the whole plugin