import sys import json import logging import os # Add project root to sys.path so we can import api_hook_client project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if project_root not in sys.path: sys.path.append(project_root) try: from api_hook_client import ApiHookClient except ImportError: print("FATAL: Failed to import ApiHookClient. Ensure it's in the Python path.", file=sys.stderr) sys.exit(1) def main(): logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stderr) logging.debug("Claude Tool Bridge script started.") try: input_data = sys.stdin.read() if not input_data: logging.debug("No input received from stdin. Exiting gracefully.") return logging.debug(f"Received raw input data: {input_data}") try: hook_input = json.loads(input_data) except json.JSONDecodeError: logging.error("Failed to decode JSON from stdin.") print(json.dumps({"decision": "deny", "reason": "Invalid JSON received from stdin."})) return # Claude Code PreToolUse hook format: tool_name + tool_input tool_name = hook_input.get('tool_name') tool_input = hook_input.get('tool_input', {}) if tool_name is None: logging.error("Could not determine tool name from input. Expected 'tool_name'.") print(json.dumps({"decision": "deny", "reason": "Missing 'tool_name' in hook input."})) return if not isinstance(tool_input, dict): logging.warning(f"tool_input is not a dict: {tool_input}. Treating as empty.") tool_input = {} logging.debug(f"Resolved tool_name: '{tool_name}', tool_input: {tool_input}") # Check context — if not running via Manual Slop, pass through hook_context = os.environ.get("CLAUDE_CLI_HOOK_CONTEXT") logging.debug(f"Checking CLAUDE_CLI_HOOK_CONTEXT: '{hook_context}'") if hook_context == 'mma_headless': # Sub-agents in headless MMA mode: auto-allow all tools logging.debug("CLAUDE_CLI_HOOK_CONTEXT is 'mma_headless'. Allowing for sub-agent.") print(json.dumps({"decision": "allow", "reason": "Sub-agent headless mode (MMA)."})) return if hook_context != 'manual_slop': # Not a programmatic Manual Slop session — allow through silently logging.debug(f"CLAUDE_CLI_HOOK_CONTEXT is '{hook_context}', not 'manual_slop'. Allowing.") print(json.dumps({"decision": "allow", "reason": f"Non-programmatic usage (CLAUDE_CLI_HOOK_CONTEXT={hook_context})."})) return # manual_slop context: route to GUI for approval logging.debug("CLAUDE_CLI_HOOK_CONTEXT is 'manual_slop'. Routing to API Hook Client.") client = ApiHookClient(base_url="http://127.0.0.1:8999") try: logging.debug(f"Requesting confirmation for tool '{tool_name}' with args: {tool_input}") response = client.request_confirmation(tool_name, tool_input) if response and response.get('approved') is True: logging.debug("User approved tool execution.") print(json.dumps({"decision": "allow"})) else: reason = response.get('reason', 'User rejected tool execution in GUI.') if response else 'No response from GUI.' logging.debug(f"User denied tool execution. Reason: {reason}") print(json.dumps({"decision": "deny", "reason": reason})) except Exception as e: logging.error(f"API Hook Client error: {str(e)}", exc_info=True) print(json.dumps({"decision": "deny", "reason": f"Manual Slop hook server unreachable: {str(e)}"})) except Exception as e: logging.error(f"Unexpected error in bridge: {str(e)}", exc_info=True) print(json.dumps({"decision": "deny", "reason": f"Internal bridge error: {str(e)}"})) if __name__ == "__main__": main()