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.
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()