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)); const DEFAULT_SKILLS_DIR = join(HOOKS_DIR, '..', 'skills'); 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, }; } export function loadSkillValidateRules(skillsDir = DEFAULT_SKILLS_DIR) { if (skillsDir === DEFAULT_SKILLS_DIR && cachedRules) return cachedRules; const rules = readdirSync(skillsDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => join(skillsDir, entry.name, 'SKILL.md')) .map((skillPath) => parseSkillValidateRule(skillPath)) .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 ''; } function findSkillForPath(filePath, rules) { if (!filePath) return null; for (const rule of rules) { if (rule.pathPatterns.some((pattern) => globToRegExp(pattern).test(filePath))) return rule; } return null; } function selectValidationMatches(content, rule) { const seen = new Set(); const matches = []; for (const item of rule.validate) { if (new RegExp(item.pattern).test(content) && !seen.has(item.message)) { seen.add(item.message); matches.push(item); } } return matches; } function renderValidationReminder(messages) { return `Validation reminder:\n${messages.map((message) => `- ${message}`).join('\n')}`; } export function buildValidateWriteOutput(payload, options = {}) { if (!WRITE_TOOL_NAMES.has(getToolName(payload))) return null; const content = getToolWriteContent(payload); if (!content) return null; const rule = findSkillForPath( getToolPath(getToolInput(payload)), options.rules || loadSkillValidateRules(), ); if (!rule) return null; const matches = selectValidationMatches(content, rule); if (matches.length === 0) return null; if (shouldWriteSignalLog(options)) { for (const match of matches) { writeSignalLog( { hook: 'PreToolUse', trigger: 'validate', matchedSkill: rule.skill, reason: match.message, - 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); assert.deepEqual(edgeFunctions.validate, [ { pattern: 'process\\.env', message: 'Use context.env in EdgeOne Makers runtime code.', }, { pattern: 'new\\s+Headers\\s*\\(', message: 'Use plain object headers for this runtime surface.', }, { pattern: 'fs\\.writeFile', message: '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'); try { buildValidateWriteOutput( { tool_name: 'Write', tool_input: { file_path: 'functions/index.ts', content: 'export default () => process.env.API_KEY;', }, }, { signalLogPath, now: new Date('2026-07-03T00:00:00.000Z'), }, ); const [line] = (await readFile(signalLogPath, 'utf8')).trim().split('\n'); assert.deepEqual(JSON.parse(line), {
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
Stats
2,018
Stars
171
Forks
Active
Maintenance
JavaScript
Language
2h ago
Last commit
5mo ago
Created
Repo: tencentedgeone/edgeone-pages-skills

