Created
November 7, 2025 11:55
-
-
Save feroult/731c564f7920859d9dec2e47a358fa11 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Minimal reproduction of PreToolUse hook not being called for non-existent file paths. | |
| This script demonstrates that PreToolUse hooks are NOT invoked when Claude calls | |
| the Read tool with a file path that doesn't exist, contradicting the documentation | |
| which states hooks execute "before processing the tool call". | |
| """ | |
| import asyncio | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional | |
| from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient | |
| from claude_agent_sdk.types import ( | |
| PreToolUseHookInput, | |
| PreToolUseHookSpecificOutput, | |
| HookContext, | |
| HookMatcher, | |
| ) | |
| logging.basicConfig(level=logging.INFO, format='%(message)s') | |
| logger = logging.getLogger(__name__) | |
| async def test_hook( | |
| hook_input: PreToolUseHookInput, | |
| _subscription_id: Optional[str], | |
| _context: HookContext | |
| ) -> PreToolUseHookSpecificOutput: | |
| """Hook that logs ALL invocations.""" | |
| tool_name = hook_input.get("tool_name", "") | |
| tool_input = hook_input.get("tool_input", {}) | |
| logger.info("=" * 80) | |
| logger.info(f"[HOOK CALLED] Tool: {tool_name}") | |
| logger.info(f"[HOOK CALLED] Input: {tool_input}") | |
| logger.info("=" * 80) | |
| return {"hookSpecificOutput": {"hookEventName": "PreToolUse"}} | |
| async def main(): | |
| # Setup: Create test file in /tmp | |
| test_dir = Path("/tmp/hook_test") | |
| test_dir.mkdir(exist_ok=True) | |
| test_file = test_dir / "test.txt" | |
| test_file.write_text("Hello World") | |
| logger.info(f"Created test file: {test_file}") | |
| logger.info(f"File exists: {test_file.exists()}") | |
| # Configure SDK with hook | |
| options = ClaudeAgentOptions( | |
| permission_mode="bypassPermissions", | |
| cwd=str(test_dir), | |
| hooks={ | |
| "PreToolUse": [ | |
| HookMatcher(matcher="Read", hooks=[test_hook]) | |
| ] | |
| }, | |
| ) | |
| # Test 1: Read non-existent file (hook should fire but doesn't) | |
| logger.info("\n" + "=" * 80) | |
| logger.info("TEST 1: Reading /workspace/test.txt (DOES NOT EXIST)") | |
| logger.info("=" * 80) | |
| async with ClaudeSDKClient(options=options) as client: | |
| await client.query("Read the file /workspace/test.txt") | |
| async for _ in client.receive_response(): | |
| pass | |
| # Test 2: Read existing file (hook fires) | |
| logger.info("\n" + "=" * 80) | |
| logger.info(f"TEST 2: Reading {test_file} (EXISTS)") | |
| logger.info("=" * 80) | |
| async with ClaudeSDKClient(options=options) as client: | |
| await client.query(f"Read the file {test_file}") | |
| async for _ in client.receive_response(): | |
| pass | |
| logger.info("\n" + "=" * 80) | |
| logger.info("EXPECTED: Hook called for BOTH tests") | |
| logger.info("ACTUAL: Hook only called for TEST 2 (existing file)") | |
| logger.info("=" * 80) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment