Skip to content

Commit 1c90b40

Browse files
author
codejunkie99
committed
Merge branch 'codex/gateflow-stop-hook-1.5.2'
2 parents 3a2908e + e850ed4 commit 1c90b40

4 files changed

Lines changed: 246 additions & 6 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
# Node
88
node_modules/
99

10+
# Python
11+
__pycache__/
12+
*.py[cod]
13+
1014
# Simulation output
1115
*.vcd
1216
obj_dir/

plugins/gateflow/hooks/hooks.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
{
1616
"hooks": [
1717
{
18-
"type": "prompt",
19-
"prompt": "Is this prompt CLEARLY asking to CREATE, DEBUG, TEST, SIMULATE, or MODIFY SystemVerilog/Verilog/RTL hardware code? Return ONLY a JSON object. If YES (actual hardware design work): {\"systemMessage\": \"SystemVerilog task detected. Consider using /gf to plan first.\"}. If NO or UNSURE: return exactly {}. NEVER block non-SV prompts.",
20-
"timeout": 10
18+
"type": "command",
19+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/userpromptsubmit-sv-nudge.py",
20+
"timeout": 2
2121
}
2222
]
2323
}
@@ -27,9 +27,9 @@
2727
"matcher": "Bash",
2828
"hooks": [
2929
{
30-
"type": "prompt",
31-
"prompt": "Check if this Bash command could destructively affect SystemVerilog/Verilog files (.sv, .svh, .v, .vh). Look for: rm/delete of SV files, git checkout/reset that discards SV changes, overwriting SV files with redirects. Return ONLY a JSON object. If destructive: {\"hookSpecificOutput\": {\"permissionDecision\": \"ask\"}, \"systemMessage\": \"⚠️ This command may delete or overwrite SystemVerilog files. Confirm with the user before proceeding.\"}. If safe: {}.",
32-
"timeout": 10
30+
"type": "command",
31+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pretooluse-bash-guard.py",
32+
"timeout": 2
3333
}
3434
]
3535
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python3
2+
"""GateFlow PreToolUse hook guard for Bash commands.
3+
4+
This is intentionally deterministic (no LLM call) to avoid noisy hook failures.
5+
It asks for confirmation when a Bash command appears likely to delete/overwrite
6+
SystemVerilog/Verilog source files.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import json
12+
import os
13+
import re
14+
import sys
15+
from typing import Any, Dict
16+
17+
18+
SV_EXT_RE = re.compile(r"\.(?:svh?|vh?|v)(?=$|[^A-Za-z0-9_])", re.IGNORECASE)
19+
20+
21+
def _looks_like_sv_project(cwd: str) -> bool:
22+
# Keep this cheap: check only a couple common directories.
23+
candidates = [cwd, os.path.join(cwd, "rtl"), os.path.join(cwd, "tb")]
24+
for d in candidates:
25+
try:
26+
for name in os.listdir(d):
27+
lower = name.lower()
28+
if lower.endswith((".sv", ".svh", ".v", ".vh")):
29+
return True
30+
except Exception:
31+
continue
32+
return False
33+
34+
35+
def _has_sv_path(cmd: str) -> bool:
36+
return SV_EXT_RE.search(cmd) is not None
37+
38+
39+
def _is_destructive_git(cmd_l: str, sv_project: bool, mentions_sv: bool) -> bool:
40+
if "git " not in cmd_l:
41+
return False
42+
43+
# These are destructive for the working tree; we only trigger if we can
44+
# reasonably believe it's an SV project, or the command explicitly mentions
45+
# SV/V file paths.
46+
if (sv_project or mentions_sv) and "git reset" in cmd_l and "--hard" in cmd_l:
47+
return True
48+
49+
if (sv_project or mentions_sv) and "git clean" in cmd_l and (
50+
" -f" in cmd_l or "--force" in cmd_l
51+
):
52+
return True
53+
54+
if "git checkout" in cmd_l and (sv_project or mentions_sv):
55+
# `git checkout -- <path>` or forced checkout are most likely to discard changes.
56+
if " -- " in cmd_l or " checkout --" in cmd_l or " -f" in cmd_l or "--force" in cmd_l:
57+
return True
58+
59+
if "git restore" in cmd_l:
60+
# Restore is inherently a revert; only ask when it targets SV/V or in an SV project
61+
# with forceful flags.
62+
if mentions_sv:
63+
return True
64+
if sv_project and ("--staged" in cmd_l or "--source" in cmd_l or "--worktree" in cmd_l):
65+
return True
66+
67+
return False
68+
69+
70+
def _overwrites_via_redirection(cmd: str, mentions_sv: bool) -> bool:
71+
if not mentions_sv:
72+
return False
73+
# Catch: `> file.sv`, `>> file.sv`, `>| file.sv`, and common `tee file.sv`.
74+
if re.search(r"(?:^|[^0-9])>>?\s*[^\\n]*\.(?:svh?|vh?|v)(?=$|[^A-Za-z0-9_])", cmd, re.IGNORECASE):
75+
return True
76+
if re.search(r"\btee\b[^\n]*\.(?:svh?|vh?|v)(?=$|[^A-Za-z0-9_])", cmd, re.IGNORECASE):
77+
return True
78+
return False
79+
80+
81+
def _deletes_sv(cmd_l: str, sv_project: bool, mentions_sv: bool) -> bool:
82+
if "rm" not in cmd_l:
83+
return False
84+
85+
# Directly references SV/V files.
86+
if mentions_sv and re.search(r"\brm\b", cmd_l):
87+
return True
88+
89+
# Recursively deleting common SV directories.
90+
if sv_project and re.search(r"\brm\b[^\n]*(?:-rf|-fr|--recursive)[^\n]*(?:\brtl\b|\btb\b|/rtl/|/tb/)", cmd_l):
91+
return True
92+
93+
return False
94+
95+
96+
def _should_ask(cmd: str, cwd: str) -> bool:
97+
cmd_l = cmd.lower()
98+
mentions_sv = _has_sv_path(cmd)
99+
sv_project = _looks_like_sv_project(cwd) if cwd else False
100+
101+
return (
102+
_overwrites_via_redirection(cmd, mentions_sv)
103+
or _deletes_sv(cmd_l, sv_project, mentions_sv)
104+
or _is_destructive_git(cmd_l, sv_project, mentions_sv)
105+
)
106+
107+
108+
def main() -> int:
109+
try:
110+
payload: Dict[str, Any] = json.load(sys.stdin)
111+
except Exception:
112+
# Malformed input: do nothing (never block tool usage due to hook failure).
113+
sys.stdout.write("{}")
114+
return 0
115+
116+
tool_name = payload.get("tool_name", "")
117+
tool_input = payload.get("tool_input", {}) or {}
118+
cmd = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
119+
cwd = payload.get("cwd", "") or ""
120+
121+
if tool_name != "Bash" or not isinstance(cmd, str) or not cmd.strip():
122+
sys.stdout.write("{}")
123+
return 0
124+
125+
if _should_ask(cmd, cwd):
126+
out: Dict[str, Any] = {
127+
"hookSpecificOutput": {"permissionDecision": "ask"},
128+
"systemMessage": (
129+
"This command looks like it may delete/overwrite .sv/.svh/.v/.vh files. "
130+
"Please confirm before I run it."
131+
),
132+
}
133+
sys.stdout.write(json.dumps(out))
134+
return 0
135+
136+
sys.stdout.write("{}")
137+
return 0
138+
139+
140+
if __name__ == "__main__":
141+
raise SystemExit(main())
142+
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env python3
2+
"""GateFlow UserPromptSubmit hook (deterministic).
3+
4+
This replaces a prompt-based hook which could return empty/non-JSON (or an
5+
unexpected schema like {"ok": false, ...}) and block user prompts.
6+
7+
Behavior:
8+
- Never blocks user prompts.
9+
- Emits a small systemMessage nudge only when the prompt looks SV/V related.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import json
15+
import re
16+
import sys
17+
from typing import Any, Dict
18+
19+
20+
SV_EXT_RE = re.compile(r"\.(?:svh?|vh?|v)(?=$|[^A-Za-z0-9_])", re.IGNORECASE)
21+
GF_CMD_RE = re.compile(r"(^|\\s)/gf(?:\\s|$|-)", re.IGNORECASE)
22+
23+
SV_KEYWORDS = (
24+
"systemverilog",
25+
"verilog",
26+
"rtl",
27+
"testbench",
28+
"verilator",
29+
"vcd",
30+
"waveform",
31+
"uvm",
32+
"sva",
33+
"always_ff",
34+
"always_comb",
35+
"always_latch",
36+
)
37+
38+
39+
def _extract_prompt(payload: Dict[str, Any]) -> str:
40+
for key in ("prompt", "user_prompt", "userPrompt", "input"):
41+
val = payload.get(key)
42+
if isinstance(val, str) and val.strip():
43+
return val
44+
45+
for key in ("hook", "event", "params", "data"):
46+
node = payload.get(key)
47+
if not isinstance(node, dict):
48+
continue
49+
for sub in ("prompt", "user_prompt", "userPrompt", "input"):
50+
val = node.get(sub)
51+
if isinstance(val, str) and val.strip():
52+
return val
53+
54+
return ""
55+
56+
57+
def _looks_like_sv_prompt(prompt: str) -> bool:
58+
if SV_EXT_RE.search(prompt):
59+
return True
60+
lower = prompt.lower()
61+
return any(k in lower for k in SV_KEYWORDS)
62+
63+
64+
def main() -> int:
65+
try:
66+
payload: Dict[str, Any] = json.load(sys.stdin)
67+
except Exception:
68+
sys.stdout.write("{}")
69+
return 0
70+
71+
prompt = _extract_prompt(payload)
72+
if not prompt:
73+
sys.stdout.write("{}")
74+
return 0
75+
76+
# If the user is explicitly using GateFlow commands, don't spam.
77+
if GF_CMD_RE.search(prompt):
78+
sys.stdout.write("{}")
79+
return 0
80+
81+
if _looks_like_sv_prompt(prompt):
82+
out = {
83+
"systemMessage": "SystemVerilog task detected. Consider using /gf to plan first."
84+
}
85+
sys.stdout.write(json.dumps(out))
86+
return 0
87+
88+
sys.stdout.write("{}")
89+
return 0
90+
91+
92+
if __name__ == "__main__":
93+
raise SystemExit(main())
94+

0 commit comments

Comments
 (0)