Initial commit
This commit is contained in:
109
hooks/scripts/auto-changelog-updater.py
Executable file
109
hooks/scripts/auto-changelog-updater.py
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto Changelog Updater Hook
|
||||
|
||||
This hook automatically updates the changelog after git commits are made.
|
||||
It runs the update-changelog.py script in automatic mode to analyze recent
|
||||
commits and update the CHANGELOG.md file accordingly.
|
||||
|
||||
Hook Type: post_tool_use
|
||||
Triggers On: git commit commands
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
# Read the tool use data from stdin
|
||||
tool_data = json.load(sys.stdin)
|
||||
|
||||
# Check if this is a git commit command
|
||||
tool_name = tool_data.get("tool", "")
|
||||
|
||||
# We're looking for Bash tool with git commit commands
|
||||
if tool_name != "Bash":
|
||||
# Not a bash command, skip
|
||||
return 0
|
||||
|
||||
# Check if the command contains git commit
|
||||
command = tool_data.get("arguments", {}).get("command", "")
|
||||
if not command:
|
||||
return 0
|
||||
|
||||
# Check for various forms of git commit commands
|
||||
git_commit_patterns = [
|
||||
"git commit",
|
||||
"git commit -m",
|
||||
"git commit --message",
|
||||
"git commit -am",
|
||||
"git commit --amend",
|
||||
]
|
||||
|
||||
is_git_commit = any(pattern in command for pattern in git_commit_patterns)
|
||||
|
||||
if not is_git_commit:
|
||||
# Not a git commit command, skip
|
||||
return 0
|
||||
|
||||
# Check if the command was successful
|
||||
result = tool_data.get("result", {})
|
||||
if isinstance(result, dict):
|
||||
exit_code = result.get("exitCode", 0)
|
||||
if exit_code != 0:
|
||||
# Git commit failed, don't update changelog
|
||||
return 0
|
||||
|
||||
# Find the update-changelog.py script
|
||||
script_path = (
|
||||
Path(__file__).parent.parent.parent
|
||||
/ "scripts"
|
||||
/ "changelog"
|
||||
/ "update-changelog.py"
|
||||
)
|
||||
|
||||
if not script_path.exists():
|
||||
print(
|
||||
f"Warning: Changelog update script not found at {script_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
# Run the changelog update script in auto mode
|
||||
try:
|
||||
print(
|
||||
"\n🔄 Automatically updating changelog after git commit...", file=sys.stderr
|
||||
)
|
||||
|
||||
# Run the script with --auto flag
|
||||
result = subprocess.run(
|
||||
["python", str(script_path), "--auto"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=Path(__file__).parent.parent.parent, # Run from project root
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✅ Changelog updated successfully!", file=sys.stderr)
|
||||
if result.stdout:
|
||||
print(result.stdout, file=sys.stderr)
|
||||
else:
|
||||
print(
|
||||
f"⚠️ Changelog update completed with warnings (exit code: {result.returncode})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if result.stderr:
|
||||
print(f"Error output: {result.stderr}", file=sys.stderr)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error updating changelog: {e}", file=sys.stderr)
|
||||
# Don't fail the hook even if changelog update fails
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
118
hooks/scripts/auto_commit_on_changes.py
Executable file
118
hooks/scripts/auto_commit_on_changes.py
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.8"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_git_command(command: list[str]) -> subprocess.CompletedProcess:
|
||||
"""Run a git command and return the completed process."""
|
||||
try:
|
||||
return subprocess.run(
|
||||
command, capture_output=True, text=True, check=True, cwd=Path.cwd()
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Git command failed: {' '.join(command)}")
|
||||
print(f"Error: {e.stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def count_changed_files(max_count: int = 6) -> int:
|
||||
"""
|
||||
Count all changed files (staged, unstaged, and untracked) with early exit.
|
||||
Ignores files in .gitignore. Returns count up to max_count.
|
||||
"""
|
||||
changed_files = set()
|
||||
|
||||
try:
|
||||
# 1. Get unstaged changes (working tree vs index)
|
||||
result = subprocess.run(
|
||||
["git", "diff-files", "--name-only"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if result.stdout.strip():
|
||||
changed_files.update(result.stdout.strip().split("\n"))
|
||||
if len(changed_files) >= max_count:
|
||||
return max_count
|
||||
|
||||
# 2. Get staged changes (index vs HEAD)
|
||||
result = subprocess.run(
|
||||
["git", "diff-index", "--cached", "--name-only", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if result.stdout.strip():
|
||||
changed_files.update(result.stdout.strip().split("\n"))
|
||||
if len(changed_files) >= max_count:
|
||||
return max_count
|
||||
|
||||
# 3. Get untracked files (respects .gitignore)
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "--others", "--exclude-standard"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
if result.stdout.strip():
|
||||
changed_files.update(result.stdout.strip().split("\n"))
|
||||
|
||||
return min(len(changed_files), max_count)
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
# If git command fails, assume no changes
|
||||
return 0
|
||||
|
||||
|
||||
def check_git_repository() -> bool:
|
||||
"""Check if we're in a git repository."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "rev-parse", "--git-dir"], capture_output=True, check=True
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def request_claude_commit():
|
||||
"""Request Claude Code to make a commit by echoing the appropriate message."""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
commit_message = f"Auto-commit: 5+ file changes detected at {timestamp}"
|
||||
|
||||
# Echo a message that Claude Code can interpret as a commit request
|
||||
print(f"CLAUDE_COMMIT_REQUEST: {commit_message}")
|
||||
print("🔄 Requesting Claude Code to stage and commit changes...")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution function."""
|
||||
print("🔍 Checking for file changes...")
|
||||
|
||||
# Verify we're in a git repository
|
||||
if not check_git_repository():
|
||||
print("❌ Not in a git repository. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Count changed files with early exit at 6
|
||||
changed_count = count_changed_files(max_count=6)
|
||||
|
||||
print(f"📊 Found {changed_count} changed file(s)")
|
||||
|
||||
# Check if we hit the threshold
|
||||
if changed_count >= 5:
|
||||
print("🚨 Threshold reached: 5+ files changed")
|
||||
request_claude_commit()
|
||||
else:
|
||||
print(f"✅ Below threshold: {changed_count}/5 files changed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
256
hooks/scripts/commit-message-validator.py
Executable file
256
hooks/scripts/commit-message-validator.py
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class CommitMessageValidator:
|
||||
def __init__(self, input_data: dict[str, Any]):
|
||||
self.input = input_data
|
||||
self.valid_types = ["feat", "fix", "docs", "style", "refactor", "test", "chore"]
|
||||
|
||||
def validate(self) -> dict[str, Any]:
|
||||
"""Main validation entry point"""
|
||||
tool_name = self.input.get("tool_name")
|
||||
tool_input = self.input.get("tool_input", {})
|
||||
command = tool_input.get("command")
|
||||
|
||||
# Security: Basic input validation
|
||||
if command and not isinstance(command, str):
|
||||
return self.approve("Invalid command format")
|
||||
|
||||
# Only validate git commit commands
|
||||
if tool_name != "Bash" or not self.is_commit_command(command):
|
||||
return self.approve()
|
||||
|
||||
# Extract commit message from command
|
||||
message = self.extract_commit_message(command)
|
||||
if not message:
|
||||
return self.approve() # Can't validate without message
|
||||
|
||||
# Validate the commit message format
|
||||
validation = self.validate_message(message)
|
||||
|
||||
if validation["valid"]:
|
||||
return self.approve(validation["details"])
|
||||
else:
|
||||
return self.block(validation["errors"], validation["suggestions"])
|
||||
|
||||
def is_commit_command(self, command: str | None) -> bool:
|
||||
"""Check if command is a git commit"""
|
||||
return command and (
|
||||
"git commit" in command
|
||||
or "git cm" in command # common alias
|
||||
or "gc -m" in command # common alias
|
||||
)
|
||||
|
||||
def extract_commit_message(self, command: str) -> str:
|
||||
"""Extract commit message from command"""
|
||||
message = ""
|
||||
|
||||
# Format: git commit -m "message"
|
||||
single_quote_match = re.search(r"-m\s+'([^']+)'", command)
|
||||
double_quote_match = re.search(r'-m\s+"([^"]+)"', command)
|
||||
|
||||
# Format: git commit -m "$(cat <<'EOF'...EOF)"
|
||||
heredoc_match = re.search(
|
||||
r"cat\s*<<\s*['\"]?EOF['\"]?\s*([\s\S]*?)\s*EOF", command
|
||||
)
|
||||
|
||||
if single_quote_match:
|
||||
message = single_quote_match.group(1)
|
||||
elif double_quote_match:
|
||||
message = double_quote_match.group(1)
|
||||
elif heredoc_match:
|
||||
message = heredoc_match.group(1).strip()
|
||||
|
||||
# Get just the first line for conventional commit validation
|
||||
return message.split("\n")[0].strip()
|
||||
|
||||
def validate_message(self, message: str) -> dict[str, Any]:
|
||||
"""Validate commit message format"""
|
||||
errors = []
|
||||
suggestions = []
|
||||
details = []
|
||||
|
||||
# Check for empty message
|
||||
if not message:
|
||||
errors.append("Commit message cannot be empty")
|
||||
return {"valid": False, "errors": errors, "suggestions": suggestions}
|
||||
|
||||
# Check basic format: type(scope): subject or type: subject
|
||||
conventional_format = re.compile(r"^(\w+)(?:\(([^)]+)\))?:\s*(.+)$")
|
||||
match = conventional_format.match(message)
|
||||
|
||||
if not match:
|
||||
errors.append(
|
||||
"Commit message must follow conventional format: type(scope): subject"
|
||||
)
|
||||
suggestions.extend(
|
||||
[
|
||||
"Examples:",
|
||||
" feat(auth): add login functionality",
|
||||
" fix: resolve memory leak in provider list",
|
||||
" docs(api): update REST endpoint documentation",
|
||||
]
|
||||
)
|
||||
return {"valid": False, "errors": errors, "suggestions": suggestions}
|
||||
|
||||
type_, scope, subject = match.groups()
|
||||
|
||||
# Validate type
|
||||
if type_ not in self.valid_types:
|
||||
errors.append(f"Invalid commit type '{type_}'")
|
||||
suggestions.append(f"Valid types: {', '.join(self.valid_types)}")
|
||||
else:
|
||||
details.append(f"Type: {type_} ✓")
|
||||
|
||||
# Validate scope (optional but recommended for features)
|
||||
if scope:
|
||||
if len(scope) > 20:
|
||||
errors.append("Scope should be concise (max 20 characters)")
|
||||
else:
|
||||
details.append(f"Scope: {scope} ✓")
|
||||
elif type_ in ["feat", "fix"]:
|
||||
suggestions.append("Consider adding a scope for better context")
|
||||
|
||||
# Validate subject
|
||||
if subject:
|
||||
# Check first character is lowercase
|
||||
if re.match(r"^[A-Z]", subject):
|
||||
errors.append("Subject should start with lowercase letter")
|
||||
|
||||
# Check for ending punctuation
|
||||
if re.search(r"[.!?]$", subject):
|
||||
errors.append("Subject should not end with punctuation")
|
||||
|
||||
# Check length
|
||||
if len(subject) > 50:
|
||||
suggestions.append(
|
||||
f"Subject is {len(subject)} characters (recommended: max 50)"
|
||||
)
|
||||
|
||||
# Check for imperative mood (basic check)
|
||||
first_word = subject.split()[0]
|
||||
past_tense_words = [
|
||||
"added",
|
||||
"updated",
|
||||
"fixed",
|
||||
"removed",
|
||||
"implemented",
|
||||
"created",
|
||||
"deleted",
|
||||
"improved",
|
||||
"refactored",
|
||||
"changed",
|
||||
"moved",
|
||||
"renamed",
|
||||
]
|
||||
|
||||
if first_word.lower() in past_tense_words:
|
||||
errors.append(
|
||||
'Use imperative mood in subject (e.g., "add" not "added")'
|
||||
)
|
||||
|
||||
if not errors:
|
||||
details.append(f'Subject: "{subject}" ✓')
|
||||
else:
|
||||
errors.append("Subject cannot be empty")
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"suggestions": suggestions,
|
||||
"details": details,
|
||||
}
|
||||
|
||||
def approve(self, details: list[str] | None = None) -> dict[str, Any]:
|
||||
"""Approve the operation"""
|
||||
message = "✅ Commit message validation passed"
|
||||
if details:
|
||||
message += "\n" + "\n".join(details)
|
||||
|
||||
return {"approve": True, "message": message}
|
||||
|
||||
def block(self, errors: list[str], suggestions: list[str]) -> dict[str, Any]:
|
||||
"""Block the operation due to invalid format"""
|
||||
message_parts = [
|
||||
"❌ Invalid commit message format:",
|
||||
*[f" - {e}" for e in errors],
|
||||
"",
|
||||
*[f" {s}" for s in suggestions],
|
||||
"",
|
||||
"Commit format: type(scope): subject",
|
||||
"",
|
||||
"Types:",
|
||||
" feat - New feature",
|
||||
" fix - Bug fix",
|
||||
" docs - Documentation only",
|
||||
" style - Code style changes",
|
||||
" refactor - Code refactoring",
|
||||
" test - Add/update tests",
|
||||
" chore - Maintenance tasks",
|
||||
"",
|
||||
"Example: feat(providers): add location filter to provider list",
|
||||
]
|
||||
|
||||
return {"approve": False, "message": "\n".join(message_parts)}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main execution"""
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Comprehensive logging functionality
|
||||
# Ensure log directory exists
|
||||
log_dir = Path.cwd() / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_path = log_dir / "commit_message_validator.json"
|
||||
|
||||
# Read existing log data or initialize empty list
|
||||
if log_path.exists():
|
||||
with open(log_path) as f:
|
||||
try:
|
||||
log_data = json.load(f)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
log_data = []
|
||||
else:
|
||||
log_data = []
|
||||
|
||||
# Add timestamp to the log entry
|
||||
timestamp = datetime.now().strftime("%b %d, %I:%M%p").lower()
|
||||
input_data["timestamp"] = timestamp
|
||||
|
||||
# Process validation and get results
|
||||
validator = CommitMessageValidator(input_data)
|
||||
result = validator.validate()
|
||||
|
||||
# Add validation result to log entry
|
||||
input_data["validation_result"] = result
|
||||
|
||||
# Append new data to log
|
||||
log_data.append(input_data)
|
||||
|
||||
# Write back to file with formatting
|
||||
with open(log_path, "w") as f:
|
||||
json.dump(log_data, f, indent=2)
|
||||
|
||||
print(json.dumps(result))
|
||||
except Exception as error:
|
||||
print(
|
||||
json.dumps({"approve": True, "message": f"Commit validator error: {error}"})
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
86
hooks/scripts/prevent-direct-push.py
Executable file
86
hooks/scripts/prevent-direct-push.py
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
command = tool_input.get("command", "")
|
||||
|
||||
# Only validate git push commands
|
||||
if tool_name != "Bash" or "git push" not in command:
|
||||
sys.exit(0)
|
||||
|
||||
# Get current branch
|
||||
try:
|
||||
current_branch = subprocess.check_output(
|
||||
["git", "branch", "--show-current"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True
|
||||
).strip()
|
||||
except:
|
||||
current_branch = ""
|
||||
|
||||
# Check if pushing to main or develop
|
||||
push_cmd = command
|
||||
is_force_push = "--force" in push_cmd or "-f" in push_cmd
|
||||
|
||||
# Check if command or current branch targets protected branches
|
||||
targets_protected = (
|
||||
"origin main" in push_cmd or
|
||||
"origin develop" in push_cmd or
|
||||
current_branch in ["main", "develop"]
|
||||
)
|
||||
|
||||
# Block direct push to main/develop (unless force push which is already dangerous)
|
||||
if targets_protected and not is_force_push:
|
||||
if current_branch in ["main", "develop"] or "origin main" in push_cmd or "origin develop" in push_cmd:
|
||||
reason = f"""❌ Direct push to main/develop is not allowed!
|
||||
|
||||
Protected branches:
|
||||
- main (production)
|
||||
- develop (integration)
|
||||
|
||||
Git Flow workflow:
|
||||
1. Create a feature branch:
|
||||
/feature <name>
|
||||
|
||||
2. Make your changes and commit
|
||||
|
||||
3. Push feature branch:
|
||||
git push origin feature/<name>
|
||||
|
||||
4. Create pull request:
|
||||
gh pr create
|
||||
|
||||
5. After approval, merge with:
|
||||
/finish
|
||||
|
||||
For releases:
|
||||
/release <version> → PR → /finish
|
||||
|
||||
For hotfixes:
|
||||
/hotfix <name> → PR → /finish
|
||||
|
||||
Current branch: {current_branch}
|
||||
|
||||
💡 Use feature/release/hotfix branches instead of pushing directly to main/develop."""
|
||||
|
||||
output = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason
|
||||
}
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# Allow the command
|
||||
sys.exit(0)
|
||||
96
hooks/scripts/validate-branch-name.py
Executable file
96
hooks/scripts/validate-branch-name.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
command = tool_input.get("command", "")
|
||||
|
||||
# Only validate git checkout -b commands
|
||||
if tool_name != "Bash" or "git checkout -b" not in command:
|
||||
sys.exit(0)
|
||||
|
||||
# Extract branch name
|
||||
match = re.search(r'git checkout -b\s+([^\s]+)', command)
|
||||
if not match:
|
||||
sys.exit(0)
|
||||
|
||||
branch_name = match.group(1)
|
||||
|
||||
# Allow main and develop branches
|
||||
if branch_name in ["main", "develop"]:
|
||||
sys.exit(0)
|
||||
|
||||
# Validate Git Flow naming convention
|
||||
if not re.match(r'^(feature|release|hotfix)/', branch_name):
|
||||
reason = f"""❌ Invalid Git Flow branch name: {branch_name}
|
||||
|
||||
Git Flow branches must follow these patterns:
|
||||
• feature/<descriptive-name>
|
||||
• release/v<MAJOR>.<MINOR>.<PATCH>
|
||||
• hotfix/<descriptive-name>
|
||||
|
||||
Examples:
|
||||
✅ feature/user-authentication
|
||||
✅ release/v1.2.0
|
||||
✅ hotfix/critical-security-fix
|
||||
|
||||
Invalid:
|
||||
❌ {branch_name} (missing Git Flow prefix)
|
||||
❌ feat/something (use 'feature/' not 'feat/')
|
||||
❌ fix/bug (use 'hotfix/' not 'fix/')
|
||||
|
||||
💡 Use Git Flow commands instead:
|
||||
/feature <name> - Create feature branch
|
||||
/release <version> - Create release branch
|
||||
/hotfix <name> - Create hotfix branch"""
|
||||
|
||||
output = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason
|
||||
}
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# Validate release version format
|
||||
if branch_name.startswith("release/"):
|
||||
if not re.match(r'^release/v\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$', branch_name):
|
||||
reason = f"""❌ Invalid release version: {branch_name}
|
||||
|
||||
Release branches must follow semantic versioning:
|
||||
release/vMAJOR.MINOR.PATCH[-prerelease]
|
||||
|
||||
Valid examples:
|
||||
✅ release/v1.0.0
|
||||
✅ release/v2.1.3
|
||||
✅ release/v1.0.0-beta.1
|
||||
|
||||
Invalid:
|
||||
❌ release/1.0.0 (missing 'v' prefix)
|
||||
❌ release/v1.0 (incomplete version)
|
||||
❌ {branch_name}
|
||||
|
||||
💡 Use: /release v1.2.0"""
|
||||
|
||||
output = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason
|
||||
}
|
||||
}
|
||||
print(json.dumps(output))
|
||||
sys.exit(0)
|
||||
|
||||
# Allow the command
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user