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.
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
- Matches
Edit|Write|replace_in_file|write_to_filenode "${CLAUDE_PLUGIN_ROOT}/hooks/validate-write.mjs"
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

