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

1"""Security patterns and hooks for blocking dangerous commands. 

2 

3Contains patterns for detecting dangerous bash commands and destructive git 

4operations, plus hooks for enforcing these restrictions. 

5""" 

6 

7from __future__ import annotations 

8 

9from collections.abc import Awaitable, Callable 

10from typing import Any 

11 

12from ..mcp import MALA_DISALLOWED_TOOLS 

13 

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] 

19 

20# Type alias for PostToolUse hooks (mirrors PreToolUseHook pattern) 

21PostToolUseHook = Callable[ 

22 [Any, str | None, Any], 

23 Awaitable[dict[str, Any]], 

24] 

25 

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] 

41 

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] 

71 

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} 

85 

86# Tool names that should be treated as bash (case-insensitive matching) 

87BASH_TOOL_NAMES = frozenset(["bash"]) 

88 

89 

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. 

96 

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 

105 

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 

112 

113 command = hook_input["tool_input"].get("command", "") 

114 

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 } 

122 

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 } 

134 

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 } 

145 

146 return {} # Allow the command 

147 

148 

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. 

155 

156 Blocks tools that cause excessive token usage without proportional value. 

157 

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 {}