Files
dev-workflows/scripts/hooks/bash_guard.py
T
EvandroandClaude Opus 5.5 768b5f6d83 feat: package as a Claude Code plugin with a deterministic quality gate
Workflows move to skills/<name>/SKILL.md so the repo installs as a plugin.
scripts/quality-gate runs only the checks of the requested tier on the diff
and reports missing tools as unverified instead of passing them. Hooks block
stopping on a failed or stale gate, guard destructive shell commands and
format edited files with the project's own formatter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 21:41:57 -03:00

49 lines
2.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""PreToolUse guard for shell commands: denies gate bypasses, asks before destructive git/db commands.
Claude Code: reads the hook JSON on stdin and prints a permissionDecision.
opencode: `bash_guard.py --check "<command>"` prints the reason and exits 2 when the command must not run.
"""
import json, re, sys
DENY = [
(r"(^|\s)--no-verify(\s|$)", "--no-verify pula os hooks do git. Corrija o que o hook reclamou em vez de pular."),
(r"\bgit\s+push\b(?=.*\s(--force|-f)(\s|$))(?!.*--force-with-lease)", "push --force reescreve histórico remoto. Use --force-with-lease e só com pedido explícito do usuário."),
(r"\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\s+(/|~|\$HOME|/\*|\.\.)(\s|/?$)", "rm -rf na raiz, no home ou no diretório pai não roda."),
]
ASK = [
(r"\bgit\s+reset\s+--hard\b", "git reset --hard descarta trabalho não commitado."),
(r"\bgit\s+clean\s+-[a-zA-Z]*f", "git clean -f apaga arquivos não rastreados."),
(r"\bgit\s+(checkout|restore)\s+(--\s+)?\.(\s|$)", "descarta todas as mudanças locais."),
(r"\b(DROP\s+(DATABASE|TABLE|SCHEMA)|TRUNCATE\s+TABLE)\b", "comando destrutivo de banco de dados."),
]
def decide(command):
for pattern, why in DENY:
if re.search(pattern, command, re.IGNORECASE):
return "deny", why
for pattern, why in ASK:
if re.search(pattern, command, re.IGNORECASE):
return "ask", why
return None, None
def main():
if len(sys.argv) > 2 and sys.argv[1] == "--check":
decision, why = decide(sys.argv[2])
if decision: # opencode has no "ask": both block, the agent must ask the user
print(f"[dev-workflows] {why} Peça confirmação ao usuário.")
return 2
return 0
event = json.load(sys.stdin)
decision, why = decide(event.get("tool_input", {}).get("command", ""))
if decision:
print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": decision,
"permissionDecisionReason": f"[dev-workflows] {why}"}}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())