Skip to content

Instantly share code, notes, and snippets.

@feroult
Created November 7, 2025 11:55
Show Gist options
  • Select an option

  • Save feroult/731c564f7920859d9dec2e47a358fa11 to your computer and use it in GitHub Desktop.

Select an option

Save feroult/731c564f7920859d9dec2e47a358fa11 to your computer and use it in GitHub Desktop.
#!/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