Skip to content
Development
Hook

Hooks

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

From plugin
edgeone-makers-tools
1.9k11 skills1 hook
Install
> /plugin marketplace add tencentedgeone/edgeone-pages-skills
> /plugin install edgeone-makers-tools@edgeone-makers

Ships with edgeone-makers-tools. Installing the plugin gets these hooks.

What fires, and when

PreToolUse

  • MatchesEdit|Write|replace_in_file|write_to_filenode "${CLAUDE_PLUGIN_ROOT}/hooks/validate-write.mjs"
Read hooks/hooks.json

Where it lives

  • hooks/signal-log.mjsGitHub
    Read the script
    import { appendFileSync, mkdirSync } from 'node:fs';
    import { dirname, join } from 'node:path';
    
    const REQUIRED_SIGNAL_FIELDS = ['hook', 'trigger', 'matchedSkill', 'reason'];
    
    export function defaultSignalLogPath(env = process.env, cwd = process.cwd()) {
      return env.EDGEONE_MAKERS_SIGNAL_LOG || join(cwd, '.edgeone', 'signal-log.jsonl');
    }
    
    function normalizeSignalLogEntry(entry, now = new Date()) {
      for (const field of REQUIRED_SIGNAL_FIELDS) {
        if (!entry?.[field]) {
          throw new Error(`Missing signal log field: ${field}`);
        }
      }
    
      const normalized = {
        timestamp: now.toISOString(),
        hook: entry.hook,
        trigger: entry.trigger,
        matchedSkill: entry.matchedSkill,
        reason: entry.reason,
      };
    
      if (entry.platform) normalized.platform = entry.platform;
      if (entry.toolName) normalized.toolName = entry.toolName;
    
      return normalized;
    }
    
    export function shouldWriteSignalLog(options = {}) {
      return Boolean(options.enableSignalLog || options.signalLogPath);
    }
    
    export function writeSignalLog(entry, options = {}) {
      const normalized = normalizeSignalLogEntry(entry, options.now);
      const logPath = options.logPath || options.signalLogPath || defaultSignalLogPath();
    
      mkdirSync(dirname(logPath), { recursive: true });
      appendFileSync(logPath, `${JSON.stringify(normalized)}\n`);
    
      return normalized;
    }
    
  • hooks/signal-log.test.mjsGitHub
    Read the script
    import assert from 'node:assert/strict';
    import { mkdtemp, readFile, rm } from 'node:fs/promises';
    import { tmpdir } from 'node:os';
    import { join } from 'node:path';
    import test from 'node:test';
    
    import { writeSignalLog } from './signal-log.mjs';
    
    test('plugin-skill-injection-optimization.SIGNAL_LOGGING.1 appends signal entries to JSONL', async () => {
      const tmp = await mkdtemp(join(tmpdir(), 'makers-signal-log-'));
      const logPath = join(tmp, '.edgeone', 'signal-log.jsonl');
    
      try {
        writeSignalLog(
          {
            hook: 'PreToolUse',
            trigger: 'pathPatterns',
            matchedSkill: 'makers-edge-functions',
            reason: 'functions/index.ts matched functions/**',
            platform: 'claude-code',
            toolName: 'Read',
          },
          { logPath, now: new Date('2026-06-24T00:00:00.000Z') },
        );
    
        const [line] = (await readFile(logPath, 'utf8')).trim().split('\n');
    
        assert.deepEqual(JSON.parse(line), {
          timestamp: '2026-06-24T00:00:00.000Z',
          hook: 'PreToolUse',
          trigger: 'pathPatterns',
          matchedSkill: 'makers-edge-functions',
          reason: 'functions/index.ts matched functions/**',
          platform: 'claude-code',
          toolName: 'Read',
        });
      } finally {
        await rm(tmp, { recursive: true, force: true });
      }
    });
    
    test('plugin-skill-injection-optimization.SIGNAL_LOGGING.2 requires the core signal fields', () => {
      assert.throws(
        () =>
          writeSignalLog({
            hook: 'PreToolUse',
            trigger: 'pathPatterns',
            matchedSkill: 'makers-edge-functions',
          }),
        /Missing signal log field: reason/,
      );
    });
    
  • hooks/validate-write.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    import { readFileSync, readdirSync } from 'node:fs';
    import { dirname, join } from 'node:path';
    import { fileURLToPath, pathToFileURL } from 'node:url';
    
    import { shouldWriteSignalLog, writeSignalLog } from './signal-log.mjs';
    
    const HOOKS_DIR = dirname(fileURLToPath(import.meta.url));
    // Single-skill layout: capabilities (each carrying its own validate rules in
    // frontmatter) live under the one skill's references/ directory.
    const DEFAULT_SKILLS_DIR = join(HOOKS_DIR, '..', 'skills', 'edgeone-makers-tools', 'references');
    const WRITE_TOOL_NAMES = new Set(['Edit', 'Write', 'replace_in_file', 'write_to_file']);
    const WRITE_CONTENT_KEYS = ['content', 'new_string', 'new_str', 'newString', 'text'];
    const PATH_KEYS = ['file_path', 'filePath', 'path', 'target_file'];
    
    let cachedRules = null;
    
    function escapeRegExp(value) {
      return value.replace(/[|\\{}()[\]^$+?.*]/g, '\\$&');
    }
    
    function globToRegExp(pattern) {
      const source = String(pattern)
        .replace(/\\/g, '/')
        .split('/')
        .map((segment) => (segment === '**' ? '.*' : escapeRegExp(segment).replace(/\\\*/g, '[^/]*')))
        .join('/');
      return new RegExp(`(^|/)${source}$`);
    }
    
    function parseFrontmatter(content) {
      const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
      return match ? match[1] : '';
    }
    
    function parseYamlScalar(value) {
      const trimmed = String(value || '').trim();
      if (!trimmed) return '';
      if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
        try {
          return JSON.parse(trimmed);
        } catch {
          return trimmed.slice(1, -1);
        }
      }
      if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
        return trimmed.slice(1, -1).replace(/''/g, "'");
      }
      return trimmed;
    }
    
    function parseFrontmatterString(frontmatter, key) {
      const match = new RegExp(`^${key}:\\s*(.+)$`, 'm').exec(frontmatter);
      return match ? parseYamlScalar(match[1]) : '';
    }
    
    function parseFrontmatterList(frontmatter, key) {
      const lines = frontmatter.split(/\r?\n/);
      const values = [];
      let inList = false;
      for (const line of lines) {
        if (!inList) {
          inList = new RegExp(`^${key}:\\s*$`).test(line);
          continue;
        }
        if (!line.trim()) continue;
        if (/^\S/.test(line)) break;
        const item = /^\s+-\s*(.+?)\s*$/.exec(line);
        if (item) values.push(parseYamlScalar(item[1]));
      }
      return values;
    }
    
    function assignObjectField(object, text) {
      const field = /^([A-Za-z][\w-]*):\s*(.*?)\s*$/.exec(text);
      if (!field) return;
      object[field[1]] = parseYamlScalar(field[2]);
    }
    
    function parseFrontmatterObjectList(frontmatter, key, requiredFields = ['pattern', 'message']) {
      const lines = frontmatter.split(/\r?\n/);
      const values = [];
      let current = null;
      let inList = false;
      for (const line of lines) {
        if (!inList) {
          inList = new RegExp(`^${key}:\\s*$`).test(line);
          continue;
        }
        if (!line.trim()) continue;
        if (/^\S/.test(line)) break;
        const item = /^\s+-\s*(.*?)\s*$/.exec(line);
        if (item) {
          current = {};
          values.push(current);
          if (item[1]) assignObjectField(current, item[1]);
          continue;
        }
        if (current) assignObjectField(current, line.trim());
      }
      return values.filter((value) => requiredFields.every((field) => value[field]));
    }
    
    function parseSkillValidateRule(skillPath) {
      const frontmatter = parseFrontmatter(readFileSync(skillPath, 'utf8'));
      const skill = parseFrontmatterString(frontmatter, 'name');
      if (!skill) return null;
      const validate = parseFrontmatterObjectList(frontmatter, 'validate');
      if (validate.length === 0) return null;
      return {
        skill,
        pathPatterns: parseFrontmatterList(frontmatter, 'pathPatterns'),
        validate,
      };
    }
    
    /**
     * 读取各能力声明的 validate 规则。
     *
     * 读不到就返回空数组,绝不抛错:本函数跑在 PreToolUse 钩子里,
     * 模型每写一个文件都会经过它。references/ 不存在(部分安装、
     * CLAUDE_PLUGIN_ROOT 解析错位)时若抛 ENOENT,用户每次写文件都会看到一次报错。
     * 校验器失效的正确表现是「不提醒」,而不是「报错」。
     */
    export function loadSkillValidateRules(skillsDir = DEFAULT_SKILLS_DIR) {
      if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules;
      let entries;
      try {
        entries = readdirSync(skillsDir, { withFileTypes: true });
      } catch {
        return [];
      }
      const rules = entries
        .filter((entry) => entry.isDirectory())
        .map((entry) => join(skillsDir, entry.name, 'SKILL.md'))
        .map((skillPath) => {
          try {
            return parseSkillValidateRule(skillPath);
          } catch {
            return null;
          }
        })
        .filter(Boolean);
      if (skillsDir === DEFAULT_SKILLS_DIR) cachedRules = rules;
      return rules;
    }
    
    function getToolName(payload) {
      return String(payload?.tool_name || payload?.toolName || '').trim();
    }
    
    function getToolInput(payload) {
      return payload?.tool_input || payload?.toolInput || {};
    }
    
    function getToolPath(toolInput) {
      const raw = PATH_KEYS.map((key) => toolInput[key]).find((value) => typeof value === 'string');
      return String(raw || '').replace(/\\/g, '/');
    }
    
    function getToolWriteContent(payload) {
      if (!WRITE_TOOL_NAMES.has(getToolName(payload))) return '';
      const toolInput = getToolInput(payload);
      for (const key of WRITE_CONTENT_KEYS) {
        if (typeof toolInput[key] === 'string') return toolInput[key];
      }
      return '';
    }
    
    /**
     * 返回所有 pathPatterns 命中该路径的规则。
     *
     * 不能只取第一条:规则按目录字母序加载,而 `agents/**` 与
     * `cloud-functions/**` 这类前缀天然会重叠。只取首条等于让「哪条铁律生效」
     * 由目录名的字母序偶然决定,多个能力共管同一路径时会静默丢提醒。
     */
    function findSkillsForPath(filePath, rules) {
      if (!filePath) return [];
      return rules.filter((rule) =>
        rule.pathPatterns.some((pattern) => globToRegExp(pattern).test(filePath)),
      );
    }
    
    /**
     * 收集全部命中的校验项,message 去重并保留首次出现顺序。
     * 每项带上来源 skill,供 signal log 归因。
     */
    function selectValidationMatches(content, matchedRules) {
      const seen = new Set();
      const matches = [];
      for (const rule of matchedRules) {
        for (const item of rule.validate) {
          if (!new RegExp(item.pattern).test(content)) continue;
          if (seen.has(item.message)) continue;
          seen.add(item.message);
          matches.push({ ...item, skill: rule.skill });
        }
      }
      return ma
  • hooks/validate-write.test.mjsGitHub
    Read the script
    import assert from 'node:assert/strict';
    import { mkdtemp, readFile, rm } from 'node:fs/promises';
    import { join } from 'node:path';
    import { tmpdir } from 'node:os';
    import test from 'node:test';
    
    import { buildValidateWriteOutput, loadSkillValidateRules } from './validate-write.mjs';
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.1 loads validate rules from Skill frontmatter', async () => {
      const rules = await loadSkillValidateRules();
      const edgeFunctions = rules.find((rule) => rule.skill === 'edgeone-makers-edge-functions');
    
      assert.ok(edgeFunctions, 'edge-functions capability should declare validate rules');
      assert.deepEqual(edgeFunctions.pathPatterns, ['edge-functions/**', 'functions/**']);
    
      // 断言不变量而不是冻结的数组:每加一条规则都让测试失败,只会逼着人改断言。
      assert.ok(edgeFunctions.validate.length >= 3);
      for (const item of edgeFunctions.validate) {
        assert.equal(typeof item.pattern, 'string');
        assert.equal(typeof item.message, 'string');
        assert.ok(item.message.length > 0);
        assert.doesNotThrow(() => new RegExp(item.pattern));
      }
    
      // 三条原始红线必须始终在场,新增规则不得把它们挤掉。
      const messages = edgeFunctions.validate.map((item) => item.message);
      assert.ok(messages.includes('Use context.env in EdgeOne Makers runtime code.'));
      assert.ok(messages.includes('Use plain object headers for this runtime surface.'));
      assert.ok(messages.includes('Edge Functions do not support filesystem writes.'));
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.2 warns on Edit content without blocking writes', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'Edit',
        tool_input: {
          file_path: 'functions/index.ts',
          new_string: 'export default () => process.env.API_KEY;',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.4 warns on Write content using new Headers', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'Write',
        tool_input: {
          file_path: 'functions/index.ts',
          content: 'return new Response(body, { headers: new Headers() });',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.5 warns on Edge Function filesystem writes', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'Write',
        tool_input: {
          file_path: 'functions/index.ts',
          content: 'fs.writeFile("/tmp/out.txt", "data", () => {});',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Edge Functions do not support filesystem writes.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for read-only tool use', () => {
      assert.equal(
        buildValidateWriteOutput({
          tool_name: 'Read',
          tool_input: {
            file_path: 'functions/index.ts',
            content: 'process.env.API_KEY',
          },
        }),
        null,
      );
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for frontend paths outside validate scope', () => {
      assert.equal(
        buildValidateWriteOutput({
          tool_name: 'Edit',
          tool_input: {
            file_path: 'src/components/Button.tsx',
            new_string: 'export default () => process.env.API_KEY;',
          },
        }),
        null,
      );
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 does not warn for skills without validate rules', () => {
      assert.equal(
        buildValidateWriteOutput({
          tool_name: 'Edit',
          tool_input: {
            file_path: 'agents/chat.ts',
            new_string: 'const session = context.store.openaiSession(context.conversation_id);',
          },
        }),
        null,
      );
    });
    
    test('plugin-skill-injection-optimization.VALIDATE_RED_LINES.6 matches edge-functions path after pathPatterns fix', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'Write',
        tool_input: {
          file_path: 'edge-functions/api/hello.js',
          content: 'export default () => process.env.API_KEY;',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy replace_in_file validate with new_str', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'replace_in_file',
        tool_input: {
          filePath: 'functions/index.ts',
          new_str: 'export default () => process.env.API_KEY;',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Use context.env in EdgeOne Makers runtime code.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.DOMESTIC_IDE_ADAPTATION.5 supports CodeBuddy write_to_file validate with filePath', () => {
      const output = buildValidateWriteOutput({
        tool_name: 'write_to_file',
        tool_input: {
          filePath: 'functions/index.ts',
          content: 'return new Response(body, { headers: new Headers() });',
        },
      });
    
      assert.deepEqual(output, {
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          additionalContext: 'Validation reminder:\n- Use plain object headers for this runtime surface.',
        },
      });
    });
    
    test('plugin-skill-injection-optimization.SIGNAL_LOGGING.3 logs validate matches with readable reasons', async () => {
      const tmp = await mkdtemp(join(tmpdir(), 'makers-validate-log-'));
      const signalLogPath = join(tmp, '.edgeone', 'signal-log.jsonl');

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 withedgeone-makers-tools

Official AI Agent Skills for developing and deploying projects on EdgeOne Makers.

Get the whole plugin, auto-invoked
Stats
1,894
Stars
152
Forks
Active
Maintenance
JavaScript
Language
7d ago
Last commit
6mo ago
Created

Repo: tencentedgeone/edgeone-pages-skills