Initial commit
This commit is contained in:
261
skills/hook.define/SKILL.md
Normal file
261
skills/hook.define/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# hook.define
|
||||
|
||||
## Overview
|
||||
|
||||
**hook.define** is a Betty skill that creates and registers validation hooks for Claude Code. Hooks enable automatic validation and policy enforcement by triggering skills on events like file edits, commits, and pushes.
|
||||
|
||||
## Purpose
|
||||
|
||||
Transform manual validation into automatic safety rails:
|
||||
- **Before**: Developers must remember to run validation
|
||||
- **After**: Validation happens automatically on every file edit
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
python skills/hook.define/hook_define.py <event> <command> [options]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Required | Description | Example |
|
||||
|-----------|----------|-------------|---------|
|
||||
| `event` | Yes | Hook event trigger | `on_file_edit` |
|
||||
| `command` | Yes | Command to execute | `api.validate {file_path} zalando` |
|
||||
| `--pattern` | No | File pattern to match | `*.openapi.yaml` |
|
||||
| `--blocking` | No | Block on failure (default: true) | `true` |
|
||||
| `--timeout` | No | Timeout in ms (default: 30000) | `10000` |
|
||||
| `--description` | No | Human-readable description | `Validate OpenAPI specs` |
|
||||
|
||||
### Valid Events
|
||||
|
||||
| Event | Triggers When | Use Case |
|
||||
|-------|---------------|----------|
|
||||
| `on_file_edit` | File is edited in editor | Syntax validation |
|
||||
| `on_file_save` | File is saved | Code generation |
|
||||
| `on_commit` | Git commit attempted | Breaking change detection |
|
||||
| `on_push` | Git push attempted | Full validation suite |
|
||||
| `on_tool_use` | Any tool is used | Audit logging |
|
||||
| `on_agent_start` | Agent begins execution | Context injection |
|
||||
| `on_workflow_end` | Workflow completes | Cleanup/notification |
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Validate OpenAPI Specs on Edit
|
||||
|
||||
```bash
|
||||
python skills/hook.define/hook_define.py \
|
||||
on_file_edit \
|
||||
"python betty/skills/api.validate/api_validate.py {file_path} zalando" \
|
||||
--pattern="*.openapi.yaml" \
|
||||
--blocking=true \
|
||||
--timeout=10000 \
|
||||
--description="Validate OpenAPI specs against Zalando guidelines"
|
||||
```
|
||||
|
||||
**Result**: Every time a `*.openapi.yaml` file is edited, it's automatically validated against Zalando guidelines. If validation fails, the edit is blocked.
|
||||
|
||||
### Example 2: Check Breaking Changes on Commit
|
||||
|
||||
```bash
|
||||
python skills/hook.define/hook_define.py \
|
||||
on_commit \
|
||||
"python betty/skills/api.compatibility/check_breaking_changes.py {file_path}" \
|
||||
--pattern="specs/**/*.yaml" \
|
||||
--blocking=true \
|
||||
--description="Prevent commits with breaking API changes"
|
||||
```
|
||||
|
||||
**Result**: Commits are blocked if they contain breaking API changes.
|
||||
|
||||
### Example 3: Regenerate Models on Save
|
||||
|
||||
```bash
|
||||
python skills/hook.define/hook_define.py \
|
||||
on_file_save \
|
||||
"python betty/skills/api.generate-models/auto_generate.py {file_path}" \
|
||||
--pattern="specs/*.openapi.yaml" \
|
||||
--blocking=false \
|
||||
--description="Auto-regenerate models when specs change"
|
||||
```
|
||||
|
||||
**Result**: When an OpenAPI spec is saved, models are regenerated automatically (non-blocking).
|
||||
|
||||
### Example 4: Audit Trail for All Tool Use
|
||||
|
||||
```bash
|
||||
python skills/hook.define/hook_define.py \
|
||||
on_tool_use \
|
||||
"python betty/skills/audit.log/log_api_change.py {tool_name}" \
|
||||
--blocking=false \
|
||||
--description="Log all tool usage for compliance"
|
||||
```
|
||||
|
||||
**Result**: All tool usage is logged for audit trails (non-blocking).
|
||||
|
||||
## Output
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"hook_config": {
|
||||
"name": "api-validate-all-openapi-yaml",
|
||||
"command": "python betty/skills/api.validate/api_validate.py {file_path} zalando",
|
||||
"blocking": true,
|
||||
"timeout": 10000,
|
||||
"when": {
|
||||
"pattern": "*.openapi.yaml"
|
||||
},
|
||||
"description": "Validate OpenAPI specs against Zalando guidelines"
|
||||
},
|
||||
"hooks_file_path": "/home/user/betty/.claude/hooks.yaml",
|
||||
"event": "on_file_edit",
|
||||
"total_hooks": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generated Hooks File
|
||||
|
||||
The skill creates/updates `.claude/hooks.yaml`:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_file_edit:
|
||||
- name: api-validate-all-openapi-yaml
|
||||
command: python betty/skills/api.validate/api_validate.py {file_path} zalando
|
||||
blocking: true
|
||||
timeout: 10000
|
||||
when:
|
||||
pattern: "*.openapi.yaml"
|
||||
description: Validate OpenAPI specs against Zalando guidelines
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Load Existing Hooks**: Reads `.claude/hooks.yaml` if it exists
|
||||
2. **Create Hook Config**: Builds configuration from parameters
|
||||
3. **Add or Update**: Adds new hook or updates existing one with same name
|
||||
4. **Save**: Writes updated configuration back to `.claude/hooks.yaml`
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Developers
|
||||
- ✅ **Instant feedback**: Errors caught immediately, not at commit time
|
||||
- ✅ **No discipline required**: Validation happens automatically
|
||||
- ✅ **Consistent quality**: Standards enforced, not suggested
|
||||
|
||||
### For Teams
|
||||
- ✅ **Enforced standards**: Can't save non-compliant code
|
||||
- ✅ **Reduced review time**: Automated checks before human review
|
||||
- ✅ **Onboarding**: New developers can't accidentally break standards
|
||||
|
||||
### For Organizations
|
||||
- ✅ **Compliance**: Policies enforced at tool level
|
||||
- ✅ **Audit trail**: Every validation is logged
|
||||
- ✅ **Risk reduction**: Catch issues early, not in production
|
||||
|
||||
## Integration with Betty
|
||||
|
||||
### Use in Workflows
|
||||
|
||||
```yaml
|
||||
# workflows/setup_api_validation.yaml
|
||||
steps:
|
||||
- skill: hook.define
|
||||
args:
|
||||
- "on_file_edit"
|
||||
- "api.validate {file_path} zalando"
|
||||
- "--pattern=*.openapi.yaml"
|
||||
- "--blocking=true"
|
||||
```
|
||||
|
||||
### Use in Agents
|
||||
|
||||
Agents can dynamically create hooks based on project needs:
|
||||
|
||||
```python
|
||||
# Agent detects OpenAPI specs in project
|
||||
# Automatically sets up validation hooks
|
||||
```
|
||||
|
||||
## File Pattern Examples
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `*.openapi.yaml` | All OpenAPI files in current directory |
|
||||
| `*.asyncapi.yaml` | All AsyncAPI files in current directory |
|
||||
| `specs/**/*.yaml` | All YAML files in specs/ and subdirectories |
|
||||
| `src/**/*.ts` | All TypeScript files in src/ and subdirectories |
|
||||
| `**/*.py` | All Python files anywhere |
|
||||
|
||||
## Blocking vs Non-Blocking
|
||||
|
||||
### Blocking Hooks (blocking: true)
|
||||
- Operation is **paused** until hook completes
|
||||
- If hook **fails**, operation is **aborted**
|
||||
- Use for: Validation, compliance checks, breaking change detection
|
||||
|
||||
### Non-Blocking Hooks (blocking: false)
|
||||
- Hook runs **asynchronously**
|
||||
- Operation **continues** regardless of hook result
|
||||
- Use for: Logging, notifications, background tasks
|
||||
|
||||
## Timeout Considerations
|
||||
|
||||
| Operation | Suggested Timeout | Reasoning |
|
||||
|-----------|-------------------|-----------|
|
||||
| Syntax validation | 5,000 - 10,000 ms | Fast, local checks |
|
||||
| Zally API call | 10,000 - 30,000 ms | Network latency |
|
||||
| Model generation | 30,000 - 60,000 ms | Compilation time |
|
||||
| Full test suite | 300,000 ms (5 min) | Comprehensive testing |
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Hook Execution Failed
|
||||
|
||||
If a blocking hook fails:
|
||||
```
|
||||
❌ Hook 'validate-openapi' failed:
|
||||
- Missing required field: info.x-api-id
|
||||
- Property 'userId' should use snake_case
|
||||
|
||||
Operation blocked. Fix errors and try again.
|
||||
```
|
||||
|
||||
### Hook Timeout
|
||||
|
||||
If a hook exceeds timeout:
|
||||
```
|
||||
⚠️ Hook 'validate-openapi' timed out after 10000ms
|
||||
Operation blocked for safety.
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PyYAML**: Required for YAML file handling
|
||||
```bash
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
- **context.schema**: For validation rule definitions
|
||||
|
||||
## Files Created
|
||||
|
||||
- `.claude/hooks.yaml` - Hook configurations for Claude Code
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.validate](../api.validate/SKILL.md) - API validation skill
|
||||
- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model
|
||||
- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide
|
||||
- [Claude Code Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks)
|
||||
|
||||
## Version
|
||||
|
||||
**0.1.0** - Initial implementation with basic hook definition support
|
||||
1
skills/hook.define/__init__.py
Normal file
1
skills/hook.define/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
310
skills/hook.define/hook_define.py
Executable file
310
skills/hook.define/hook_define.py
Executable file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Define and register validation hooks for Claude Code.
|
||||
|
||||
This skill creates hook configurations in .claude/hooks.yaml that automatically
|
||||
trigger validation skills on events like file edits, commits, and pushes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Add betty module to path
|
||||
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import format_error_response, BettyError
|
||||
from betty.validation import validate_path
|
||||
from betty.file_utils import safe_read_json, safe_write_json
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# Valid hook events
|
||||
VALID_EVENTS = [
|
||||
"on_file_edit",
|
||||
"on_file_save",
|
||||
"on_commit",
|
||||
"on_push",
|
||||
"on_tool_use",
|
||||
"on_agent_start",
|
||||
"on_workflow_end"
|
||||
]
|
||||
|
||||
|
||||
def create_hooks_directory() -> Path:
|
||||
"""
|
||||
Create .claude directory if it doesn't exist.
|
||||
|
||||
Returns:
|
||||
Path to .claude directory
|
||||
"""
|
||||
claude_dir = Path.cwd() / ".claude"
|
||||
claude_dir.mkdir(exist_ok=True)
|
||||
logger.info(f"Ensured .claude directory exists at {claude_dir}")
|
||||
return claude_dir
|
||||
|
||||
|
||||
def load_existing_hooks(hooks_file: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Load existing hooks configuration if it exists.
|
||||
|
||||
Args:
|
||||
hooks_file: Path to hooks.yaml file
|
||||
|
||||
Returns:
|
||||
Existing hooks configuration or empty structure
|
||||
"""
|
||||
if hooks_file.exists():
|
||||
try:
|
||||
import yaml
|
||||
with open(hooks_file, 'r') as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
logger.info(f"Loaded existing hooks from {hooks_file}")
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load existing hooks: {e}")
|
||||
return {"hooks": {}}
|
||||
else:
|
||||
logger.info(f"No existing hooks file found at {hooks_file}")
|
||||
return {"hooks": {}}
|
||||
|
||||
|
||||
def create_hook_config(
|
||||
event: str,
|
||||
command: str,
|
||||
pattern: Optional[str] = None,
|
||||
blocking: bool = True,
|
||||
timeout: int = 30000,
|
||||
description: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a hook configuration object.
|
||||
|
||||
Args:
|
||||
event: Hook event trigger
|
||||
command: Command to execute
|
||||
pattern: File pattern to match (optional)
|
||||
blocking: Whether hook should block on failure
|
||||
timeout: Timeout in milliseconds
|
||||
description: Human-readable description
|
||||
|
||||
Returns:
|
||||
Hook configuration dictionary
|
||||
"""
|
||||
# Generate a name from the command and pattern
|
||||
if pattern:
|
||||
name = f"{command.replace('/', '-').replace('.', '-')}-{pattern.replace('*', 'all').replace('/', '-')}"
|
||||
else:
|
||||
name = command.replace('/', '-').replace('.', '-')
|
||||
|
||||
hook_config = {
|
||||
"name": name,
|
||||
"command": command,
|
||||
"blocking": blocking,
|
||||
"timeout": timeout
|
||||
}
|
||||
|
||||
if pattern:
|
||||
hook_config["when"] = {"pattern": pattern}
|
||||
|
||||
if description:
|
||||
hook_config["description"] = description
|
||||
else:
|
||||
hook_config["description"] = f"Auto-generated hook for {command}"
|
||||
|
||||
return hook_config
|
||||
|
||||
|
||||
def add_hook_to_config(
|
||||
config: Dict[str, Any],
|
||||
event: str,
|
||||
hook_config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Add a hook to the configuration, avoiding duplicates.
|
||||
|
||||
Args:
|
||||
config: Existing hooks configuration
|
||||
event: Event to add hook to
|
||||
hook_config: Hook configuration to add
|
||||
|
||||
Returns:
|
||||
Updated configuration
|
||||
"""
|
||||
if "hooks" not in config:
|
||||
config["hooks"] = {}
|
||||
|
||||
if event not in config["hooks"]:
|
||||
config["hooks"][event] = []
|
||||
|
||||
# Check if hook with same name already exists
|
||||
existing_names = [h.get("name") for h in config["hooks"][event]]
|
||||
if hook_config["name"] in existing_names:
|
||||
# Update existing hook
|
||||
for i, hook in enumerate(config["hooks"][event]):
|
||||
if hook.get("name") == hook_config["name"]:
|
||||
config["hooks"][event][i] = hook_config
|
||||
logger.info(f"Updated existing hook: {hook_config['name']}")
|
||||
break
|
||||
else:
|
||||
# Add new hook
|
||||
config["hooks"][event].append(hook_config)
|
||||
logger.info(f"Added new hook: {hook_config['name']}")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def save_hooks_config(hooks_file: Path, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Save hooks configuration to YAML file.
|
||||
|
||||
Args:
|
||||
hooks_file: Path to hooks.yaml file
|
||||
config: Configuration to save
|
||||
"""
|
||||
try:
|
||||
import yaml
|
||||
with open(hooks_file, 'w') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
||||
logger.info(f"Saved hooks configuration to {hooks_file}")
|
||||
except Exception as e:
|
||||
raise BettyError(f"Failed to save hooks configuration: {e}")
|
||||
|
||||
|
||||
def define_hook(
|
||||
event: str,
|
||||
command: str,
|
||||
pattern: Optional[str] = None,
|
||||
blocking: bool = True,
|
||||
timeout: int = 30000,
|
||||
description: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Define a new hook or update an existing one.
|
||||
|
||||
Args:
|
||||
event: Hook event trigger
|
||||
command: Command to execute
|
||||
pattern: File pattern to match
|
||||
blocking: Whether hook should block on failure
|
||||
timeout: Timeout in milliseconds
|
||||
description: Human-readable description
|
||||
|
||||
Returns:
|
||||
Result dictionary with hook config and file path
|
||||
"""
|
||||
# Validate event
|
||||
if event not in VALID_EVENTS:
|
||||
raise BettyError(
|
||||
f"Invalid event '{event}'. Valid events: {', '.join(VALID_EVENTS)}"
|
||||
)
|
||||
|
||||
# Create .claude directory
|
||||
claude_dir = create_hooks_directory()
|
||||
hooks_file = claude_dir / "hooks.yaml"
|
||||
|
||||
# Load existing hooks
|
||||
config = load_existing_hooks(hooks_file)
|
||||
|
||||
# Create new hook config
|
||||
hook_config = create_hook_config(
|
||||
event=event,
|
||||
command=command,
|
||||
pattern=pattern,
|
||||
blocking=blocking,
|
||||
timeout=timeout,
|
||||
description=description
|
||||
)
|
||||
|
||||
# Add to configuration
|
||||
config = add_hook_to_config(config, event, hook_config)
|
||||
|
||||
# Save configuration
|
||||
save_hooks_config(hooks_file, config)
|
||||
|
||||
return {
|
||||
"hook_config": hook_config,
|
||||
"hooks_file_path": str(hooks_file),
|
||||
"event": event,
|
||||
"total_hooks": len(config.get("hooks", {}).get(event, []))
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Define and register validation hooks for Claude Code"
|
||||
)
|
||||
parser.add_argument(
|
||||
"event",
|
||||
type=str,
|
||||
choices=VALID_EVENTS,
|
||||
help="Hook event trigger"
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
type=str,
|
||||
help="Command to execute when hook triggers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pattern",
|
||||
type=str,
|
||||
help="File pattern to match (e.g., '*.openapi.yaml')"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--blocking",
|
||||
type=lambda x: x.lower() in ['true', '1', 'yes'],
|
||||
default=True,
|
||||
help="Whether hook should block on failure (default: true)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=30000,
|
||||
help="Timeout in milliseconds (default: 30000)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--description",
|
||||
type=str,
|
||||
help="Human-readable description of what the hook does"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Check if PyYAML is installed
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise BettyError(
|
||||
"PyYAML is required for hook.define. Install with: pip install pyyaml"
|
||||
)
|
||||
|
||||
# Define the hook
|
||||
logger.info(f"Defining hook for event '{args.event}' with command '{args.command}'")
|
||||
result = define_hook(
|
||||
event=args.event,
|
||||
command=args.command,
|
||||
pattern=args.pattern,
|
||||
blocking=args.blocking,
|
||||
timeout=args.timeout,
|
||||
description=args.description
|
||||
)
|
||||
|
||||
# Return structured result
|
||||
output = {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to define hook: {e}")
|
||||
print(json.dumps(format_error_response(e), indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
60
skills/hook.define/skill.yaml
Normal file
60
skills/hook.define/skill.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: hook.define
|
||||
version: 0.1.0
|
||||
description: Create and register validation hooks for Claude Code
|
||||
|
||||
inputs:
|
||||
- name: event
|
||||
type: string
|
||||
required: true
|
||||
description: Hook event trigger (on_file_edit, on_file_save, on_commit, on_push, on_tool_use)
|
||||
|
||||
- name: pattern
|
||||
type: string
|
||||
required: false
|
||||
description: File pattern to match (e.g., "*.openapi.yaml", "specs/**/*.yaml")
|
||||
|
||||
- name: command
|
||||
type: string
|
||||
required: true
|
||||
description: Command to execute when hook triggers (skill name or full command)
|
||||
|
||||
- name: blocking
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Whether hook should block operation if it fails
|
||||
|
||||
- name: timeout
|
||||
type: number
|
||||
required: false
|
||||
default: 30000
|
||||
description: Timeout in milliseconds (default 30 seconds)
|
||||
|
||||
- name: description
|
||||
type: string
|
||||
required: false
|
||||
description: Human-readable description of what the hook does
|
||||
|
||||
outputs:
|
||||
- name: hook_config
|
||||
type: object
|
||||
description: Generated hook configuration
|
||||
|
||||
- name: hooks_file_path
|
||||
type: string
|
||||
description: Path to .claude/hooks.yaml file
|
||||
|
||||
dependencies:
|
||||
- context.schema
|
||||
|
||||
entrypoints:
|
||||
- command: /skill/hook/define
|
||||
handler: hook_define.py
|
||||
runtime: python
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
|
||||
status: active
|
||||
|
||||
tags: [hooks, validation, automation, claude-code]
|
||||
Reference in New Issue
Block a user