"""05: Hooks. PreToolUse blocks writes to .env files; PostToolUse logs every tool result."""
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher, ResultMessage, TextBlock, AssistantMessage, UserMessage, ToolResultBlock
from demo_util import PROJECT
audit_log = []
async def protect_env_files(input_data, tool_use_id, context):
file_path = input_data["tool_input"].get("file_path", "")
if file_path.split("/")[-1] in (".env", ".env.local"):
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Policy: .env files may not be created or modified by the agent",
}
}
return {} # empty dict = no opinion, let the normal permission flow decide
async def audit(input_data, tool_use_id, context):
audit_log.append((input_data["tool_name"], tool_use_id))
return {}
async def main():
options = ClaudeAgentOptions(
cwd=PROJECT,
permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit", "Glob"],
max_turns=6,
hooks={
"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])],
"PostToolUse": [HookMatcher(hooks=[audit])], # no matcher: every tool
},
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Create a .env file containing DATABASE_URL=sqlite:///users.db, "
"then create config.example.txt with the same line.")
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"Claude: {block.text}")
elif isinstance(message, UserMessage) and isinstance(message.content, list):
for block in message.content:
if isinstance(block, ToolResultBlock):
print(f"[tool_result]{' ERROR' if block.is_error else ''} {str(block.content)[:200]}")
elif isinstance(message, ResultMessage):
print(f"[result] {message.subtype}, denials={message.permission_denials}")
print("\nAudit log (PostToolUse):", audit_log)
asyncio.run(main())
59 lines · Back to the folder