-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhook_factory.py
More file actions
182 lines (167 loc) · 7.08 KB
/
Copy pathhook_factory.py
File metadata and controls
182 lines (167 loc) · 7.08 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/usr/bin/env python3
"""Hook Factory CLI: generate and manage AI agent lifecycle hooks."""
import argparse, json, os, sys, subprocess
SETTINGS_PATH = ".claude/settings.json"
SCRIPTS_DIR = ".claude/scripts"
CATEGORIES = {
"Safety": [
("Block dangerous commands", "PreToolUse", "Bash",
"Blocks shell commands matching a regex pattern."),
("Protect critical paths", "PreToolUse", "Write|Edit",
"Blocks writes to paths matching a protected pattern."),
("Rate-limit shell calls", "PreToolUse", "Bash",
"Enforces a cooldown between Bash tool calls."),
("Sandbox enforcement", "PreToolUse", "Bash",
"Restricts commands to allowed directories."),
],
"Quality": [
("Auto-format on write", "PostToolUse", "Write|Edit",
"Runs a formatter on files after every write."),
("Auto-lint on write", "PostToolUse", "Write|Edit",
"Runs a linter on files after every write."),
("Auto-test on edit", "PostToolUse", "Edit",
"Runs related tests after every file edit."),
("Dependency version check", "PostToolUse", "Bash",
"Checks for pinned dependency changes."),
],
"Notification": [
("Desktop alert", "Notification", ".*",
"Sends a desktop notification on agent events."),
("Sound alert", "Notification", ".*",
"Plays a sound on agent events."),
("Webhook / Slack", "Notification", ".*",
"POSTs a webhook payload on agent events."),
("Email summary", "Notification", ".*",
"Sends an email summary on agent events."),
],
"Context": [
("Inject project context", "PreCompact", ".*",
"Injects project docs into session before compaction."),
("Load environment variables", "PreToolUse", "Bash",
"Injects env vars into shell commands."),
("Attach documentation", "PreToolUse", ".*",
"Attaches relevant docs to tool calls."),
],
"Logging": [
("Command log", "PostToolUse", "Bash",
"Logs every shell command and its output."),
("Session transcript", "PostToolUse", ".*",
"Records a full transcript of all tool usage."),
("Error tracker", "PostToolUse", ".*",
"Logs errors and failed tool calls to a file."),
],
"Advanced": [
("Custom script", "PreToolUse", ".*",
"Runs a custom script with full tool input."),
],
}
C = {"G": "\033[32m", "R": "\033[31m", "Y": "\033[33m", "B": "\033[34m", "X": "\033[0m"}
def load_settings():
if os.path.exists(SETTINGS_PATH):
with open(SETTINGS_PATH) as f:
return json.load(f)
return {}
def save_settings(data):
os.makedirs(os.path.dirname(SETTINGS_PATH), exist_ok=True)
with open(SETTINGS_PATH, "w") as f:
json.dump(data, f, indent=2)
def get_hooks(settings):
return settings.get("hooks", [])
def cmd_init(args):
if os.path.exists(SETTINGS_PATH):
s = load_settings()
if "hooks" in s:
print(f"{C['Y']}Settings already exist with {len(s['hooks'])} hooks.{C['X']}")
return
else:
s = {}
s["hooks"] = s.get("hooks", [])
save_settings(s)
print(f"{C['G']}Initialized {SETTINGS_PATH} with empty hooks array.{C['X']}")
def cmd_list(args):
hooks = get_hooks(load_settings())
if not hooks:
print(f"{C['Y']}Zero hooks installed. Run 'add' to create one.{C['X']}")
return
for i, h in enumerate(hooks):
print(f" {C['B']}[{i}]{C['X']} {h.get('type','?'):14s} "
f"{h.get('matcher','?'):12s} {h.get('command','?')}")
def cmd_add(args):
settings = load_settings()
cats = list(CATEGORIES.keys())
print(f"\n{C['B']}Categories:{C['X']}")
for i, cat in enumerate(cats):
print(f" {i+1}. {cat}")
ci = int(input(f"\n{C['Y']}Pick category (1-{len(cats)}): {C['X']}")) - 1
cat = cats[ci]
hooks_in_cat = CATEGORIES[cat]
print(f"\n{C['B']}{cat} hooks:{C['X']}")
for i, (name, *_ ) in enumerate(hooks_in_cat):
print(f" {i+1}. {name}")
hi = int(input(f"\n{C['Y']}Pick hook (1-{len(hooks_in_cat)}): {C['X']}")) - 1
name, htype, matcher, desc = hooks_in_cat[hi]
detail = input(f"{C['Y']}Enter detail (pattern/path/cmd): {C['X']}").strip()
os.makedirs(SCRIPTS_DIR, exist_ok=True)
script_name = name.lower().replace(" ", "_").replace("/", "")
script_path = f"{SCRIPTS_DIR}/{script_name}.py"
with open(script_path, "w") as sf:
sf.write(f'#!/usr/bin/env python3\n"""{name} hook helper."""\n')
sf.write(f'import sys, json\ndata = json.load(sys.stdin)\n')
sf.write(f'print(json.dumps(data, indent=2))\n')
os.chmod(script_path, 0o755)
entry = {"type": htype, "matcher": matcher,
"command": f"python3 {script_path}"}
settings.setdefault("hooks", []).append(entry)
save_settings(settings)
print(f"{C['G']}Added {htype} hook: {name}{C['X']}")
print(f" Matcher: {matcher}")
print(f" Script: {script_path}")
def cmd_remove(args):
settings = load_settings()
hooks = get_hooks(settings)
if args.index >= len(hooks):
print(f"{C['R']}Index {args.index} out of range.{C['X']}"); return
removed = hooks.pop(args.index)
settings["hooks"] = hooks
save_settings(settings)
print(f"{C['R']}Removed hook [{args.index}]: {removed.get('command','?')}{C['X']}")
def cmd_test(args):
hooks = get_hooks(load_settings())
if args.index >= len(hooks):
print(f"{C['R']}Index {args.index} out of range.{C['X']}"); return
hook = hooks[args.index]
cmd_str = hook.get("command", "")
inp = args.input or "{}"
print(f"{C['B']}Testing hook [{args.index}]: {cmd_str}{C['X']}")
print(f"{C['Y']}Input: {inp}{C['X']}")
try:
result = subprocess.run(cmd_str, shell=True, input=inp,
capture_output=True, text=True, timeout=10)
print(f"Exit code: {result.returncode}")
if result.stdout: print(f"stdout: {result.stdout.strip()}")
if result.stderr: print(f"stderr: {result.stderr.strip()}")
except Exception as e:
print(f"{C['R']}Error: {e}{C['X']}")
def cmd_export(args):
hooks = get_hooks(load_settings())
print(json.dumps({"hooks": hooks}, indent=2))
def main():
p = argparse.ArgumentParser(description="Hook Factory CLI")
sub = p.add_subparsers(dest="command")
sub.add_parser("init", help="Initialize settings.json")
sub.add_parser("list", help="List all hooks")
sub.add_parser("add", help="Add a hook interactively")
rm = sub.add_parser("remove", help="Remove a hook by index")
rm.add_argument("index", type=int)
t = sub.add_parser("test", help="Dry-run a hook against sample input")
t.add_argument("--index", type=int, required=True)
t.add_argument("--input", default=None)
sub.add_parser("export", help="Export hooks.json to stdout")
args = p.parse_args()
cmds = {"init": cmd_init, "list": cmd_list, "add": cmd_add,
"remove": cmd_remove, "test": cmd_test, "export": cmd_export}
fn = cmds.get(args.command)
if fn: fn(args)
else: p.print_help()
if __name__ == "__main__":
main()