mcpField
H
Hedera Policy Creation

Hedera Policy Creation

github.com/hedera-devSecurity28500 installsUpdated 8w ago
Repository

About Hedera Policy Creation

Hedera Policy Creation is a skill for Hedera Agent Kit developers who need to write policies that block tool calls. It explains lifecycle stages, built-in policies, registration, and implementation patterns for thresholds, rate limits, and access lists.

Key features

  1. 01Write blocking rules with AbstractPolicy
  2. 02Check calls at tool lifecycle stages
  3. 03Implement thresholds, lists, and time rules
  4. 04Use Context for session rate limits
  5. 05Register policies in Context.hooks
  6. 06Customize blocking error messages

Use cases

  1. 01Limit recipients in transfers or airdrops
  2. 02Block tool calls above an amount threshold
  3. 03Restrict specific tools or accounts
  4. 04Control actions by business hours or session call count

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/hedera-dev/hedera-skills --skill hedera-policy-creation

After installing, type/hedera-policy-creationin Claude Code to confirm it loaded.

Bundled filesexamples/max-amount-policy/index.tsreferences/policy-interface.md

Details

From the project README

Creating Hedera Agent Kit Policies

Policies are blocking validation rules layered on the BaseTool lifecycle. A policy checks the call at one or more stages and throws to abort execution — the error propagates back to the LLM agent. Use them to enforce thresholds, allowlists/blocklists, rate limits, business hours, compliance rules, etc.

Policies fire only for tools that extend BaseTool. A tool that implements the Tool interface directly without extending BaseTool bypasses policies entirely.

Policy vs. hook

GoalUse
Stop a call when a condition holdsPolicy (this skill)
Reject specific tool methodsPolicy
Threshold / rate-limit / allowlist / blocklistPolicy
Log, audit, enrich contextHook (see agent-kit-hook skill)

AbstractPolicy extends AbstractHook, so policies live in the same Context.hooks array and run through the same lifecycle.

Built-in policies live in @hashgraph/hedera-agent-kit/policies:

  • RejectToolPolicy(relevantTools: string[]) — blocks the listed tool methods unconditionally
  • MaxRecipientsPolicy(maxRecipients, additionalTools?, customStrategies?) — caps recipient count for transfer/airdrop tools

How blocking actually works

You never override preToolExecutionHook or any of the other *Hook methods on a policy — those are wired up by the base class to call your shouldBlock... method and throw if it returns true.

Override one or more of these instead:

abstract class AbstractPolicy extends AbstractHook {
  abstract name: string;
  abstract description?: string;
  abstract relevantTools: string[];

  protected shouldBlockPreToolExecution(params, method): boolean | Promise<boolean>;
  protected shouldBlockPostParamsNormalization(params, method): boolean | Promise<boolean>;
  protected shouldBlockPostCoreAction(params, method): boolean | Promise<boolean>;
  protected shouldBlockPostSecondaryAction(params, method): boolean | Promise<boolean>;
}

Defaults all return false (allow). Returning true causes the base class to throw a generic block error. Throwing your own error from shouldBlock... lets you control the message.

Don't override preToolExecutionHook etc. — those are @internal on AbstractPolicy. They drive the block flow. If you override them, your policy will never block.

Picking the stage

StageWhat's availableGood for
shouldBlockPreToolExecutionrawParams onlyallowlist/blocklist by tool method, time-based, rate limits
shouldBlockPostParamsNormalization+ normalisedParamsthresholds on resolved values, recipient counts
shouldBlockPostCoreAction+ coreActionResultinspect the built Transaction before submission
shouldBlockPostSecondaryAction+ toolResultvery rare — call already executed; usually too late to "block"

Most policies live at stage 1 (cheapest) or stage 3 (after defaults are filled in).

Skeleton

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

export class MyPolicy extends AbstractPolicy {
  name = 'My Policy';
  description = 'What this policy blocks, in one sentence';
  relevantTools = ['transfer_hbar', 'transfer_fungible_token'];

  // Stage 1 — block by raw input or method
  protected shouldBlockPreToolExecution(
    params: PreToolExecutionParams,
    method: string,
  ): boolean {
    if (!this.relevantTools.includes(method)) return false;
    // throw new Error('<custom message>') for tailored errors
    return /* condition */ false;
  }

  // Stage 3 — block on resolved/normalised values
  protected shouldBlockPostParamsNormalization(
    params: PostParamsNormalizationParams,
    method: string,
  ): boolean {
    if (!this.relevantTools.includes(method)) return false;
    return /* condition */ false;
  }
}

relevantTools is informational unless you check it — the kit dispatches your policy for every tool call. Always filter by method first thing in the method.

Registering policies

Policies are AbstractHook instances. They go in the same Context.hooks array as observation hooks:

import { AgentMode } from '@hashgraph/hedera-agent-kit';
import { allCorePlugins } from '@hashgraph/hedera-agent-kit/plugins';
import { RejectToolPolicy, MaxRecipientsPolicy } from '@hashgraph/hedera-agent-kit/policies';
import { HederaLangchainToolkit } from '@hashgraph/hedera-agent-kit-langchain';
import { MyPolicy } from './my-policy';

const toolkit = new HederaLangchainToolkit({
  client,
  configuration: {
    plugins: allCorePlugins,
    context: {
      mode: AgentMode.AUTONOMOUS,
      hooks: [
        new RejectToolPolicy(['delete_account', 'freeze_token']),
        new MaxRecipientsPolicy(5),
        new MyPolicy(),
      ],
    },
  },
});

Policies run in array order at each stage — the first one to return true (or throw) wins.

Common patterns

Threshold
protected shouldBlockPostParamsNormalization(
  params: PostParamsNormalizationParams,
  method: string,
): boolean {
  if (!this.relevantTools.includes(method)) return false;
  const p = params.normalisedParams as { amount?: number };
  return (p.amount ?? 0) > this.maxAmount;
}
Allowlist / Blocklist
protected shouldBlockPreToolExecution(
  params: PreToolExecutionParams,
  method: string,
): boolean {
  if (!this.relevantTools.includes(method)) return false;
  const accountId = (params.rawParams as { accountId?: string }).accountId;
  return accountId !== undefined && this.blockedAccounts.has(accountId);
}
Time-based
protected shouldBlockPreToolExecution(
  _params: PreToolExecutionParams,
  method: string,
): boolean {
  if (!this.relevantTools.includes(method)) return false;
  const hour = new Date().getUTCHours();
  return hour < 9 || hour > 17;  // outside business hours
}
Rate limiting (uses Context as scratchpad)
protected shouldBlockPreToolExecution(
  params: PreToolExecutionParams,
  method: string,
): boolean {
  if (!this.relevantTools.includes(method)) return false;
  const counters = ((params.context as any).__rateCounts ??= {} as Record<string, number>);
  counters[method] = (counters[method] ?? 0) + 1;
  return counters[method] > this.maxCallsPerSession;
}
Strategy map (pluggable for custom tools)

When a single policy needs to support tools the kit doesn't know about, accept a Record<string, (params) => SomeMetric> and validate that every tool added via additionalTools has a strategy. This is the pattern of MaxRecipientsPolicy:

constructor(
  private maxRecipients: number,
  additionalTools: string[] = [],
  private customStrategies: Record<string, (p: any) => number> = {},
) {
  super();
  this.relevantTools = [...DEFAULT_TOOLS, ...additionalTools];
  for (const tool of additionalTools) {
    if (!this.customStrategies[tool]) {
      throw new Error(`Custom tool "${tool}" requires a strategy function`);
    }
  }
}

Custom error messages

The default block error is generic. To control the message, throw from your shouldBlock... method instead of returning true:

protected shouldBlockPostParamsNormalization(
  params: PostParamsNormalizationParams,
  method: string,
): boolean {
  if (!this.relevantTools.includes(method)) return false;
  const p = params.normalisedParams as { amount?: number };
  if ((p.amount ?? 0) > this.maxAmount) {
    throw new Error(
      `[max-amount-policy] Refusing ${method}: amount ${p.amount} exceeds limit ${this.maxAmount}`,
    );
  }
  return false;
}

The agent receives the thrown message verbatim — make it informative.

Best practices

  1. Filter by relevantTools at the top of every shouldBlock... method
  2. Pick the earliest stage that has the data you need — stage 1 is cheapest
  3. Throw with a clear message when blocking; the LLM sees this
  4. Don't override *Hook methods — only shouldBlock...
  5. Don't do I/O on the hot path unless you must — policies run synchronously in the lifecycle
  6. Document relevantTools defaults so consumers understand what your policy guards
  7. Tag the error message with [<policy-name>] for easy log correlation

Resources

  • references/policy-interface.md — full AbstractPolicy contract, when each shouldBlock fires
  • examples/max-amount-policy/ — threshold policy that blocks transfers above a cap

Common imports

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

// Built-in implementations live in the /policies subpath
import { RejectToolPolicy, MaxRecipientsPolicy } from '@hashgraph/hedera-agent-kit/policies';

Comments

Markdown supported