Coverage for src / infra / hooks / dangerous_commands.py: 33%
36 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
1"""Security patterns and hooks for blocking dangerous commands.
3Contains patterns for detecting dangerous bash commands and destructive git
4operations, plus hooks for enforcing these restrictions.
5"""
7from __future__ import annotations
9from collections.abc import Awaitable, Callable
10from typing import Any
12from ..mcp import MALA_DISALLOWED_TOOLS
14# Type alias for PreToolUse hooks (using Any to avoid SDK import)
15PreToolUseHook = Callable[
16 [Any, str | None, Any],
17 Awaitable[dict[str, Any]],
18]
20# Type alias for PostToolUse hooks (mirrors PreToolUseHook pattern)
21PostToolUseHook = Callable[
22 [Any, str | None, Any],
23 Awaitable[dict[str, Any]],
24]
26# Dangerous bash command patterns to block
27DANGEROUS_PATTERNS = [
28 "rm -rf /",
29 "rm -rf ~",
30 "rm -rf $HOME",
31 ":(){:|:&};:", # fork bomb
32 "mkfs.",
33 "dd if=",
34 "> /dev/sd",
35 "chmod -R 777 /",
36 "curl | bash",
37 "wget | bash",
38 "curl | sh",
39 "wget | sh",
40]
42# Destructive git command patterns to block in multi-agent contexts.
43# These operations modify working tree or history in ways that can conflict
44# between concurrent agents.
45DESTRUCTIVE_GIT_PATTERNS = [
46 # Hard reset - discards uncommitted changes silently
47 "git reset --hard",
48 # Clean - removes untracked files
49 "git clean -fd",
50 "git clean -f",
51 "git clean -df",
52 "git clean -d -f",
53 # Force checkout - discards local changes
54 "git checkout -- .",
55 "git checkout -f",
56 "git checkout --force",
57 # Restore - discards uncommitted changes without confirmation
58 "git restore",
59 # Rebase - can rewrite history and requires conflict resolution
60 "git rebase",
61 # Force delete branches
62 "git branch -D",
63 "git branch -d -f",
64 # Stash - hides changes that other agents cannot see
65 "git stash",
66 # Abort operations - may discard other agents' work in progress
67 "git merge --abort",
68 "git rebase --abort",
69 "git cherry-pick --abort",
70]
72# Safe alternatives to blocked git operations (for error messages)
73SAFE_GIT_ALTERNATIVES: dict[str, str] = {
74 "git stash": "commit changes instead: git add . && git commit -m 'WIP: ...'",
75 "git reset --hard": "use git checkout <file> to revert specific files, or commit first",
76 "git rebase": "use git merge instead, or coordinate with other agents",
77 "git checkout -f": "commit or stash changes first (in non-agent context)",
78 "git checkout --force": "commit or stash changes first (in non-agent context)",
79 "git restore": "commit changes first, or use git diff to review before discarding",
80 "git clean -f": "manually remove specific untracked files with rm",
81 "git merge --abort": "resolve merge conflicts instead of aborting",
82 "git rebase --abort": "resolve rebase conflicts instead of aborting",
83 "git cherry-pick --abort": "resolve cherry-pick conflicts instead of aborting",
84}
86# Tool names that should be treated as bash (case-insensitive matching)
87BASH_TOOL_NAMES = frozenset(["bash"])
90async def block_dangerous_commands(
91 hook_input: Any, # noqa: ANN401 - SDK type, avoid import
92 stderr: str | None,
93 context: Any, # noqa: ANN401 - SDK type, avoid import
94) -> dict[str, Any]:
95 """PreToolUse hook to block dangerous bash commands.
97 In multi-agent contexts, certain git operations are blocked because they
98 can cause conflicts between concurrent agents. Blocked operations include:
99 - git stash (all subcommands) - hides changes other agents cannot see
100 - git reset --hard - discards uncommitted changes silently
101 - git rebase - requires human input and can rewrite history
102 - git checkout -f/--force - discards local changes
103 - git clean -f - removes untracked files without warning
104 - git merge/rebase/cherry-pick --abort - may discard other agents' work
106 When a blocked operation is detected, the error message includes a safe
107 alternative when available.
108 """
109 tool_name = hook_input["tool_name"].lower()
110 if tool_name not in BASH_TOOL_NAMES:
111 return {} # Allow non-Bash tools
113 command = hook_input["tool_input"].get("command", "")
115 # Block dangerous patterns
116 for pattern in DANGEROUS_PATTERNS:
117 if pattern in command:
118 return {
119 "decision": "block",
120 "reason": f"Blocked dangerous command pattern: {pattern}",
121 }
123 # Block destructive git patterns with safe alternatives
124 for pattern in DESTRUCTIVE_GIT_PATTERNS:
125 if pattern in command:
126 alternative = SAFE_GIT_ALTERNATIVES.get(pattern, "")
127 reason = f"Blocked destructive git command: {pattern}"
128 if alternative:
129 reason = f"{reason}. Safe alternative: {alternative}"
130 return {
131 "decision": "block",
132 "reason": reason,
133 }
135 # Block force push to ALL branches (--force-with-lease is allowed as safer alternative)
136 if "git push" in command:
137 # Allow --force-with-lease (safer alternative)
138 if "--force-with-lease" in command:
139 pass # Allow
140 elif "--force" in command or "-f " in command:
141 return {
142 "decision": "block",
143 "reason": "Blocked force push (use --force-with-lease if needed)",
144 }
146 return {} # Allow the command
149async def block_mala_disallowed_tools(
150 hook_input: Any, # noqa: ANN401 - SDK type, avoid import
151 stderr: str | None,
152 context: Any, # noqa: ANN401 - SDK type, avoid import
153) -> dict[str, Any]:
154 """PreToolUse hook to block tools disabled for mala agents.
156 Blocks tools that cause excessive token usage without proportional value.
158 Note: We use a hook instead of the SDK's `disallowed_tools` parameter because
159 it has a known bug where it's sometimes ignored.
160 See: https://github.com/anthropics/claude-agent-sdk-python/issues/361
161 """
162 tool_name = hook_input["tool_name"]
163 if tool_name in MALA_DISALLOWED_TOOLS:
164 return {
165 "decision": "block",
166 "reason": f"Tool {tool_name} is disabled for mala agents to reduce token waste.",
167 }
168 return {}