Skip to main content

Module 14: Permission Callbacks

Implementing dynamic tool permission decisions with hooks.

What You’ll Learn

  • Using HookRegistry.registerPreToolUse() for permission decisions
  • Allowing or blocking tools based on runtime context
  • Pattern matching on tool names and inputs
  • Building a permission callback system

Permission Callbacks via Hooks

The SDK provides permission callbacks through the PreToolUse hook mechanism. When Claude attempts to use a tool, your hook is invoked with the tool name and input, allowing you to make runtime decisions.
Hook MethodResultPurpose
HookOutput.allow()Tool executesPermit the operation
HookOutput.block(message)Tool blockedDeny with reason shown to Claude

Implementing Permission Callbacks

Register a PreToolUse hook to intercept all tool calls:
import org.springaicommunity.claude.agent.sdk.ClaudeClient;
import org.springaicommunity.claude.agent.sdk.ClaudeSyncClient;
import org.springaicommunity.claude.agent.sdk.config.PermissionMode;
import org.springaicommunity.claude.agent.sdk.hooks.HookRegistry;
import org.springaicommunity.claude.agent.sdk.transport.CLIOptions;
import org.springaicommunity.claude.agent.sdk.types.control.HookInput;
import org.springaicommunity.claude.agent.sdk.types.control.HookOutput;

import java.nio.file.Path;
import java.util.List;

// Define dangerous command patterns
List<String> DANGEROUS_PATTERNS = List.of("rm -rf", "sudo", "chmod 777", "mkfs");

// Create hook registry with permission logic
HookRegistry hooks = new HookRegistry();

hooks.registerPreToolUse(input -> {
    var preToolUse = (HookInput.PreToolUseInput) input;
    String toolName = preToolUse.toolName();

    System.out.println("[Permission] Tool: " + toolName);

    // Always allow read-only operations
    if (toolName.equals("Read") || toolName.equals("Glob") || toolName.equals("Grep")) {
        return HookOutput.allow();
    }

    // Check Bash commands for dangerous patterns
    if (toolName.equals("Bash")) {
        String command = preToolUse.getArgument("command", String.class).orElse("");

        for (String pattern : DANGEROUS_PATTERNS) {
            if (command.contains(pattern)) {
                return HookOutput.block("Blocked dangerous command: " + pattern);
            }
        }
    }

    // Default: allow other tools
    return HookOutput.allow();
});

// Use the hooks with a client
try (ClaudeSyncClient client = ClaudeClient.sync()
        .workingDirectory(Path.of("."))
        .model(CLIOptions.MODEL_HAIKU)
        .permissionMode(PermissionMode.BYPASS_PERMISSIONS)
        .allowedTools(List.of("Read", "Bash", "Glob"))
        .hookRegistry(hooks)
        .build()) {

    // Safe command - will be allowed
    client.connect("Run: echo 'Hello'");
    // ... process response

    // Dangerous command - will be blocked by hook
    client.query("Run: rm -rf /tmp/test");
    // Claude receives the block message and reports it couldn't execute
}

Permission Decision Flow

Claude requests tool → PreToolUse hook fires → Your code decides

                                         ┌─────────┴─────────┐
                                         ▼                   ▼
                                  HookOutput.allow()  HookOutput.block(msg)
                                         │                   │
                                         ▼                   ▼
                                   Tool executes       Tool blocked
                                                     (msg sent to Claude)

Common Permission Patterns

Read-Only Mode

hooks.registerPreToolUse(input -> {
    var preToolUse = (HookInput.PreToolUseInput) input;
    String toolName = preToolUse.toolName();

    return switch (toolName) {
        case "Read", "Glob", "Grep" -> HookOutput.allow();
        default -> HookOutput.block("Only read operations are permitted");
    };
});

Path-Based Restrictions

hooks.registerPreToolUse(input -> {
    var preToolUse = (HookInput.PreToolUseInput) input;

    if (preToolUse.toolName().equals("Read")) {
        String path = preToolUse.getArgument("file_path", String.class).orElse("");
        if (path.contains("/etc/") || path.contains("/secrets/")) {
            return HookOutput.block("Access to sensitive paths denied");
        }
    }
    return HookOutput.allow();
});

Key Points

  • Use HookRegistry.registerPreToolUse() for permission callbacks
  • Return HookOutput.allow() to permit, HookOutput.block(message) to deny
  • The block message is shown to Claude, who reports it to the user
  • Hooks fire for all tool types including MCP tools
Claude has built-in safety checks and may refuse dangerous commands even before your hook fires. Your permission callbacks provide an additional layer of control.

Source Code

View on GitHub

Running the Example

mvn compile exec:java -pl module-14-permission-callbacks

Next Module

Module 15: Hooks PreToolUse - Tool-specific hook registration and logging.