How Lifecycle Hooks Gate Tool Calls and Drive Auditing in Kimi Code

Kimi Code's agent engine intercepts every LLM tool invocation through a serializable pipeline of four lifecycle hooks—prepareToolExecution, authorizeToolExecution, finalizeToolResult, and onDidExecuteTool—enabling pre-execution gating, synthetic result injection, and comprehensive post-hoc auditing without modifying core scheduler logic.

Kimi Code processes each conversational turn through an extensible tool-call lifecycle defined in packages/agent-core/src/loop/types.ts and orchestrated within packages/agent-core/src/loop/tool-call.ts. These lifecycle hooks provide injection points for security policies, argument validation, and audit logging while maintaining strict guarantees that every tool.call event pairs with exactly one tool.result event in the transcript.

The Four Stages of the Tool-Call Lifecycle

The pipeline invokes hooks in provider order, ensuring deterministic execution even when multiple extensions register callbacks. Each stage serves a distinct purpose in the validation and observation pipeline.

Hook Points and Execution Timing

The system exposes four primary interception points:

  • prepareToolExecution: Runs after the LLM's tool-call payload parses but before recording the tool.call event. This hook validates or rewrites arguments, synthesizes cached results, or aborts execution entirely.
  • authorizeToolExecution: Executes after pre-validation succeeds but before the ToolScheduler queues the operation. Enforces permissions, rate limits, or organizational policies.
  • finalizeToolResult: Invokes after tool completion (or synthetic result generation) but before the turn concludes. Post-processes outputs, redacts sensitive data, or signals stopTurn to halt further processing.
  • onDidExecuteTool: An L4 telemetry hook firing after transcript recording. Handles audit events, deduplication logic, and goal-service updates without affecting execution order or transcript fidelity.

Decision Types and Flow Control

Gating hooks return specific decision enums that determine pipeline behavior:

  • PrepareToolExecutionDecision: Returns allowed, synthetic, blocked, or hookFailed. A blocked or hookFailed decision short-circuits execution and emits a synthetic tool.result containing the hook's output.
  • AuthorizeToolExecutionResult: Returns allowed or blocked. Blocking triggers immediate synthetic result generation, bypassing the scheduler entirely.

Because these decisions occur before the ToolScheduler receives the call, blocked operations never consume execution resources or trigger downstream resource-conflict handling.

Gating Tool Calls Before Execution

The primary security mechanism combines prepareToolExecution and authorizeToolExecution to intercept calls before they reach the execution layer. When either hook returns a non-allowed decision, the pipeline synthesizes a result immediately:

// Simplified flow from packages/agent-core/src/loop/tool-call.ts
if (hooks?.prepareToolExecution) {
  const prep = await hooks.prepareToolExecution(ctx);
  if (prep.kind !== 'allowed') return synthResult(prep);
}
if (hooks?.authorizeToolExecution) {
  const auth = await hooks.authorizeToolExecution(ctx);
  if (auth.kind !== 'allowed') return synthResult(auth);
}

A blocked decision prevents the call from reaching the tool executor, emitting a synthetic tool.result with the hook's explanatory output. The turn may optionally terminate via stopTurn = true. A synthetic decision allows the hook to supply a result directly—such as a cached response—bypassing actual execution while maintaining transcript consistency.

Auditing Decisions and Post-Processing

Auditing occurs through hooks that execute after the tool result finalizes. The finalizeToolResult hook modifies or redacts data before it enters the transcript, while onDidExecuteTool observes the finalized record without altering execution order.

The most common audit pattern registers a listener on toolExecutor.hooks.onDidExecuteTool, as implemented in packages/agent-core/src/agent/turn/tool-dedup.ts:

toolExecutor.hooks.onDidExecuteTool.register(
  'audit',
  async (ctx, next) => {
    // ctx contains toolCall, toolName, args, and result
    await auditService.record(ctx.toolCall.id, ctx.result);
    return next(); // continue the chain
  },
);

This L4 hook guarantees that the transcript remains a faithful, replayable log of the LLM-agent interaction while allowing extensions to react to decisions through external services, background tasks, or telemetry pipelines.

Practical Implementation Patterns

Blocking Dangerous Tools via prepareToolExecution

Register a validation hook to intercept unsafe operations before they execute:

import { toolExecutor } from '@moonshot-ai/agent-core';

toolExecutor.hooks.prepareToolExecution.register(
  'block-dangerous-tools',
  async (ctx, next) => {
    if (ctx.toolName === 'delete-filesystem') {
      return {
        kind: 'blocked',
        args: ctx.args,
        output: 'Tool blocked by policy: unsafe filesystem deletion.',
      };
    }
    return next();
  },
);

The delete-filesystem tool never reaches the scheduler; instead, the transcript records a synthetic result containing the policy violation message.

Synthesizing Cached Responses

Avoid redundant network requests by returning synthetic results for cache hits:

toolExecutor.hooks.prepareToolExecution.register(
  'cached-search',
  async (ctx, next) => {
    if (ctx.toolName === 'web-search' && ctx.args.query === 'latest-price') {
      return {
        kind: 'synthetic',
        args: ctx.args,
        result: { content: 'Cached price: $123.45' },
      };
    }
    return next();
  },
);

The LLM receives the cached answer immediately, and the transcript contains a valid tool.result event without invoking the actual search provider.

Recording Comprehensive Audit Trails

Capture every decision—including blocked and synthetic calls—for compliance analysis:

import { toolExecutor } from '@moonshot-ai/agent-core';
import { auditService } from './audit-service';

toolExecutor.hooks.onDidExecuteTool.register(
  'record-audit',
  async (ctx, next) => {
    await auditService.log({
      id: ctx.toolCall.id,
      name: ctx.toolName,
      args: ctx.args,
      result: ctx.result,
      timestamp: Date.now(),
    });
    return next();
  },
);

This captures the complete execution context regardless of whether the result originated from a live tool, cache, or security block.

Summary

  • Lifecycle hooks in Kimi Code provide four injection points (prepareToolExecution, authorizeToolExecution, finalizeToolResult, onDidExecuteTool) for intercepting tool calls.
  • Gating occurs through prepareToolExecution and authorizeToolExecution, which can block execution or synthesize results before the ToolScheduler consumes resources.
  • Synthetic results maintain transcript integrity by ensuring every tool.call pairs with a tool.result, even when execution aborts.
  • Auditing relies on onDidExecuteTool L4 hooks that observe finalized results without mutating transcript order, enabling telemetry and compliance logging.
  • The pipeline wraps all steps in try…finally blocks (lines 80-84 of tool-call.ts) to settle spawned tasks and prevent dangling promises.

Frequently Asked Questions

What is the difference between prepareToolExecution and authorizeToolExecution?

prepareToolExecution runs first and handles argument validation, transformation, and caching logic, while authorizeToolExecution enforces policy decisions like permissions and rate limits after pre-validation succeeds. Both can block execution, but prepareToolExecution also supports argument rewriting and synthetic result injection, whereas authorizeToolExecution focuses strictly on access control.

Can lifecycle hooks modify tool arguments before execution?

Yes. The prepareToolExecution hook receives the parsed arguments in ctx.args and can return a modified argument object within an allowed decision. This enables normalization, default value injection, or sanitization before the tool executor receives the payload.

How does Kimi Code ensure audit logs capture blocked tool calls?

The pipeline emits synthetic tool.result events for any call blocked by prepareToolExecution or authorizeToolExecution. The onDidExecuteTool hook fires after transcript recording, meaning it observes both successful executions and synthetic results from blocked calls, ensuring comprehensive audit coverage without execution gaps.

What happens to the transcript when a hook synthesizes a result?

The transcript remains consistent and replayable. When a hook returns a synthetic or blocked decision, the system generates a synthetic tool.result event containing the hook's output payload. This pairs with the original tool.call event in provider order, maintaining the strict one-to-one relationship required for accurate conversation reconstruction.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →