Skip to content
Development
Hook

Hooks

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

From plugin
skill-semver
181 skill1 hook
Install
> /plugin marketplace add cathy-kim/skill-semver
> /plugin install skill-semver@skill-semver-marketplace

Ships with skill-semver. Installing the plugin gets these hooks.

What fires, and when

PostToolUse

  • MatchesWrite|Editnpx tsx hooks/skill-version-hook.ts
Read hooks/hooks.json

Where it lives

  • hooks/skill-version-hook.tsRunsGitHub
    Read the script
    #!/usr/bin/env npx tsx
    
    /**
     * Skill Version Hook v1.1.0
     *
     * SKILL.md 파일이 수정될 때 자동으로 버전 백업을 생성합니다.
     *
     * Hook Event: PostToolUse (Write, Edit tools)
     *
     * 동작:
     * 1. Write/Edit 도구가 SKILL.md를 수정했는지 확인
     * 2. SKILL.md에서 버전 정보 추출 (pre-release 지원: 1.0.0-alpha, 1.0.0-beta.1)
     * 3. releases/ 폴더에 버전별 백업 생성
     * 4. CHANGELOG.md 업데이트
     * 5. Last Updated 날짜 자동 업데이트
     * 6. 로그 출력
     *
     * 파일 명명 규칙: v{VERSION}_{YYYY-MM-DD}_SKILL.md
     *
     * Changelog:
     * - v1.1.0: Pre-release 지원, Last Updated 자동화, 경로 버그 수정, 성능 최적화
     * - v1.0.0: 초기 버전
     */
    
    import * as fs from "fs";
    import * as path from "path";
    
    // ============================================================================
    // Types
    // ============================================================================
    
    interface PostToolUseInput {
      session_id: string;
      tool_name: string;
      tool_input: {
        file_path?: string;
        content?: string;
        old_string?: string;
        new_string?: string;
      };
      tool_output?: {
        success?: boolean;
        error?: string;
      };
      transcript_path?: string;
    }
    
    interface HookResult {
      continue: boolean;
      message?: string;
    }
    
    // ============================================================================
    // Constants
    // ============================================================================
    
    const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
    const LOG_DIR = path.join(PROJECT_DIR, ".claude", "hooks", "logs");
    
    // ============================================================================
    // Main Hook
    // ============================================================================
    
    async function main(): Promise<void> {
      try {
        // Read input from stdin
        const inputData = fs.readFileSync(0, "utf-8");
        const input: PostToolUseInput = JSON.parse(inputData);
    
        const result = await processHook(input);
    
        // Output result
        console.log(JSON.stringify(result));
      } catch (error: any) {
        // Graceful degradation - don't block the session
        console.log(
          JSON.stringify({
            continue: true,
            message: `[skill-version-hook] Error: ${error.message}`,
          })
        );
      }
    }
    
    async function processHook(input: PostToolUseInput): Promise<HookResult> {
      const { tool_name, tool_input, tool_output } = input;
    
      // 1. Check if this is a Write or Edit tool
      if (tool_name !== "Write" && tool_name !== "Edit") {
        return { continue: true };
      }
    
      // 2. Check if the tool succeeded
      if (tool_output?.success === false) {
        return { continue: true };
      }
    
      // 3. Check if the file is a SKILL.md
      const filePath = tool_input?.file_path;
      if (!filePath || !isSkillMdFile(filePath)) {
        return { continue: true };
      }
    
      // 4. Extract skill name from path
      const skillName = extractSkillName(filePath);
      if (!skillName) {
        return { continue: true };
      }
    
      // 5. Read the SKILL.md content once (optimization: single read)
      let content: string;
      try {
        content = fs.readFileSync(filePath, "utf-8");
      } catch (error: any) {
        log(`[skill-version-hook] Failed to read ${filePath}: ${error.message}`);
        return { continue: true };
      }
    
      // 6. Extract version from content (supports pre-release: 1.0.0-alpha, 1.0.0-beta.1)
      const version = extractVersionFromContent(content);
      if (!version) {
        log(`[skill-version-hook] No version found in ${filePath}, skipping backup`);
        return {
          continue: true,
          message: `[skill-version-hook] No version header found in ${skillName}/SKILL.md`,
        };
      }
    
      // 7. Create releases directory if needed
      const releasesDir = path.join(path.dirname(filePath), "releases");
      if (!fs.existsSync(releasesDir)) {
        fs.mkdirSync(releasesDir, { recursive: true });
      }
    
      // 8. Generate backup filename (sanitize pre-release for filename)
      const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
      const safeVersion = version.replace(/[^a-zA-Z0-9.-]/g, "-");
      const backupFilename = `v${safeVersion}_${today}_SKILL.md`;
      const backupPath = path.join(releasesDir, backupFilename);
    
      // 9. Check if backup already exists for this version
      if (fs.existsSync(backupPath)) {
        log(`[skill-version-hook] Backup already exists: ${backupFilename}`);
        return {
          continue: true,
          message: `[skill-version-hook] Backup already exists for ${skillName} v${version}`,
        };
      }
    
      // 10. Update Last Updated date in SKILL.md
      const updatedContent = updateLastUpdated(content, today);
      if (updatedContent !== content) {
        try {
          fs.writeFileSync(filePath, updatedContent, "utf-8");
          content = updatedContent;
          log(`[skill-version-hook] Updated Last Updated to ${today}`);
        } catch (error: any) {
          log(`[skill-version-hook] Failed to update Last Updated: ${error.message}`);
        }
      }
    
      // 11. Create backup
      try {
        fs.writeFileSync(backupPath, content, "utf-8");
        log(`[skill-version-hook] Created backup: ${backupFilename}`);
    
        // 12. Update CHANGELOG.md
        const changelogPath = path.join(path.dirname(filePath), "CHANGELOG.md");
        updateChangelog(changelogPath, skillName, version, today);
    
        // 13. Add initial development notice for 0.x.x versions
        const versionNote = version.startsWith("0.")
          ? " (Initial Development)"
          : "";
    
        return {
          continue: true,
          message: `[skill-version-hook] Backed up ${skillName}/SKILL.md to releases/${backupFilename}${versionNote}`,
        };
      } catch (error: any) {
        log(`[skill-version-hook] Failed to create backup: ${error.message}`);
        return {
          continue: true,
          message: `[skill-version-hook] Failed to backup: ${error.message}`,
        };
      }
    }
    
    // ============================================================================
    // Helper Functions
    // ============================================================================
    
    /**
     * Check if the file path is a SKILL.md file
     * Fixed: Use regex for cross-platform path separator compatibility
     */
    function isSkillMdFile(filePath: string): boolean {
      // Normalize path and convert to forward slashes for co

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 withskill-semver

Automatic version control for Claude Code Skills with semantic versioning, auto-backup, and changelog generation.

Get the whole plugin
Stats
18
Stars
2
Forks
Quiet
Maintenance
TypeScript
Language
7mo ago
Last commit
7mo ago
Created

Repo: cathy-kim/skill-semver