Liking cljdoc? Tell your friends :D

Post-Tool Use Hook

The onPostToolUse hook is called after a tool executes. Use it to:

  • Transform or filter tool results
  • Log tool execution for auditing
  • Add context based on results
  • Suppress results from the conversation

Hook Signature

Node.js / TypeScript
type PostToolUseHandler = (
  input: PostToolUseHookInput,
  invocation: HookInvocation
) => Promise<PostToolUseHookOutput | null | undefined>;
Python
PostToolUseHandler = Callable[
    [PostToolUseHookInput, HookInvocation],
    Awaitable[PostToolUseHookOutput | None]
]
Go
type PostToolUseHandler func(
    input PostToolUseHookInput,
    invocation HookInvocation,
) (*PostToolUseHookOutput, error)
.NET
public delegate Task<PostToolUseHookOutput?> PostToolUseHandler(
    PostToolUseHookInput input,
    HookInvocation invocation);

Input

FieldTypeDescription
timestampnumberUnix timestamp when the hook was triggered
cwdstringCurrent working directory
toolNamestringName of the tool that was called
toolArgsobjectArguments that were passed to the tool
toolResultobjectResult returned by the tool

Output

Return null or undefined to pass through the result unchanged. Otherwise, return an object with any of these fields:

FieldTypeDescription
modifiedResultobjectModified result to use instead of original
additionalContextstringExtra context injected into the conversation
suppressOutputbooleanIf true, result won't appear in conversation

Examples

Log All Tool Results

Node.js / TypeScript
const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input, invocation) => {
      console.log(`[${invocation.sessionId}] Tool: ${input.toolName}`);
      console.log(`  Args: ${JSON.stringify(input.toolArgs)}`);
      console.log(`  Result: ${JSON.stringify(input.toolResult)}`);
      return null; // Pass through unchanged
    },
  },
});
Python
async def on_post_tool_use(input_data, invocation):
    print(f"[{invocation['session_id']}] Tool: {input_data['toolName']}")
    print(f"  Args: {input_data['toolArgs']}")
    print(f"  Result: {input_data['toolResult']}")
    return None  # Pass through unchanged

session = await client.create_session({
    "hooks": {"on_post_tool_use": on_post_tool_use}
})
Go
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
    Hooks: &copilot.SessionHooks{
        OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {
            fmt.Printf("[%s] Tool: %s\n", inv.SessionID, input.ToolName)
            fmt.Printf("  Args: %v\n", input.ToolArgs)
            fmt.Printf("  Result: %v\n", input.ToolResult)
            return nil, nil
        },
    },
})
.NET
var session = await client.CreateSessionAsync(new SessionConfig
{
    Hooks = new SessionHooks
    {
        OnPostToolUse = (input, invocation) =>
        {
            Console.WriteLine($"[{invocation.SessionId}] Tool: {input.ToolName}");
            Console.WriteLine($"  Args: {input.ToolArgs}");
            Console.WriteLine($"  Result: {input.ToolResult}");
            return Task.FromResult<PostToolUseHookOutput?>(null);
        },
    },
});

Redact Sensitive Data

const SENSITIVE_PATTERNS = [
  /api[_-]?key["\s:=]+["']?[\w-]+["']?/gi,
  /password["\s:=]+["']?[\w-]+["']?/gi,
  /secret["\s:=]+["']?[\w-]+["']?/gi,
];

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      if (typeof input.toolResult === "string") {
        let redacted = input.toolResult;
        for (const pattern of SENSITIVE_PATTERNS) {
          redacted = redacted.replace(pattern, "[REDACTED]");
        }
        
        if (redacted !== input.toolResult) {
          return { modifiedResult: redacted };
        }
      }
      return null;
    },
  },
});

Truncate Large Results

const MAX_RESULT_LENGTH = 10000;

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      const resultStr = JSON.stringify(input.toolResult);
      
      if (resultStr.length > MAX_RESULT_LENGTH) {
        return {
          modifiedResult: {
            truncated: true,
            originalLength: resultStr.length,
            content: resultStr.substring(0, MAX_RESULT_LENGTH) + "...",
          },
          additionalContext: `Note: Result was truncated from ${resultStr.length} to ${MAX_RESULT_LENGTH} characters.`,
        };
      }
      return null;
    },
  },
});

Add Context Based on Results

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      // If a file read returned an error, add helpful context
      if (input.toolName === "read_file" && input.toolResult?.error) {
        return {
          additionalContext: "Tip: If the file doesn't exist, consider creating it or checking the path.",
        };
      }
      
      // If shell command failed, add debugging hint
      if (input.toolName === "shell" && input.toolResult?.exitCode !== 0) {
        return {
          additionalContext: "The command failed. Check if required dependencies are installed.",
        };
      }
      
      return null;
    },
  },
});

Filter Error Stack Traces

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      if (input.toolResult?.error && input.toolResult?.stack) {
        // Remove internal stack trace details
        return {
          modifiedResult: {
            error: input.toolResult.error,
            // Keep only first 3 lines of stack
            stack: input.toolResult.stack.split("\n").slice(0, 3).join("\n"),
          },
        };
      }
      return null;
    },
  },
});

Audit Trail for Compliance

interface AuditEntry {
  timestamp: number;
  sessionId: string;
  toolName: string;
  args: unknown;
  result: unknown;
  success: boolean;
}

const auditLog: AuditEntry[] = [];

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input, invocation) => {
      auditLog.push({
        timestamp: input.timestamp,
        sessionId: invocation.sessionId,
        toolName: input.toolName,
        args: input.toolArgs,
        result: input.toolResult,
        success: !input.toolResult?.error,
      });
      
      // Optionally persist to database/file
      await saveAuditLog(auditLog);
      
      return null;
    },
  },
});

Suppress Noisy Results

const NOISY_TOOLS = ["list_directory", "search_codebase"];

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      if (NOISY_TOOLS.includes(input.toolName)) {
        // Summarize instead of showing full result
        const items = Array.isArray(input.toolResult) 
          ? input.toolResult 
          : input.toolResult?.items || [];
        
        return {
          modifiedResult: {
            summary: `Found ${items.length} items`,
            firstFew: items.slice(0, 5),
          },
        };
      }
      return null;
    },
  },
});

Best Practices

  1. Return null when no changes needed - This is more efficient than returning an empty object or the same result.

  2. Be careful with result modification - Changing results can affect how the model interprets tool output. Only modify when necessary.

  3. Use additionalContext for hints - Instead of modifying results, add context to help the model interpret them.

  4. Consider privacy when logging - Tool results may contain sensitive data. Apply redaction before logging.

  5. Keep hooks fast - Post-tool hooks run synchronously. Heavy processing should be done asynchronously or batched.

See Also

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close