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