Pre-tool-use hook that validates bash commands using an allowlist. Only commands in ALLOWED_COMMANDS and project-specific commands are permitted. Args: input_data: Dict containing tool_name and tool_input tool_use_id: Optional tool use ID context: Optional cont
(input_data, tool_use_id=None, context=None)
| 861 | |
| 862 | |
| 863 | async def bash_security_hook(input_data, tool_use_id=None, context=None): |
| 864 | """ |
| 865 | Pre-tool-use hook that validates bash commands using an allowlist. |
| 866 | |
| 867 | Only commands in ALLOWED_COMMANDS and project-specific commands are permitted. |
| 868 | |
| 869 | Args: |
| 870 | input_data: Dict containing tool_name and tool_input |
| 871 | tool_use_id: Optional tool use ID |
| 872 | context: Optional context dict with 'project_dir' key |
| 873 | |
| 874 | Returns: |
| 875 | Empty dict to allow, or {"decision": "block", "reason": "..."} to block |
| 876 | """ |
| 877 | if input_data.get("tool_name") != "Bash": |
| 878 | return {} |
| 879 | |
| 880 | command = input_data.get("tool_input", {}).get("command", "") |
| 881 | if not command: |
| 882 | return {} |
| 883 | |
| 884 | # Extract all commands from the command string |
| 885 | commands = extract_commands(command) |
| 886 | |
| 887 | if not commands: |
| 888 | # Could not parse - fail safe by blocking |
| 889 | return { |
| 890 | "decision": "block", |
| 891 | "reason": f"Could not parse command for security validation: {command}", |
| 892 | } |
| 893 | |
| 894 | # Get project directory from context |
| 895 | project_dir = None |
| 896 | if context and isinstance(context, dict): |
| 897 | project_dir_str = context.get("project_dir") |
| 898 | if project_dir_str: |
| 899 | project_dir = Path(project_dir_str) |
| 900 | |
| 901 | # Get effective commands using hierarchy resolution |
| 902 | allowed_commands, blocked_commands = get_effective_commands(project_dir) |
| 903 | |
| 904 | # Get effective pkill processes (includes org/project config) |
| 905 | pkill_processes = get_effective_pkill_processes(project_dir) |
| 906 | |
| 907 | # Split into segments for per-command validation |
| 908 | segments = split_command_segments(command) |
| 909 | |
| 910 | # Check each command against the blocklist and allowlist |
| 911 | for cmd in commands: |
| 912 | # Check blocklist first (highest priority) |
| 913 | if cmd in blocked_commands: |
| 914 | return { |
| 915 | "decision": "block", |
| 916 | "reason": f"Command '{cmd}' is blocked at organization level and cannot be approved.", |
| 917 | } |
| 918 | |
| 919 | # Check allowlist (with pattern matching) |
| 920 | if not is_command_allowed(cmd, allowed_commands): |