Agent Hooks and Policies

July 21, 2026 ยท View on GitHub

The Hedera Agent Kit provides a flexible and powerful system for putting limits on tool usage and enforcing business logic, effectively enabling you to limit the functionality of AI agents through Hooks and Policies. These hooks and policies can be used to enforce security, compliance, and other business rules. through Hooks and Policies.


Table of Contents

Part 1: For Hooks and Policies Users

Part 2: For Policy and Hook Developers


Part 1: For Hooks and Policies Users

Quick Overview

Hooks and Policies let you customize how tools behave:

  • Hooks: Extensions that observe and modify tool execution (logging, tracking, etc.)
  • Policies: Validation rules that can block tool execution if certain conditions aren't met

Note

Only tools extending BaseTool support hooks and policies. Tools that directly implement the Tool interface do not.

When Hooks and Policies are Called

Hooks can execute at 4 different points during a tool's lifecycle:

  1. Pre-Tool Execution - Before anything happens, when parameters are passed
  2. Post-Parameter Normalization - After parameters are validated and cleaned
  3. Post-Core Action - After the main logic executes (e.g., transaction created), before tool execution when a transaction has been formed
  4. Post-Tool Execution - After everything completes; after tool execution when a transaction has been signed and submitted

How to Use Hooks and Policies

Add hooks and policies to your agent's context during initialization:

import { HcsAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';
import { MaxRecipientsPolicy, RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';

const context = {
  hooks: [
    new HcsAuditTrailHook(['transfer_hbar_tool'], '0.0.12345'),
    new MaxRecipientsPolicy(5),
    new RejectToolPolicy(['delete_account_tool']),
  ],
  // ... other configuration
};

Note

Policies and hooks are both attached through the same context.hooks array โ€” there is no separate policies field. AbstractPolicy extends AbstractHook, so a policy is just a hook that can block execution.

Tip

Example Application: See Option I: Run the Audit Trail Agent in the Developer Examples for a complete working implementation across different frameworks.

Available Hooks

1. HcsAuditTrailHook (Hook)

Description:
Provides an immutable audit trail by logging tool executions to a Hedera Consensus Service (HCS) topic.

Important


Autonomous Mode Only: This hook is strictly available in AUTONOMOUS mode. It will throw an error if used in RETURN_BYTES mode.

Warning


HIP-991 (Paid Topics): If a paid topic is used, it will incur submission fees. Ensure the loggingClient has sufficient funds to avoid draining the account.

Prerequisites:

  1. Topic Creation: The HCS topic must be created before initializing the hook.
  2. Permissions: The Hedera account associated with the loggingClient (or the agent's operator account) must have permissions to submit messages to the topic (i.e., it must hold the submitKey if one is defined).

Parameters:

  • relevantTools: string[] - List of tools to audit (e.g., ['transfer_hbar_tool', 'create_fungible_token_tool']).
  • hcsTopicId: string - The pre-created Hedera topic ID (e.g., '0.0.12345').
  • loggingClient?: Client - (Optional) A separate Hedera client for logging. If not provided, defaults to the agent's operator client. Must have submission access to the topic.

Example Usage:

import { HcsAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';

const auditHook = new HcsAuditTrailHook(
  ['transfer_hbar_tool', 'create_fungible_token_tool'],
  '0.0.12345' // Ensure this topic exists and the agent can post to it
);

// Add to your agent configuration
const context = {
  hooks: [auditHook],
  // ...
};

Note

Error Handling: If the hook fails to post a message to HCS (e.g., due to network issues or incorrect topic ID), it will catch the error and log it to the CLI as console.error. Note that this might affect the auditing and it means that agent might have executed some transaction that was not logged in the auditing topic if misconfigured or external problems appeared. We are looking into further improvements of the auditing trail to make it more secure.

Note

Complete Examples: See the Audit Trail agent implementations in AI SDK or LangChain v1.


2. HolAuditTrailHook (Hook)

Description:
Hook that writes HOL-standards-compliant audit trails to an HCS session topic. It uses an HCS-2 INDEXED registry as the session topic to list audit entries.

Important


Autonomous Mode Only: This hook is strictly available in AUTONOMOUS mode. It will throw an error if used in RETURN_BYTES mode.

Prerequisites:

  1. Topic Creation: The HCS topic must be created before initializing the hook.
  2. Standard Compliance: To be fully compliant with the HCS-2 standard, the topic should be created with the memo hcs-2:0:0.

Parameters:

  • relevantTools: string[] - List of tool names that trigger audit trail logging.
  • sessionId: string - The Hedera topic ID (format 0.0.xxx) used as the audit session registry.

Example Usage:

import { HolAuditTrailHook } from '@hashgraph/hedera-agent-kit/hooks';

const holAuditHook = new HolAuditTrailHook({
  relevantTools: ['transfer_hbar_tool', 'create_fungible_token_tool'],
  sessionId: '0.0.12345'
});

// Add to your agent configuration
const context = {
  hooks: [holAuditHook],
  // ...
};

Note

Error Handling: Similar to HcsAuditTrailHook, if this hook fails to log an audit entry, it will catch the error and log it to the CLI as console.error to avoid interrupting the tool's lifecycle. Note that this might affect the auditing and it means that agent might have executed some transaction that was not logged in the auditing topic if misconfigured or external problems appeared. We are looking into further improvements of the auditing trail to make it more secure.


Available Policies

1. MaxRecipientsPolicy (Policy)

Description:
A security policy that limits the number of recipients in transfer and airdrop operations. It blocks requests that exceed a defined threshold to prevent massive unauthorized transfers.

Default Supported Tools: By default, the policy knows how to count recipients for:

  • transfer_hbar_tool
  • transfer_hbar_with_allowance_tool
  • airdrop_fungible_token_tool
  • transfer_fungible_token_with_allowance_tool
  • transfer_non_fungible_token_tool
  • transfer_non_fungible_token_with_allowance_tool

Parameters:

  • maxRecipients: number - Maximum number of recipients allowed.
  • additionalTools?: string[] - (Optional) Extra tools to apply this policy to.
  • customStrategies?: Record<string, (params: any) => number> - (Optional) A mapping of tool names to functions that count recipients. If you add tools via additionalTools, you must provide a strategy for each one, otherwise the policy will throw an error at runtime.

Example with Custom Strategies:

import { MaxRecipientsPolicy } from '@hashgraph/hedera-agent-kit/policies';

// Basic usage with default tools only
const basicPolicy = new MaxRecipientsPolicy(5);

// With custom tool - strategy is REQUIRED
const extendedPolicy = new MaxRecipientsPolicy(
  5, // max 5 recipients
  ['my_custom_bulk_tool'], // additional tool
  {
    // This strategy is required for the custom tool
    'my_custom_bulk_tool': (params) => params.recipients.length
  }
);

Note

Complete Examples: Check the Policy Enforcement Agent for full implementations in AI SDK or LangChain v1.


2. RejectToolPolicy (Policy)

Description: A restrictive policy used to explicitly disable specific tools. Even if a tool is technically available in a plugin, this policy ensures the agent cannot execute it under any circumstances.

Parameters:

  • relevantTools: string[] - The list of tool methods to be blocked (e.g., ['delete_account_tool', 'delete_topic_tool']).

Example Usage:

import { RejectToolPolicy } from '@hashgraph/hedera-agent-kit/policies';

const safetyPolicy = new RejectToolPolicy(['delete_account_tool']);

const context = {
  hooks: [safetyPolicy],
  // ...
};

Part 2: For Policy and Hook Developers

Tool Lifecycle Deep Dive

Every tool in the kit follows a standardized 7-stage lifecycle. The execution logic is defined in packages/core/src/shared/tools.ts.

[1. Pre-Tool Execution] --------> Hook: preToolExecutionHook
         |
[2. Parameter Normalization]
         |
[3. Post-Parameter Normalization] --> Hook: postParamsNormalizationHook
         |
[4. Core Action]
         |
[5. Post-Core Action] --------------> Hook: postCoreActionHook
         |
[6. Secondary Action]
         |
[7. Post-Tool Execution] -----------> Hook: postToolExecutionHook
         |
[Result Returned]

Stage Details:

  1. Pre-Tool Execution: Before any processing begins. Use for early validation or logging.
  2. Parameter Normalization: The tool validates and cleans user input (not hookable).
  3. Post-Parameter Normalization: After parameters are normalized. Use for parameter-based validation.
  4. Core Action: Primary business logic executes (e.g., creating a transaction).
  5. Post-Core Action: After core logic completes. Use to inspect or modify the result before submission.
  6. Secondary Action: Transaction signing/submission happens (not hookable).
  7. Post-Tool Execution: After everything completes. Use for final logging or cleanup.

Hook Parameter Structures

Each hook receives specialized parameter objects and the method name (string) representing the tool being executed. This allows hooks to target specific tools or apply general logic.

Hook StageParams Object ContainsMethod ParameterUse Case
preToolExecutionHookcontext, rawParams, clientmethod: stringEarly validation, logging initial state
postParamsNormalizationHookcontext, rawParams, normalisedParams, clientmethod: stringParameter-based policies, data enrichment
postCoreActionHookcontext, rawParams, normalisedParams, coreActionResult, clientmethod: stringInspect/modify transaction before submission
postToolExecutionHookcontext, rawParams, normalisedParams, coreActionResult, toolResult, clientmethod: stringFinal logging, audit trails, cleanup

Tip

Use the method parameter to filter execution and apply Type Guards for safe parameter access.


Hooks vs. Policies

Hooks (AbstractHook)

Hooks are non-blocking extensions that observe and modify execution flow. They can:

  • Log data
  • Modify context state
  • Enrich parameters
  • Track metrics

They should not stop execution unless an error occurs.

Example: HcsAuditTrailHook logs execution details to an HCS topic without blocking.

Policies (AbstractPolicy)

Policies are specialized Hooks designed to validate and block execution. They use shouldBlock... methods that return boolean values. If true is returned, the AbstractPolicy base class throws an error, immediately halting the tool's lifecycle.

Policy Execution Flow:

[Stage: Pre-Tool Execution]
         |
    (Hook Entry)
         |
    [AbstractPolicy.preToolExecutionHook] (calls shouldBlockPreToolExecution)
         |
    [shouldBlockPreToolExecution?] -- Yes --> [THROW ERROR] --> (Error returned to LLM/Agent)
         |
         No
         |
[Proceed to Normalization Stage]

Important

Policy Implementation Rule: When creating a custom Policy, you should define logic in at least one of the shouldBlock... methods (e.g., shouldBlockPreToolExecution, shouldBlockPostParamsNormalization, etc.). While the tool won't break if they are undefined, the policy won't perform any blocking logic. You must not override the native hook methods ( e.g., preToolExecutionHook) as the AbstractPolicy base class uses these internally to trigger the blocking logic and throw errors.

Available shouldBlock... methods:

  • shouldBlockPreToolExecution()
  • shouldBlockPostParamsNormalization()
  • shouldBlockPostCoreAction()
  • shouldBlockPostSecondaryAction()

Note

Every AbstractPolicy is an AbstractHook, but with pre-defined error handling and "block check" methods.


Type Safety & Multi-Tool Context

Hooks are configured for a specific set of tools (the relevantTools list). However, because AbstractHook is generic, there is no compile-time type safety for parameters. When a hook targets multiple tools, you must handle the various parameter structures using one of three patterns:

1. Universal Logic

When to use:

  • Your hook performs the same operation across all tools
  • Target tools share common parameter fields (e.g., transfers, tokenId)
  • Logic is independent of parameters (e.g., counting tool calls)

Approach: Focus on the context for state management or apply generic processing (like recursive stringification)to rawParams.

Example: HcsAuditTrailHook logs all inputs to HCS without needing to know each tool's schema.

async postToolExecutionHook(
  params: PostSecondaryActionParams,
  method: string
) {
  // Works for all tools - generic logging
  const logEntry = {
    tool: method,
    timestamp: Date.now(),
    params: JSON.stringify(params.rawParams),
  };
  await this.logToHCS(logEntry);
}

2. Conditional Logic (Type Guards)

When to use:

  • Your hook targets a small, known set of tools
  • You need to access specific parameters
  • Different tools require different handling

Approach: Check the method parameter and use Type Guards or safe casting.

Example:

public async postParamsNormalizationHook(
  params: PostParamsNormalizationParams,
  method: string
): Promise<any> {
  // Filter and branch based on the tool
  switch (method) {
    case 'transfer_hbar_tool':
    case 'transfer_hbar_with_allowance_tool': {
      // Both tools share the normalised 'hbarTransfers' structure
      const p = params.normalisedParams as { hbarTransfers: Array<{ accountId: string; amount: Hbar }> };

      // Example: log the total credited amount (skip the sender's negative entry)
      const total = p.hbarTransfers
        .filter(t => !t.amount.isNegative())
        .reduce((sum, t) => sum + t.amount.toBigNumber().toNumber(), 0);
      console.log(`Total HBAR being transferred: ${total}`);
      break;
    }
    case 'create_account_tool': {
      const p = params.normalisedParams as { initialBalance: number };
      console.log(`Creating account with balance: ${p.initialBalance}`);
      break;
    }
  }
}

3. Strategy Pattern (Dependency Injection)

When to use:

  • Your hook/policy needs to support "unknown" custom tools
  • You want maximum extensibility
  • Users should be able to extend your hook's behavior

Approach: Accept a Strategy Map (Record) during initialization that maps tool names to handling functions.

Example: MaxRecipientsPolicy uses a customStrategies map:

export class MaxRecipientsPolicy extends AbstractPolicy {
  constructor(
    private maxRecipients: number,
    additionalTools: string[] = [],
    private customStrategies: Record<string, (params: any) => number> = {}
  ) {
    super();
    this.relevantTools = [...this.defaultTools, ...additionalTools];

    // Validate that all additional tools have strategies
    for (const tool of additionalTools) {
      if (!this.customStrategies[tool]) {
        throw new Error(`Custom tool "${tool}" requires a strategy function`);
      }
    }
  }

  protected async shouldBlockPostParamsNormalization(
    params: PostParamsNormalizationParams,
    method: string
  ): Promise<boolean> {
    let recipientCount: number;

    // Use custom strategy if provided
    if (this.customStrategies[method]) {
      recipientCount = this.customStrategies[method](params.normalisedParams);
    } else {
      // Use default counting logic
      recipientCount = this.countRecipientsDefault(method, params.normalisedParams);
    }

    return recipientCount > this.maxRecipients;
  }
}

Usage:

const policy = new MaxRecipientsPolicy(
  5, // max 5 recipients
  ['my_custom_bulk_send'], // additional tool
  {
    'my_custom_bulk_send': (params) => params.recipients.length
  }
);

Creating New Hooks/Policies

Template for New Hook

import {
  AbstractHook,
  PreToolExecutionParams,
  PostParamsNormalizationParams,
  PostCoreActionParams,
  PostSecondaryActionParams
} from '@hashgraph/hedera-agent-kit';

export class MyCustomHook extends AbstractHook {
  name = 'My Custom Hook';
  description = 'Detailed explanation of what this hook does';
  relevantTools = ['create_account_tool', 'transfer_hbar_tool']; // List specific tools

  // Implement any of the 4 hook methods you need.

  async preToolExecutionHook(params: PreToolExecutionParams, method: string) {
    // Early in the lifecycle - before parameter normalization
    if (!this.relevantTools.includes(method)) return;

    // Access client, context, and raw params from the params object
    const client = params.client;
    const context = params.context;
    const rawParams = params.rawParams;

    // Your logic here
  }

  async postParamsNormalizationHook(params: PostParamsNormalizationParams, method: string) {
    // After parameters are validated and cleaned
    if (!this.relevantTools.includes(method)) return;

    // Access normalized parameters and client
    const normalizedParams = params.normalisedParams;
    const client = params.client;

    // Your logic here
  }

  async postCoreActionHook(params: PostCoreActionParams, method: string) {
    // After main logic (e.g., transaction created but not submitted)
    if (!this.relevantTools.includes(method)) return;

    // Access the core action result and client
    const txResult = params.coreActionResult;
    const client = params.client;

    // Your logic here
  }

  async postToolExecutionHook(params: PostSecondaryActionParams, method: string) {
    // After everything completes
    if (!this.relevantTools.includes(method)) return;

    // Access the final tool result and client
    const finalResult = params.toolResult;
    const client = params.client;

    // Your logic here
  }
}

Best Practices:

  1. Naming: Use descriptive names ending in Hook
  2. Description: Clearly explain what the hook does and when to use it
  3. Error Handling: Wrap your logic in try-catch to avoid breaking tool execution
  4. Performance: Keep hook logic lightweight
  5. State: Use instance fields on your hook class to persist data across invocations

Template for New Policy

import {
  AbstractPolicy,
  PreToolExecutionParams,
  PostParamsNormalizationParams,
  PostCoreActionParams,
  PostSecondaryActionParams
} from '@hashgraph/hedera-agent-kit';
import { Hbar } from '@hiero-ledger/sdk';

export class MyCustomPolicy extends AbstractPolicy {
  name = 'My Custom Policy';
  description = 'Detailed explanation of what this policy blocks';
  relevantTools = ['transfer_hbar_tool', 'airdrop_fungible_token_tool'];

  // Implement at least one of the shouldBlock... methods for the policy to function
  // Return true to BLOCK execution, false to ALLOW
  // These methods can return boolean or Promise<boolean>

  protected shouldBlockPreToolExecution(
    params: PreToolExecutionParams,
    method: string
  ): boolean | Promise<boolean> {
    // Block based on raw parameters (before normalization)
    const {rawParams} = params;

    // Example: block specific accounts
    // return rawParams.accountId === '0.0.blocked';

    return false;
  }

  protected shouldBlockPostParamsNormalization(
    params: PostParamsNormalizationParams,
    method: string
  ): boolean | Promise<boolean> {
    // Block based on normalized parameters
    const {normalisedParams} = params;

    // Example: payment-intent policy for transfer_hbar_tool โ€” block unless
    // every recipient and amount matches an approved payment intent.
    // For this tool the normalised amounts are Hbar instances, and the array
    // also contains the sender's negative (debit) entry โ€” skip it.
    const approved = { recipient: '0.0.800', maxAmount: new Hbar(10) };
    const recipients = (normalisedParams.hbarTransfers ?? []).filter(
      (t: any) => !t.amount.isNegative(),
    );
    return recipients.some(
      (t: any) =>
        String(t.accountId) !== approved.recipient ||
        t.amount.toTinybars().greaterThan(approved.maxAmount.toTinybars()),
    );
  }

  protected shouldBlockPostCoreAction(
    params: PostCoreActionParams,
    method: string
  ): boolean | Promise<boolean> {
    // Block based on the result of core action
    const {coreActionResult} = params;

    // Example: inspect the created transaction
    // return someValidationOn(coreActionResult);

    return false;
  }

  protected shouldBlockPostSecondaryAction(
    params: PostSecondaryActionParams,
    method: string
  ): boolean | Promise<boolean> {
    // Block based on final result (rarely used)
    const {toolResult} = params;

    return false;
  }
}

Best Practices:

  1. Naming: Use descriptive names ending in Policy
  2. Error Messages: The AbstractPolicy base class will create error messages. For custom messages, you can throw your own error in the shouldBlock... method
  3. Specificity: Be specific about what conditions trigger blocking
  4. Documentation: Clearly document what conditions will block execution
  5. Performance: Keep validation logic fast
  6. Return Values: Always return a boolean (true = block, false = allow)

Common Policy Patterns:

// 1. Threshold Policy
protected shouldBlockPostParamsNormalization(
  params: PostParamsNormalizationParams,
  method: string
): boolean {
  return params.normalisedParams.amount > this.maxAmount;
}

// 2. Allowlist/Blocklist Policy
protected shouldBlockPreToolExecution(
  params: PreToolExecutionParams,
  method: string
): boolean {
  return this.blockedAccounts.includes(params.rawParams.accountId);
}

// 3. Time-based Policy
protected shouldBlockPreToolExecution(
  params: PreToolExecutionParams,
  method: string
): boolean {
  const hour = new Date().getHours();
  return hour < 9 || hour > 17; // Block outside business hours
}

// 4. Rate Limiting Policy (using an instance field)
private callCount = 0;

protected shouldBlockPreToolExecution(
  params: PreToolExecutionParams,
  method: string
): boolean {
  this.callCount++;
  return this.callCount > this.maxCallsPerSession;
}

๐Ÿ“ How to Add to this Registry

When adding a new Hook or Policy:

  1. Implementation: Add the implementation file to packages/core/src/hooks or packages/core/src/policies
  2. Export: Export it from the appropriate index file
  3. Documentation: Add a new section in Available Hooks or Available Policies with:
    • Name and Type (Hook or Policy)
    • Description
    • Prerequisites (if any)
    • Configuration Parameters
    • Example Usage
  4. Testing: Add unit, integration and e2e tests in the corresponding test directory
  5. Update: Ensure your relevantTools are clearly defined