claude-code/plugins/plugin-dev/skills/hook-development/references/migration.md
Daisy S. Hollman 387dc35db7
feat: Add plugin-dev toolkit for comprehensive plugin development
Adds the plugin-dev plugin to public marketplace. A comprehensive toolkit for
developing Claude Code plugins with 7 expert skills, 3 AI-assisted agents, and
extensive documentation covering the complete plugin development lifecycle.

Key features:
- 7 skills: hook-development, mcp-integration, plugin-structure, plugin-settings,
  command-development, agent-development, skill-development
- 3 agents: agent-creator (AI-assisted generation), plugin-validator (structure
  validation), skill-reviewer (quality review)
- 1 command: /plugin-dev:create-plugin (guided 8-phase workflow)
- 10 utility scripts for validation and testing
- 21 reference docs with deep-dive guidance (~11k words)
- 9 working examples demonstrating best practices

Changes for public release:
- Replaced all references to internal repositories with "Claude Code"
- Updated MCP examples: internal.company.com → api.example.com
- Updated token variables: ${INTERNAL_TOKEN} → ${API_TOKEN}
- Reframed agent-creation-system-prompt as "proven in production"
- Preserved all ${CLAUDE_PLUGIN_ROOT} references (186 total)
- Preserved valuable test blocks in core modules

Validation:
- All 3 agents validated successfully with validate-agent.sh
- All JSON files validated with jq
- Zero internal references remaining
- 59 files migrated, 21,971 lines added

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 04:09:00 -08:00

8.1 KiB

Migrating from Basic to Advanced Hooks

This guide shows how to migrate from basic command hooks to advanced prompt-based hooks for better maintainability and flexibility.

Why Migrate?

Prompt-based hooks offer several advantages:

  • Natural language reasoning: LLM understands context and intent
  • Better edge case handling: Adapts to unexpected scenarios
  • No bash scripting required: Simpler to write and maintain
  • More flexible validation: Can handle complex logic without coding

Migration Example: Bash Command Validation

Before (Basic Command Hook)

Configuration:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "bash validate-bash.sh"
        }
      ]
    }
  ]
}

Script (validate-bash.sh):

#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')

# Hard-coded validation logic
if [[ "$command" == *"rm -rf"* ]]; then
  echo "Dangerous command detected" >&2
  exit 2
fi

Problems:

  • Only checks for exact "rm -rf" pattern
  • Doesn't catch variations like rm -fr or rm -r -f
  • Misses other dangerous commands (dd, mkfs, etc.)
  • No context awareness
  • Requires bash scripting knowledge

After (Advanced Prompt Hook)

Configuration:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Command: $TOOL_INPUT.command. Analyze for: 1) Destructive operations (rm -rf, dd, mkfs, etc) 2) Privilege escalation (sudo) 3) Network operations without user consent. Return 'approve' or 'deny' with explanation.",
          "timeout": 15
        }
      ]
    }
  ]
}

Benefits:

  • Catches all variations and patterns
  • Understands intent, not just literal strings
  • No script file needed
  • Easy to extend with new criteria
  • Context-aware decisions
  • Natural language explanation in denial

Migration Example: File Write Validation

Before (Basic Command Hook)

Configuration:

{
  "PreToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {
          "type": "command",
          "command": "bash validate-write.sh"
        }
      ]
    }
  ]
}

Script (validate-write.sh):

#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

# Check for path traversal
if [[ "$file_path" == *".."* ]]; then
  echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
  exit 2
fi

# Check for system paths
if [[ "$file_path" == "/etc/"* ]] || [[ "$file_path" == "/sys/"* ]]; then
  echo '{"decision": "deny", "reason": "System file"}' >&2
  exit 2
fi

Problems:

  • Hard-coded path patterns
  • Doesn't understand symlinks
  • Missing edge cases (e.g., /etc vs /etc/)
  • No consideration of file content

After (Advanced Prompt Hook)

Configuration:

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "File path: $TOOL_INPUT.file_path. Content preview: $TOOL_INPUT.content (first 200 chars). Verify: 1) Not system directories (/etc, /sys, /usr) 2) Not credentials (.env, tokens, secrets) 3) No path traversal 4) Content doesn't expose secrets. Return 'approve' or 'deny'."
        }
      ]
    }
  ]
}

Benefits:

  • Context-aware (considers content too)
  • Handles symlinks and edge cases
  • Natural understanding of "system directories"
  • Can detect secrets in content
  • Easy to extend criteria

When to Keep Command Hooks

Command hooks still have their place:

1. Deterministic Performance Checks

#!/bin/bash
# Check file size quickly
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
size=$(stat -f%z "$file_path" 2>/dev/null || stat -c%s "$file_path" 2>/dev/null)

if [ "$size" -gt 10000000 ]; then
  echo '{"decision": "deny", "reason": "File too large"}' >&2
  exit 2
fi

Use command hooks when: Validation is purely mathematical or deterministic.

2. External Tool Integration

#!/bin/bash
# Run security scanner
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
scan_result=$(security-scanner "$file_path")

if [ "$?" -ne 0 ]; then
  echo "Security scan failed: $scan_result" >&2
  exit 2
fi

Use command hooks when: Integrating with external tools that provide yes/no answers.

3. Very Fast Checks (< 50ms)

#!/bin/bash
# Quick regex check
command=$(echo "$input" | jq -r '.tool_input.command')

if [[ "$command" =~ ^(ls|pwd|echo)$ ]]; then
  exit 0  # Safe commands
fi

Use command hooks when: Performance is critical and logic is simple.

Hybrid Approach

Combine both for multi-stage validation:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/quick-check.sh",
          "timeout": 5
        },
        {
          "type": "prompt",
          "prompt": "Deep analysis of bash command: $TOOL_INPUT",
          "timeout": 15
        }
      ]
    }
  ]
}

The command hook does fast deterministic checks, while the prompt hook handles complex reasoning.

Migration Checklist

When migrating hooks:

  • Identify the validation logic in the command hook
  • Convert hard-coded patterns to natural language criteria
  • Test with edge cases the old hook missed
  • Verify LLM understands the intent
  • Set appropriate timeout (usually 15-30s for prompt hooks)
  • Document the new hook in README
  • Remove or archive old script files

Migration Tips

  1. Start with one hook: Don't migrate everything at once
  2. Test thoroughly: Verify prompt hook catches what command hook caught
  3. Look for improvements: Use migration as opportunity to enhance validation
  4. Keep scripts for reference: Archive old scripts in case you need to reference the logic
  5. Document reasoning: Explain why prompt hook is better in README

Complete Migration Example

Original Plugin Structure

my-plugin/
├── .claude-plugin/plugin.json
├── hooks/hooks.json
└── scripts/
    ├── validate-bash.sh
    ├── validate-write.sh
    └── check-tests.sh

After Migration

my-plugin/
├── .claude-plugin/plugin.json
├── hooks/hooks.json      # Now uses prompt hooks
└── scripts/              # Archive or delete
    └── archive/
        ├── validate-bash.sh
        ├── validate-write.sh
        └── check-tests.sh

Updated hooks.json

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Validate bash command safety: destructive ops, privilege escalation, network access"
        }
      ]
    },
    {
      "matcher": "Write|Edit",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Validate file write safety: system paths, credentials, path traversal, content secrets"
        }
      ]
    }
  ],
  "Stop": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Verify tests were run if code was modified"
        }
      ]
    }
  ]
}

Result: Simpler, more maintainable, more powerful.

Common Migration Patterns

Pattern: String Contains → Natural Language

Before:

if [[ "$command" == *"sudo"* ]]; then
  echo "Privilege escalation" >&2
  exit 2
fi

After:

"Check for privilege escalation (sudo, su, etc)"

Pattern: Regex → Intent

Before:

if [[ "$file" =~ \.(env|secret|key|token)$ ]]; then
  echo "Credential file" >&2
  exit 2
fi

After:

"Verify not writing to credential files (.env, secrets, keys, tokens)"

Pattern: Multiple Conditions → Criteria List

Before:

if [ condition1 ] || [ condition2 ] || [ condition3 ]; then
  echo "Invalid" >&2
  exit 2
fi

After:

"Check: 1) condition1 2) condition2 3) condition3. Deny if any fail."

Conclusion

Migrating to prompt-based hooks makes plugins more maintainable, flexible, and powerful. Reserve command hooks for deterministic checks and external tool integration.