This commit is contained in:
2025-09-16 21:33:47 +00:00
parent 6e06f38d5d
commit 8d6454b749

View File

@@ -374,7 +374,12 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
# Only analyze for write/edit tools
if tool_name not in ["Write", "Edit", "MultiEdit"]:
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
# Extract content based on tool type
content = None
@@ -390,11 +395,21 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
# Only analyze Python files
if not file_path or not file_path.endswith(".py") or not content:
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
# Skip analysis for configured patterns
if should_skip_file(file_path, config):
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
try:
# Store state if tracking enabled
@@ -415,31 +430,59 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
# Make decision based on enforcement mode
if config.enforcement_mode == "strict":
return {"decision": "deny", "message": message}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": message
}
}
if config.enforcement_mode == "warn":
return {"decision": "ask", "message": message}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": message
}
}
# permissive
return {
"decision": "allow",
"message": f"⚠️ Quality Warning:\n{message}",
"systemMessage": f"⚠️ Quality Warning:\n{message}",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
else:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
return {"decision": "allow"} # noqa: TRY300
except Exception as e: # noqa: BLE001
return {
"decision": "allow",
"message": f"Warning: Code quality check failed with error: {e}",
"systemMessage": f"Warning: Code quality check failed with error: {e}",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
"""Handle PostToolUse hook - verify quality after write/edit."""
tool_name = hook_data.get("tool_name", "")
tool_output = hook_data.get("tool_output", {})
tool_output = hook_data.get("tool_response", {})
# Only process write/edit tools
if tool_name not in ["Write", "Edit", "MultiEdit"]:
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse"
}
}
# Extract file path from output
file_path = None
@@ -451,10 +494,18 @@ def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
file_path = match[1]
if not file_path or not file_path.endswith(".py"):
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse"
}
}
if not Path(file_path).exists():
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse"
}
}
issues = []
@@ -479,14 +530,27 @@ def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict:
f"📝 Post-write quality notes for {Path(file_path).name}:\n"
+ "\n".join(issues)
)
return {"decision": "allow", "message": message}
if config.show_success:
return {
"decision": "allow",
"message": f"{Path(file_path).name} passed post-write verification",
"systemMessage": message,
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": message
}
}
if config.show_success:
message = f"{Path(file_path).name} passed post-write verification"
return {
"systemMessage": message,
"hookSpecificOutput": {
"hookEventName": "PostToolUse"
}
}
return {"decision": "allow"}
return {
"hookSpecificOutput": {
"hookEventName": "PostToolUse"
}
}
def main() -> None:
@@ -499,11 +563,16 @@ def main() -> None:
try:
hook_data = json.load(sys.stdin)
except json.JSONDecodeError:
print(json.dumps({"decision": "allow"})) # noqa: T201
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
})) # noqa: T201
return
# Detect hook type based on tool_output (PostToolUse) vs tool_input (PreToolUse)
if "tool_output" in hook_data:
# Detect hook type based on tool_response (PostToolUse) vs tool_input (PreToolUse)
if "tool_response" in hook_data:
# PostToolUse hook
response = posttooluse_hook(hook_data, config)
else:
@@ -512,16 +581,19 @@ def main() -> None:
print(json.dumps(response)) # noqa: T201
# Handle exit codes according to Claude Code spec
if response.get("decision") == "deny":
# Handle exit codes based on hook output
hook_output = response.get("hookSpecificOutput", {})
permission_decision = hook_output.get("permissionDecision")
if permission_decision == "deny":
# Exit code 2: Blocking error - stderr fed back to Claude
if "message" in response:
sys.stderr.write(response["message"])
reason = hook_output.get("permissionDecisionReason", "Permission denied")
sys.stderr.write(reason)
sys.exit(2)
elif response.get("decision") == "ask":
elif permission_decision == "ask":
# Also use exit code 2 for ask decisions to ensure Claude sees the message
if "message" in response:
sys.stderr.write(response["message"])
reason = hook_output.get("permissionDecisionReason", "Permission request")
sys.stderr.write(reason)
sys.exit(2)
# Exit code 0: Success (default)