deliberate 1.0.1
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.
- package/LICENSE +11 -0
- package/README.md +180 -0
- package/bin/cli.js +113 -0
- package/hooks/__pycache__/deliberate-commands.cpython-312.pyc +0 -0
- package/hooks/deliberate-changes.py +606 -0
- package/hooks/deliberate-commands-post.py +126 -0
- package/hooks/deliberate-commands.py +1742 -0
- package/hooks/hooks.json +29 -0
- package/hooks/setup-check.py +67 -0
- package/hooks/test_skip_commands.py +293 -0
- package/package.json +51 -0
- package/src/classifier/classify_command.py +346 -0
- package/src/classifier/embed_command.py +56 -0
- package/src/classifier/index.js +324 -0
- package/src/classifier/model-classifier.js +531 -0
- package/src/classifier/pattern-matcher.js +230 -0
- package/src/config.js +207 -0
- package/src/index.js +23 -0
- package/src/install.js +754 -0
- package/src/server.js +239 -0
- package/src/uninstall.js +198 -0
- package/training/build_classifier.py +325 -0
- package/training/expanded-command-safety.jsonl +712 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Deliberate - Command Analysis PostToolUse Hook
|
|
5
|
+
|
|
6
|
+
PostToolUse hook that displays cached command analysis after Bash execution.
|
|
7
|
+
Reads analysis cached by the PreToolUse hook (deliberate-commands.py).
|
|
8
|
+
|
|
9
|
+
This provides persistent visibility of command analysis even after the
|
|
10
|
+
PreToolUse permission prompt disappears.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
import hashlib
|
|
17
|
+
|
|
18
|
+
DEBUG = os.environ.get("DELIBERATE_DEBUG", "").lower() in ("1", "true", "yes")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def debug(msg: str):
|
|
22
|
+
"""Print debug message to stderr if DEBUG is enabled."""
|
|
23
|
+
if DEBUG:
|
|
24
|
+
print(f"[deliberate-cmd-post] {msg}", file=sys.stderr)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_cache_file(session_id: str, cmd_hash: str) -> str:
|
|
28
|
+
"""Get cache file path - must match PreToolUse hook."""
|
|
29
|
+
return os.path.expanduser(f"~/.claude/deliberate_cmd_cache_{session_id}_{cmd_hash}.json")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_from_cache(session_id: str, cmd_hash: str) -> dict | None:
|
|
33
|
+
"""Load analysis result from cache."""
|
|
34
|
+
cache_file = get_cache_file(session_id, cmd_hash)
|
|
35
|
+
try:
|
|
36
|
+
if os.path.exists(cache_file):
|
|
37
|
+
with open(cache_file, 'r') as f:
|
|
38
|
+
data = json.load(f)
|
|
39
|
+
# Clean up cache file after reading
|
|
40
|
+
os.remove(cache_file)
|
|
41
|
+
debug(f"Loaded and removed cache: {cache_file}")
|
|
42
|
+
return data
|
|
43
|
+
except (IOError, json.JSONDecodeError) as e:
|
|
44
|
+
debug(f"Failed to load cache: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
debug("PostToolUse hook started")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
input_data = json.load(sys.stdin)
|
|
53
|
+
debug(f"Got input: tool={input_data.get('tool_name')}")
|
|
54
|
+
except json.JSONDecodeError as e:
|
|
55
|
+
debug(f"JSON decode error: {e}")
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
# Only process Bash commands
|
|
59
|
+
tool_name = input_data.get("tool_name", "")
|
|
60
|
+
if tool_name != "Bash":
|
|
61
|
+
debug(f"Not Bash, skipping: {tool_name}")
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
# Get session ID and command
|
|
65
|
+
session_id = input_data.get("session_id", "default")
|
|
66
|
+
tool_input = input_data.get("tool_input", {})
|
|
67
|
+
command = tool_input.get("command", "")
|
|
68
|
+
|
|
69
|
+
if not command:
|
|
70
|
+
debug("No command found")
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
# Generate same hash as PreToolUse to find cache
|
|
74
|
+
# MD5 used for cache key only, not security
|
|
75
|
+
cmd_hash = hashlib.md5(command.encode(), usedforsecurity=False).hexdigest()[:16]
|
|
76
|
+
|
|
77
|
+
# Load cached analysis
|
|
78
|
+
cached = load_from_cache(session_id, cmd_hash)
|
|
79
|
+
if not cached:
|
|
80
|
+
debug("No cached analysis found")
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
risk = cached.get("risk", "MODERATE")
|
|
84
|
+
explanation = cached.get("explanation", "Command executed")
|
|
85
|
+
llm_unavailable_warning = cached.get("llm_unavailable_warning", "")
|
|
86
|
+
|
|
87
|
+
# ANSI color codes for terminal output
|
|
88
|
+
BOLD = "\033[1m"
|
|
89
|
+
CYAN = "\033[96m"
|
|
90
|
+
RED = "\033[91m"
|
|
91
|
+
YELLOW = "\033[93m"
|
|
92
|
+
GREEN = "\033[92m"
|
|
93
|
+
RESET = "\033[0m"
|
|
94
|
+
|
|
95
|
+
# Choose emoji and color based on risk
|
|
96
|
+
if risk == "DANGEROUS":
|
|
97
|
+
emoji = "🚨"
|
|
98
|
+
color = RED
|
|
99
|
+
elif risk == "SAFE":
|
|
100
|
+
emoji = "✅"
|
|
101
|
+
color = GREEN
|
|
102
|
+
else:
|
|
103
|
+
emoji = "âš¡"
|
|
104
|
+
color = YELLOW
|
|
105
|
+
|
|
106
|
+
# User-facing message - color the explanation so it's not easy to skip
|
|
107
|
+
user_message = f"{emoji} {BOLD}{CYAN}DELIBERATE{RESET} {BOLD}{color}[{risk}]{RESET}\n {color}{explanation}{RESET}{llm_unavailable_warning}"
|
|
108
|
+
|
|
109
|
+
# Context for Claude
|
|
110
|
+
context = f"**Deliberate** [{risk}]: {explanation}{llm_unavailable_warning}"
|
|
111
|
+
|
|
112
|
+
# Output for PostToolUse - systemMessage makes it visible to user
|
|
113
|
+
output = {
|
|
114
|
+
"systemMessage": user_message,
|
|
115
|
+
"hookSpecificOutput": {
|
|
116
|
+
"hookEventName": "PostToolUse",
|
|
117
|
+
"additionalContext": context
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
print(json.dumps(output))
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|