intermediate45 minLesson 2 of 6

Custom Hooks for Automation

Build custom hooks to automate repetitive tasks in OpenCode. Learn the hook lifecycle, event types, and how to create hooks that validate, transform, and guard your workflow.

Custom Hooks for Automation

What Are Hooks?

Hooks are scripts that run automatically at specific points in the OpenCode lifecycle. They enable:

  • Validation — Check inputs/outputs before processing
  • Transformation — Modify data between steps
  • Guarding — Block dangerous operations
  • Logging — Track activity for auditing

Hook Events

EventWhen It FiresUse Case
session.startSession beginsLoad context, check environment
session.endSession endsCleanup, save state
file.writeBefore file is writtenValidate content, format code
file.readBefore file is readAccess control, logging
tool.execute.beforeBefore tool runsPermission checks, validation
tool.execute.afterAfter tool runsPost-processing, logging

Hook Structure

.opencode/hooks/my-hook/ ├── hook.json # Hook configuration ├── scripts/ │ └── handler.py # Hook script └── examples/ └── sample-input.json

hook.json

json
{ "name": "validate-frontmatter", "description": "Validates YAML frontmatter in lesson files", "event": "file.write", "pattern": "content/courses/**/*.md", "script": ".opencode/hooks/validate-frontmatter/scripts/handler.py", "blocking": true }
FieldDescription
nameUnique identifier
descriptionWhat the hook does
eventWhen to trigger
patternFile pattern to match (glob)
scriptPath to handler script
blockingIf true, blocks operation on failure

Creating a Validation Hook

Step 1: Create Directory

bash
mkdir -p .opencode/hooks/validate-frontmatter/scripts

Step 2: Create hook.json

json
{ "name": "validate-frontmatter", "description": "Validates YAML frontmatter in Markdown files", "event": "file.write", "pattern": "**/*.md", "script": ".opencode/hooks/validate-frontmatter/scripts/handler.py", "blocking": true }

Step 3: Create Handler Script

python
#!/usr/bin/env python3 """Validate YAML frontmatter in Markdown files. Exit Codes: 0 - Validation passed 1 - Warning (non-blocking) 2 - Validation failed (blocking) """ import os import sys import yaml from pathlib import Path def extract_frontmatter(content: str) -> tuple[str | None, str]: """Extract YAML frontmatter from Markdown content.""" if not content.startswith("---"): return None, content parts = content.split("---", 2) if len(parts) < 3: return None, content return parts[1].strip(), parts[2] def validate_frontmatter(frontmatter: str) -> list[str]: """Validate frontmatter structure and return errors.""" errors = [] try: data = yaml.safe_load(frontmatter) except yaml.YAMLError as e: return [f"Invalid YAML: {e}"] required_fields = ["title", "description", "order"] for field in required_fields: if field not in data: errors.append(f"Missing required field: {field}") if "order" in data: if not isinstance(data["order"], int): errors.append("Field 'order' must be an integer") elif data["order"] < 1: errors.append("Field 'order' must be >= 1") return errors def main() -> int: """Main hook entry point.""" file_path = os.environ.get("OPENCODE_FILE_PATH", "") if not file_path: print("WARNING: OPENCODE_FILE_PATH not set", file=sys.stderr) return 1 path = Path(file_path) if not path.exists(): print(f"ERROR: File not found: {file_path}", file=sys.stderr) return 2 try: content = path.read_text(encoding="utf-8") except Exception as e: print(f"ERROR: Cannot read file: {e}", file=sys.stderr) return 2 frontmatter, _ = extract_frontmatter(content) if frontmatter is None: print(f"WARNING: No frontmatter found in {file_path}") return 1 errors = validate_frontmatter(frontmatter) if errors: for error in errors: print(f"VALIDATION ERROR: {error}", file=sys.stderr) return 2 print(f"VALIDATION PASSED: {file_path}") return 0 if __name__ == "__main__": sys.exit(main())

Step 4: Make Script Executable

bash
chmod +x .opencode/hooks/validate-frontmatter/scripts/handler.py

Registering Hooks

Add hooks to opencode.json:

json
{ "hooks": { "file.write": [ { "name": "validate-frontmatter", "pattern": "content/courses/**/*.md", "script": ".opencode/hooks/validate-frontmatter/scripts/handler.py", "blocking": true } ], "session.start": [ { "name": "load-context", "script": ".opencode/hooks/load-context/scripts/handler.py", "blocking": false } ] } }

Guard Hooks

Guard hooks block dangerous operations:

python
#!/usr/bin/env python3 """Block writes to protected directories.""" import os import sys from pathlib import Path PROTECTED_PATTERNS = [ ".env", "secrets/**", "*.key", "*.pem", ] def is_protected(path: Path) -> bool: """Check if path matches protected patterns.""" from fnmatch import fnmatch path_str = str(path) for pattern in PROTECTED_PATTERNS: if fnmatch(path_str, pattern): return True if fnmatch(path.name, pattern): return True return False def main() -> int: file_path = os.environ.get("OPENCODE_FILE_PATH", "") if not file_path: return 0 path = Path(file_path) if is_protected(path): print(f"BLOCKED: Cannot write to protected file: {file_path}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main())

Hook Debugging

Enable Debug Logging

bash
export OPENCODE_DEBUG=1 opencode

Test Hooks Manually

bash
OPENCODE_FILE_PATH=test.md python .opencode/hooks/validate-frontmatter/scripts/handler.py

Check Exit Codes

CodeMeaningAction
0PassContinue operation
1WarningLog, continue
2BlockStop operation, show error

Practice Questions

Practice Question

What happens when a blocking hook returns exit code 2?

Practice Question

Which hook event fires before a file is written?

Practice Question

What field in hook.json specifies which files trigger the hook?

Practice Question

How do you test a hook manually?

Practice Question

What is a guard hook used for?


Success

Key Takeaways

  • Hooks are scripts that run automatically at specific lifecycle points
  • The file.write event is ideal for validating content before saving
  • Use exit code 0 for pass, 1 for warning, 2 for blocking
  • Guard hooks protect sensitive files and dangerous operations
  • Hooks are registered in opencode.json under the hooks key
  • The pattern field uses glob syntax to match files
  • Test hooks manually by setting OPENCODE_FILE_PATH and running the script
Progress33%