Coverage for src/infra/hooks/dangerous_commands.py: 33%

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

2

3 Contains patterns for detecting dangerous bash commands and destructive git

4 operations, plus hooks for enforcing these restrictions.

5"""

6

7 from __future__ import annotations

8

9 from collections.abc import Awaitable, Callable

10 from typing import Any

11

12 from ..mcp import MALA_DISALLOWED_TOOLS

13

14# Type alias for PreToolUse hooks (using Any to avoid SDK import)

15 PreToolUseHook = Callable[

16 [Any, str | None, Any],

17 Awaitable[dict[str, Any]],

18]

19

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

21 PostToolUseHook = Callable[

22 [Any, str | None, Any],

23 Awaitable[dict[str, Any]],

24]

25

26# Dangerous bash command patterns to block

27 DANGEROUS_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.

45 DESTRUCTIVE_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)

73 SAFE_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)

87 BASH_TOOL_NAMES = frozenset(["bash"])

88

89

90 async 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

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