resolveHookPermissionDecision function

Future<({PermissionResult decision, Map<String, dynamic> input})> resolveHookPermissionDecision({
  1. required PermissionResult? hookPermissionResult,
  2. required ToolDefinition tool,
  3. required Map<String, dynamic> input,
  4. required ToolUseContext toolUseContext,
  5. required CanUseToolFn canUseTool,
  6. required AssistantMessage assistantMessage,
  7. required String toolUseID,
  8. Future<PermissionResult?> checkRuleBasedPermissions(
    1. ToolDefinition,
    2. Map<String, dynamic>,
    3. ToolUseContext
    )?,
})

Resolve a PreToolUse hook's permission result into a final decision.

Encapsulates the invariant that hook 'allow' does NOT bypass settings.json deny/ask rules.

Implementation

Future<({PermissionResult decision, Map<String, dynamic> input})>
resolveHookPermissionDecision({
  required PermissionResult? hookPermissionResult,
  required ToolDefinition tool,
  required Map<String, dynamic> input,
  required ToolUseContext toolUseContext,
  required CanUseToolFn canUseTool,
  required AssistantMessage assistantMessage,
  required String toolUseID,
  Future<PermissionResult?> Function(
    ToolDefinition,
    Map<String, dynamic>,
    ToolUseContext,
  )?
  checkRuleBasedPermissions,
}) async {
  final requiresInteraction = tool.requiresUserInteraction?.call() ?? false;
  final requireCanUseTool = toolUseContext.requireCanUseTool;

  if (hookPermissionResult?.behavior == PermissionBehavior.allow) {
    final hookInput = hookPermissionResult!.updatedInput ?? input;
    final interactionSatisfied =
        requiresInteraction && hookPermissionResult.updatedInput != null;

    if ((requiresInteraction && !interactionSatisfied) || requireCanUseTool) {
      return (
        decision: await canUseTool(
          tool,
          hookInput,
          toolUseContext,
          assistantMessage,
          toolUseID,
        ),
        input: hookInput,
      );
    }

    // Hook allow skips interactive prompt, but deny/ask rules still apply
    if (checkRuleBasedPermissions != null) {
      final ruleCheck = await checkRuleBasedPermissions(
        tool,
        hookInput,
        toolUseContext,
      );
      if (ruleCheck != null) {
        if (ruleCheck.behavior == PermissionBehavior.deny) {
          return (decision: ruleCheck, input: hookInput);
        }
        // ask rule — dialog required despite hook approval
        return (
          decision: await canUseTool(
            tool,
            hookInput,
            toolUseContext,
            assistantMessage,
            toolUseID,
          ),
          input: hookInput,
        );
      }
    }

    return (decision: hookPermissionResult, input: hookInput);
  }

  if (hookPermissionResult?.behavior == PermissionBehavior.deny) {
    return (decision: hookPermissionResult!, input: input);
  }

  // No hook decision or 'ask' — normal permission flow
  final forceDecision = hookPermissionResult?.behavior == PermissionBehavior.ask
      ? hookPermissionResult
      : null;
  final askInput =
      (hookPermissionResult?.behavior == PermissionBehavior.ask &&
          hookPermissionResult?.updatedInput != null)
      ? hookPermissionResult!.updatedInput!
      : input;

  return (
    decision: await canUseTool(
      tool,
      askInput,
      toolUseContext,
      assistantMessage,
      toolUseID,
      forceDecision,
    ),
    input: askInput,
  );
}