Table of Contents

Hook Lifecycle System

The hook system lets you intercept, inspect, and control agent execution at every lifecycle point. Hooks can allow, deny, or modify operations — enabling audit logging, safety guardrails, approval gates, and dynamic argument rewriting.

AgentHookEvent

All 13 lifecycle events:

Event When It Fires
WorkflowStarting Before the workflow begins
PreAgentPrompt Before sending a prompt to the LLM
PreToolCall Before invoking a tool
PostToolCall After a successful tool invocation
PostToolCallFailure After a tool invocation throws an exception
StepCompleted After a workflow step finishes
SubWorkflowStarting Before a sub-workflow begins
SubWorkflowCompleted After a sub-workflow finishes
PreCompact Before context compaction
PostCompact After context compaction
Checkpoint When a checkpoint is saved
Notification When an agent emits a notification
WorkflowCompleted After the workflow finishes

IAgentHook

public interface IAgentHook
{
    /// Regex pattern to match against "Event:StepName:ToolName". Null matches all.
    string? Matcher { get; }

    Task<HookResult> ExecuteAsync(AgentHookEvent hookEvent, HookContext context, CancellationToken ct = default);
}

The Matcher is a regex tested against a string built from "{Event}:{StepName}:{ToolName}". If null, the hook fires for every event.

HookResult & HookDecision

public enum HookDecision
{
    Allow,   // Let the operation proceed
    Deny,    // Block the operation
    Modify   // Allow but change arguments
}

public sealed class HookResult
{
    public HookDecision Decision { get; set; }
    public string? Reason { get; set; }
    public string? ModifiedArgs { get; set; }    // For Modify decisions
    public string? OutputMessage { get; set; }

    // Factory methods
    public static HookResult AllowResult(string? reason = null);
    public static HookResult DenyResult(string? reason = null);
    public static HookResult ModifyResult(string modifiedArgs, string? reason = null);
}

HookPipeline

HookPipeline runs hooks in registration order. A Deny result stops execution immediately. For Modify, the last modifier wins:

var pipeline = new HookPipeline();
pipeline.Add(new AuditLogHook());          // Runs first
pipeline.Add(new SafetyGuardrailHook());   // Can deny
pipeline.Add(new ArgRewriteHook());        // Can modify

// Use with AgentLoopStep
var workflow = new WorkflowBuilder()
    .AgentLoop(provider, registry, options =>
    {
        options.Hooks = pipeline;
    })
    .Build();

Handler Types

CodeHook — Inline Delegates

The simplest hook type. Wrap any Func<HookContext, Task<HookResult>>:

var auditHook = new CodeHook(
    handler: async ctx =>
    {
        Console.WriteLine($"[AUDIT] {ctx.Event}: tool={ctx.ToolName}, args={ctx.ToolArgs}");
        return HookResult.AllowResult();
    },
    matcher: "PreToolCall"  // Only fire for PreToolCall events
);

var denyDangerousHook = new CodeHook(
    handler: async ctx =>
    {
        if (ctx.ToolName == "delete_database")
            return HookResult.DenyResult("Dangerous tool blocked");
        return HookResult.AllowResult();
    },
    matcher: "PreToolCall:.*:delete_.*"  // Match tools starting with "delete_"
);

CommandHook — Shell Commands

Runs an external process. Sends HookContext as JSON on stdin, reads HookResult JSON from stdout:

var hook = new CommandHook(
    command: "python",
    args: new[] { "validate_tool_call.py" },
    matcher: "PreToolCall",
    timeout: TimeSpan.FromSeconds(10)
);

stdin (JSON):

{
    "event": "PreToolCall",
    "stepName": "Agent",
    "toolName": "run_query",
    "toolArgs": "{\"sql\": \"SELECT * FROM users\"}"
}

stdout (JSON):

{
    "Decision": 0,
    "Reason": "Query approved",
    "ModifiedArgs": null
}

If the process exits with a non-zero code, the hook returns Deny.

PromptHook — LLM-Evaluated

Uses an IAgentProvider to evaluate whether to allow or deny. The LLM response is scanned for keywords like "deny", "block", or "reject":

var hook = new PromptHook(
    provider: llmProvider,
    promptTemplate: """
        Should this tool call be allowed?
        Event: {event}
        Tool: {toolName}
        Arguments: {toolArgs}
        
        Respond with ALLOW or DENY and a brief reason.
        """,
    matcher: "PreToolCall"
);

Template variables: {event}, {toolName}, {toolArgs}.

HookContext

The context object passed to every hook:

public sealed class HookContext
{
    public AgentHookEvent Event { get; set; }
    public string? StepName { get; set; }
    public string? ToolName { get; set; }
    public string? ToolArgs { get; set; }
    public ToolResult? ToolResult { get; set; }
    public IWorkflowContext? WorkflowContext { get; set; }
    public IDictionary<string, object?> Metadata { get; set; }
    public IList<ConversationMessage> Messages { get; set; }
}

Comparison with Claude Code Hooks

Feature WorkflowFramework Hooks Claude Code Hooks
Events 13 lifecycle events Tool calls, notifications
Matching Regex on Event:Step:Tool Glob patterns
Decisions Allow / Deny / Modify Allow / Deny
Arg rewriting Yes (ModifiedArgs) No
Handler types Code, Command, Prompt (LLM) Command (shell only)
Pipeline ordering Explicit registration order File-based ordering
LLM evaluation Built-in (PromptHook) Not available
Context access Full workflow context Limited to tool call info

Complete Example

var pipeline = new HookPipeline();

// 1. Audit all events
pipeline.Add(new CodeHook(async ctx =>
{
    logger.LogInformation("Hook: {Event} tool={Tool}", ctx.Event, ctx.ToolName);
    return HookResult.AllowResult();
}));

// 2. Block file deletion tools
pipeline.Add(new CodeHook(
    async ctx => ctx.ToolName?.Contains("delete") == true
        ? HookResult.DenyResult("Deletion not allowed")
        : HookResult.AllowResult(),
    matcher: "PreToolCall"));

// 3. External validation via shell script
pipeline.Add(new CommandHook("./validate.sh", matcher: "PreToolCall"));

// 4. LLM safety check for sensitive tools
pipeline.Add(new PromptHook(provider,
    "Is this tool call safe? Tool: {toolName}, Args: {toolArgs}. Reply ALLOW or DENY.",
    matcher: "PreToolCall:.*:run_query"));

var workflow = new WorkflowBuilder()
    .AgentLoop(provider, registry, opts => opts.Hooks = pipeline)
    .Build();