A2UI Render Validation Hook
Validates A2UI JSON output before sending to client renderers.
Trigger
- Event:
PostToolUse - Pattern: Agent output contains A2UI JSON
Validation Rules
Schema Validation
-
Required Fields
type: "a2ui"(required)version(required, e.g., "0.8")components(required, non-empty array)
-
Component Validation
- Each component has unique
id - Each component has valid
type - Parent-child references are valid
- Required properties are present
- Each component has unique
-
Data Binding Validation
- Binding paths reference valid data keys
- No circular references
- Template syntax is valid
Implementation
#!/usr/bin/env python3
"""A2UI Render Validation Hook"""
import json
import sys
from typing import Any
VALID_TYPES = {
"container", "card", "grid", "stack",
"text", "image", "icon", "divider",
"button", "text-field", "select", "checkbox", "slider",
"list", "table", "chart"
}
def validate_a2ui(data: dict) -> list[str]:
"""Validate A2UI JSON structure."""
errors = []
# Required fields
if data.get("type") != "a2ui":
errors.append("Missing or invalid 'type' field (expected 'a2ui')")
if "version" not in data:
errors.append("Missing 'version' field")
components = data.get("components", [])
if not components:
errors.append("'components' array is empty or missing")
return errors
# Validate components
ids = set()
parent_refs = set()
for comp in components:
comp_id = comp.get("id")
comp_type = comp.get("type")
# Unique IDs
if not comp_id:
errors.append(f"Component missing 'id': {comp}")
elif comp_id in ids:
errors.append(f"Duplicate component id: {comp_id}")
else:
ids.add(comp_id)
# Valid type
if comp_type not in VALID_TYPES:
errors.append(f"Invalid component type '{comp_type}' for id '{comp_id}'")
# Track parent-child refs
for child_id in comp.get("children", []):
parent_refs.add(child_id)
# Validate parent-child references
for ref in parent_refs:
if ref not in ids:
errors.append(f"Invalid child reference: '{ref}' not found in components")
return errors
def main():
"""Hook entry point."""
# Read input from stdin (Claude Code hook protocol)
input_data = json.load(sys.stdin)
tool_result = input_data.get("tool_result", "")
# Check if output contains A2UI JSON
if '"type": "a2ui"' not in tool_result and '"type":"a2ui"' not in tool_result:
# Not A2UI output, allow
print(json.dumps({"decision": "allow"}))
return
# Extract and parse A2UI JSON
try:
# Find JSON in output
start = tool_result.find("{")
end = tool_result.rfind("}") + 1
if start >= 0 and end > start:
a2ui_json = json.loads(tool_result[start:end])
errors = validate_a2ui(a2ui_json)
if errors:
print(json.dumps({
"decision": "block",
"reason": f"A2UI validation failed: {'; '.join(errors)}"
}))
else:
print(json.dumps({"decision": "allow"}))
else:
print(json.dumps({"decision": "allow"}))
except json.JSONDecodeError:
print(json.dumps({
"decision": "block",
"reason": "Invalid JSON in A2UI output"
}))
if __name__ == "__main__":
main()
Configuration
Add to ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"pattern": "a2ui",
"command": "python3 ~/.coditect/hooks/a2ui-render-validation.py"
}
]
}
}
Validation Errors
| Error | Cause | Fix |
|---|---|---|
| Missing type | No "type": "a2ui" | Add required field |
| Duplicate ID | Two components same ID | Use unique IDs |
| Invalid type | Unknown component type | Use valid type from catalog |
| Invalid ref | Child ID doesn't exist | Fix parent-child refs |
| Invalid JSON | Malformed JSON | Fix JSON syntax |
When to Skip
- Non-A2UI outputs (auto-detected)
- Draft/preview mode
- Testing with
--no-validateflag
Version: 1.0.0 Created: 2026-01-13