-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathguard.py
More file actions
104 lines (86 loc) · 2.98 KB
/
Copy pathguard.py
File metadata and controls
104 lines (86 loc) · 2.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/env python3
"""Block `git commit` until /simplify runs. Each commit attempt spends one /simplify.
The marker lives in the per-worktree Git directory because /simplify reviews
the worktree's index. Claude Code mints it through the Skill event; Codex mints
it through the explicit completion signal at the end of the skill. Other
runtimes remain unblocked because they have no supported completion path.
"""
import json
import os
import shlex
import subprocess
import sys
from pathlib import Path
data = json.load(sys.stdin)
event = data.get("hook_event_name", "")
tool_input = data.get("tool_input") or {}
COMPLETION_SIGNAL = "echo simplify-guard:complete"
SHELL_OPERATORS = {";", "&&", "||", "|", "(", ")"}
GIT_OPTIONS_WITH_VALUES = {"-C", "-c", "--config-env", "--git-dir", "--namespace", "--work-tree"}
GIT_OPTIONS_WITHOUT_SUBCOMMAND = {
"-h",
"--exec-path",
"--help",
"--html-path",
"--info-path",
"--man-path",
"--version",
}
def marker(git=("git",)):
git_dir = subprocess.run(
[*git, "rev-parse", "--path-format=absolute", "--git-dir"],
capture_output=True,
cwd=data.get("cwd") or ".",
text=True,
).stdout.strip()
return Path(git_dir) / "simplify-guard.ok" if git_dir else None
def commit_prefix(command):
lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|()")
lexer.whitespace_split = True
tokens = list(lexer)
for start, arg in enumerate(tokens):
if Path(arg).name != "git":
continue
git = [arg]
for token in tokens[start + 1 :]:
if token in SHELL_OPERATORS:
break
if git[-1] in GIT_OPTIONS_WITH_VALUES:
git.append(token)
elif token == "commit":
return git
elif token in GIT_OPTIONS_WITHOUT_SUBCOMMAND or not token.startswith("-"):
break
else:
git.append(token)
def main():
is_claude = os.environ.get("CLAUDECODE") == "1"
is_codex = bool(data.get("turn_id"))
if event == "PostToolUse":
if is_claude and tool_input.get("skill") == "simplify" and (m := marker()):
m.touch()
return
if event != "PreToolUse" or not (is_claude or is_codex):
return
command = tool_input.get("command", "")
if is_codex and command.strip() == COMPLETION_SIGNAL:
if m := marker():
m.touch()
return
if not (git := commit_prefix(command)) or not (m := marker(git)):
return
if m.exists():
m.unlink() # spend the token before Git runs; every attempt needs a fresh review
return
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "simplify-guard: run /simplify on the staged diff first, then retry the commit.",
}
}
)
)
main()