failsafe-hook 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- failsafe_hook/__init__.py +0 -0
- failsafe_hook/__main__.py +15 -0
- failsafe_hook/_install.py +164 -0
- failsafe_hook/core.py +1712 -0
- failsafe_hook-1.0.0.dist-info/METADATA +313 -0
- failsafe_hook-1.0.0.dist-info/RECORD +10 -0
- failsafe_hook-1.0.0.dist-info/WHEEL +5 -0
- failsafe_hook-1.0.0.dist-info/entry_points.txt +2 -0
- failsafe_hook-1.0.0.dist-info/licenses/LICENSE +21 -0
- failsafe_hook-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
args = sys.argv[1:]
|
|
7
|
+
if args or sys.stdin.isatty():
|
|
8
|
+
from failsafe_hook._install import main as _main
|
|
9
|
+
else:
|
|
10
|
+
from failsafe_hook.core import main as _main
|
|
11
|
+
_main()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FailSafe installer logic.
|
|
4
|
+
|
|
5
|
+
Invoked via:
|
|
6
|
+
python -m failsafe_hook
|
|
7
|
+
python -m failsafe_hook --install
|
|
8
|
+
python -m failsafe_hook --uninstall
|
|
9
|
+
python -m failsafe_hook --check
|
|
10
|
+
"""
|
|
11
|
+
import importlib.util
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
HOOK_MATCHER = "Bash"
|
|
17
|
+
HOOK_MARKER = "failsafe" # matches both old path-based and new module-based commands
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _settings_path():
|
|
21
|
+
return os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_command():
|
|
25
|
+
return "python3 -m failsafe_hook 2>/dev/null || python -m failsafe_hook 2>/dev/null || true"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_settings(path):
|
|
29
|
+
if not os.path.isfile(path):
|
|
30
|
+
return {}
|
|
31
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _write_settings(path, data):
|
|
36
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
37
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
38
|
+
json.dump(data, f, indent=2)
|
|
39
|
+
f.write("\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_hook(pre_tool_use):
|
|
43
|
+
"""Return (bash_entry, hook_entry) if FailSafe is already installed, else (None, None)."""
|
|
44
|
+
for entry in pre_tool_use:
|
|
45
|
+
if entry.get("matcher") != HOOK_MATCHER:
|
|
46
|
+
continue
|
|
47
|
+
for h in entry.get("hooks") or []:
|
|
48
|
+
if HOOK_MARKER in h.get("command", ""):
|
|
49
|
+
return entry, h
|
|
50
|
+
return None, None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def install():
|
|
54
|
+
try:
|
|
55
|
+
if importlib.util.find_spec("failsafe_hook") is None:
|
|
56
|
+
raise ImportError
|
|
57
|
+
except Exception:
|
|
58
|
+
print("ERROR: failsafe_hook package is not installed.")
|
|
59
|
+
print("Install it first:")
|
|
60
|
+
print(" python -m pip install -e . (from repo root, for development)")
|
|
61
|
+
print(" python -m pip install failsafe-hook (from PyPI)")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
sp = _settings_path()
|
|
65
|
+
try:
|
|
66
|
+
settings = _read_settings(sp)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"ERROR: Could not read {sp}: {e}")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
hooks = settings.setdefault("hooks", {})
|
|
72
|
+
pre = hooks.setdefault("PreToolUse", [])
|
|
73
|
+
bash_entry, existing = _find_hook(pre)
|
|
74
|
+
|
|
75
|
+
if existing:
|
|
76
|
+
print("FailSafe is already installed.")
|
|
77
|
+
print(f" settings : {sp}")
|
|
78
|
+
print(f" command : {existing['command']}")
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
if bash_entry is None:
|
|
82
|
+
bash_entry = {"matcher": HOOK_MATCHER, "hooks": []}
|
|
83
|
+
pre.append(bash_entry)
|
|
84
|
+
|
|
85
|
+
bash_entry.setdefault("hooks", []).append({
|
|
86
|
+
"type": "command",
|
|
87
|
+
"timeout": 12,
|
|
88
|
+
"command": _build_command(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
_write_settings(sp, settings)
|
|
92
|
+
|
|
93
|
+
print("FailSafe installed.")
|
|
94
|
+
print(f" settings : {sp}")
|
|
95
|
+
print(f" command : {_build_command()}")
|
|
96
|
+
print()
|
|
97
|
+
print("Runs automatically on every Claude Code session.")
|
|
98
|
+
print("To remove: python -m failsafe_hook --uninstall")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def uninstall():
|
|
102
|
+
sp = _settings_path()
|
|
103
|
+
try:
|
|
104
|
+
settings = _read_settings(sp)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"ERROR: Could not read {sp}: {e}")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
pre = (settings.get("hooks") or {}).get("PreToolUse", [])
|
|
110
|
+
bash_entry, existing = _find_hook(pre)
|
|
111
|
+
|
|
112
|
+
if not existing:
|
|
113
|
+
print("FailSafe is not installed in ~/.claude/settings.json")
|
|
114
|
+
sys.exit(0)
|
|
115
|
+
|
|
116
|
+
bash_entry["hooks"] = [
|
|
117
|
+
h for h in bash_entry.get("hooks", [])
|
|
118
|
+
if HOOK_MARKER not in h.get("command", "")
|
|
119
|
+
]
|
|
120
|
+
if not bash_entry["hooks"]:
|
|
121
|
+
settings["hooks"]["PreToolUse"] = [
|
|
122
|
+
e for e in pre if e is not bash_entry
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
_write_settings(sp, settings)
|
|
126
|
+
print("FailSafe uninstalled.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check():
|
|
130
|
+
sp = _settings_path()
|
|
131
|
+
try:
|
|
132
|
+
settings = _read_settings(sp)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"ERROR: Could not read {sp}: {e}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
pre = (settings.get("hooks") or {}).get("PreToolUse", [])
|
|
138
|
+
_, existing = _find_hook(pre)
|
|
139
|
+
|
|
140
|
+
if existing:
|
|
141
|
+
print("Status: INSTALLED")
|
|
142
|
+
print(f" settings : {sp}")
|
|
143
|
+
print(f" command : {existing['command']}")
|
|
144
|
+
else:
|
|
145
|
+
print("Status: NOT INSTALLED")
|
|
146
|
+
print(f" settings : {sp}")
|
|
147
|
+
print("Run: python -m failsafe_hook")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main():
|
|
151
|
+
arg = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
152
|
+
if arg == "--uninstall":
|
|
153
|
+
uninstall()
|
|
154
|
+
elif arg == "--check":
|
|
155
|
+
check()
|
|
156
|
+
elif arg in ("", "--install"):
|
|
157
|
+
install()
|
|
158
|
+
else:
|
|
159
|
+
print(f"Unknown argument: {arg}")
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|