-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.py
More file actions
139 lines (101 loc) · 3.74 KB
/
install.py
File metadata and controls
139 lines (101 loc) · 3.74 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
#!/usr/bin/env python3
from __future__ import annotations
import json
import platform
import shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parent
CLAUDE_DIR = Path.home() / ".claude"
HOOKS_DIR = CLAUDE_DIR / "hooks"
COMMANDS_DIR = CLAUDE_DIR / "commands"
SETTINGS_PATH = CLAUDE_DIR / "settings.json"
SETTINGS_BACKUP_PATH = CLAUDE_DIR / "settings.userprompt-guard.backup.json"
def ensure_dirs() -> None:
HOOKS_DIR.mkdir(parents=True, exist_ok=True)
COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
def load_settings() -> dict:
if not SETTINGS_PATH.exists():
return {}
try:
return json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
except Exception:
return {}
def write_settings(settings: dict) -> None:
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
SETTINGS_PATH.write_text(json.dumps(settings, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def backup_settings_if_needed() -> Path | None:
if not SETTINGS_PATH.exists():
return None
if SETTINGS_BACKUP_PATH.exists():
return None
shutil.copy2(SETTINGS_PATH, SETTINGS_BACKUP_PATH)
return SETTINGS_BACKUP_PATH
def hook_script_target() -> Path:
return HOOKS_DIR / "claude_chat_audit.py"
def command_file_target() -> Path:
return COMMANDS_DIR / "audit-chat.md"
def command_path_literal(path: Path) -> str:
return path.as_posix()
def pick_python_command() -> str:
if platform.system() == "Windows":
return "python"
return "python3"
def hook_command() -> str:
return f'{pick_python_command()} "{command_path_literal(hook_script_target())}" hook-user'
def audit_chat_markdown() -> str:
python_cmd = pick_python_command()
script_path = command_path_literal(hook_script_target())
return f"""---
description: Audit recent Claude chat content for local policy risk
argument-hint: Optional 'history' or 'doctor'
allowed-tools: Bash(python:*), Bash(python3:*)
---
Run this exact command once:
```bash
{python_cmd} "{script_path}" report --cwd "$PWD" $ARGUMENTS
```
Return the command output verbatim.
"""
def install_files() -> None:
source_hook = ROOT / "hooks" / "claude_chat_audit.py"
shutil.copy2(source_hook, hook_script_target())
command_file_target().write_text(audit_chat_markdown(), encoding="utf-8")
def install_userprompt_hook(settings: dict) -> dict:
hooks = settings.setdefault("hooks", {})
entries = hooks.setdefault("UserPromptSubmit", [])
if not isinstance(entries, list):
entries = []
hooks["UserPromptSubmit"] = entries
command = hook_command()
for entry in entries:
hook_list = entry.get("hooks", [])
if not isinstance(hook_list, list):
continue
for hook in hook_list:
if hook.get("type") == "command" and hook.get("command") == command:
return settings
if entries:
first = entries[0]
hook_list = first.setdefault("hooks", [])
if not isinstance(hook_list, list):
first["hooks"] = []
hook_list = first["hooks"]
hook_list.append({"type": "command", "command": command})
else:
entries.append({"hooks": [{"type": "command", "command": command}]})
return settings
def main() -> int:
ensure_dirs()
backup_path = backup_settings_if_needed()
install_files()
settings = install_userprompt_hook(load_settings())
write_settings(settings)
print("Installed Claude UserPrompt Guard.")
print(f"Hook command: {hook_command()}")
if backup_path:
print(f"Settings backup: {backup_path}")
print("Doctor check: /audit-chat doctor")
print("Restart Claude Code to load the new hook.")
return 0
if __name__ == "__main__":
raise SystemExit(main())