Skip to content

Latest commit

 

History

History
558 lines (446 loc) · 15.7 KB

post-tool-use.md

File metadata and controls

558 lines (446 loc) · 15.7 KB

Post-tool use hook

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

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

Note

Failure variant: onPostToolUse only fires for successful tool executions. To observe failed tool calls, register onPostToolUseFailure (on_post_tool_use_failure in Python, OnPostToolUseFailure in Go/.NET, on_post_tool_use_failure in Rust). The handler receives the originating session, tool name and arguments, and an error string extracted from the failure result. It can return { additionalContext: string } to inject guidance for the model. The same optional tool-call and tracing metadata described in Hook metadata is available on both success and failure hooks. Other outcomes can invoke neither hook; a pre-hook does not guarantee a matching post-hook.

Hook signature

Node.js / TypeScript
import type {
  PostToolUseHookInput,
  HookInvocation,
  PostToolUseHookOutput,
} from "@github/copilot-sdk";
type PostToolUseHandler = (
  input: PostToolUseHookInput,
  invocation: HookInvocation,
) => Promise<PostToolUseHookOutput | null | undefined>;
type PostToolUseHandler = (
  input: PostToolUseHookInput,
  invocation: HookInvocation,
) => Promise<PostToolUseHookOutput | null | undefined>;
Python
from copilot.session import PostToolUseHookInput, PostToolUseHookOutput
from copilot.session import PostToolUseHandler as SdkPostToolUseHandler
from typing import Callable, Awaitable

PostToolUseHandler = Callable[
    [PostToolUseHookInput, dict[str, str]],
    Awaitable[PostToolUseHookOutput | None]
]

def check(handler: PostToolUseHandler) -> SdkPostToolUseHandler:
    return handler
PostToolUseHandler = Callable[
    [PostToolUseHookInput, dict[str, str]],
    Awaitable[PostToolUseHookOutput | None]
]
Go
package main

import copilot "github.com/github/copilot-sdk/go"

// Aliases let the signature below use the package's own type names.
type (
    PostToolUseHookInput  = copilot.PostToolUseHookInput
    HookInvocation        = copilot.HookInvocation
    PostToolUseHookOutput = copilot.PostToolUseHookOutput
)

type PostToolUseHandler func(
    input PostToolUseHookInput,
    invocation HookInvocation,
) (*PostToolUseHookOutput, error)

var _ = copilot.PostToolUseHandler(PostToolUseHandler(nil))

func main() {}
type PostToolUseHandler func(
    input PostToolUseHookInput,
    invocation HookInvocation,
) (*PostToolUseHookOutput, error)
.NET
using GitHub.Copilot;

public delegate Task<PostToolUseHookOutput?> PostToolUseHandler(
    PostToolUseHookInput input,
    HookInvocation invocation);

static class PostToolUseHandlerCheck
{
    static SessionHooks Check(PostToolUseHandler handler) =>
        new() { OnPostToolUse = handler.Invoke };
}
public delegate Task<PostToolUseHookOutput?> PostToolUseHandler(
    PostToolUseHookInput input,
    HookInvocation invocation);
Java
import com.github.copilot.rpc.*;
import java.util.concurrent.CompletableFuture;

public class PostToolUseSignature {
    static class Documented {
        @FunctionalInterface
        public interface PostToolUseHandler {
            CompletableFuture<PostToolUseHookOutput> handle(
                PostToolUseHookInput input,
                HookInvocation invocation);
        }
    }

    // The documented interface and the SDK's accept each other's implementations.
    static PostToolUseHandler toSdk(Documented.PostToolUseHandler documented) {
        return documented::handle;
    }

    static Documented.PostToolUseHandler fromSdk(PostToolUseHandler sdk) {
        return sdk::handle;
    }

    public static void main(String[] args) {}
}
@FunctionalInterface
public interface PostToolUseHandler {
    CompletableFuture<PostToolUseHookOutput> handle(
        PostToolUseHookInput input,
        HookInvocation invocation);
}

Input

Field Type Description
timestamp SDK timestamp type When the hook was triggered
workingDirectory string Current working directory
sessionId string Session reported by the hook input, not necessarily the callback owner's session
toolCallId string (optional) Original tool-call ID, unchanged by pre-hook argument modifications
traceparent string (optional) Existing W3C trace context
tracestate string (optional) Existing W3C vendor trace state
toolName string Name of the tool that was called
toolArgs object Arguments that were passed to the tool
toolResult object Result returned by the tool

Tracing fields are exposed in TypeScript, Python, Go, Rust, and Java, not .NET. See Hook metadata for language-specific accessors and compatibility with older runtimes. Correlate with the pre-hook and tool-execution events using (input.sessionId, input.toolCallId).

Output

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

Field Type Description
modifiedResult object Modified result to use instead of original
additionalContext string Extra context injected into the conversation
suppressOutput boolean If 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
from copilot import CopilotClient
from copilot.session import PermissionHandler

client = CopilotClient()

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(on_permission_request=PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use})
Go
package main

import (
	"context"
	"fmt"
	copilot "github.com/github/copilot-sdk/go"
)

func main() {
	client := copilot.NewClient(nil)
	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
			},
		},
	})
	_ = session
}
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);
        },
    },
});
Java
import com.github.copilot.*;
import com.github.copilot.rpc.*;
import java.util.concurrent.CompletableFuture;

public class PostToolUseExample {
    public static void main(String[] args) throws Exception {
        try (var client = new CopilotClient()) {
            var hooks = new SessionHooks()
                .setOnPostToolUse((input, invocation) -> {
                    System.out.println("[" + invocation.getSessionId() + "] Tool: " + input.getToolName());
                    System.out.println("  Args: " + input.getToolArgs());
                    System.out.println("  Result: " + input.getToolResult());
                    return CompletableFuture.completedFuture(null);
                });

            var session = client.createSession(
                new SessionConfig()
                    .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
                    .setHooks(hooks)
            ).get();
        }
    }
}
import com.github.copilot.*;
import com.github.copilot.rpc.*;
import java.util.concurrent.CompletableFuture;

var hooks = new SessionHooks()
    .setOnPostToolUse((input, invocation) -> {
        System.out.println("[" + invocation.getSessionId() + "] Tool: " + input.getToolName());
        System.out.println("  Args: " + input.getToolArgs());
        System.out.println("  Result: " + input.getToolResult());
        return CompletableFuture.completedFuture(null);
    });

var session = client.createSession(
    new SessionConfig()
        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
        .setHooks(hooks)
).get();

Redact sensitive data

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

const redact = (text: string) =>
  SENSITIVE_PATTERNS.reduce((result, pattern) => result.replace(pattern, "[REDACTED]"), text);

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      const { textResultForLlm, sessionLog } = input.toolResult;
      const redacted = {
        ...input.toolResult,
        textResultForLlm: redact(textResultForLlm),
        sessionLog: sessionLog && redact(sessionLog),
      };

      if (redacted.textResultForLlm !== textResultForLlm || redacted.sessionLog !== sessionLog) {
        return { modifiedResult: redacted };
      }
      return null;
    },
  },
});

Truncate large results

const MAX_RESULT_LENGTH = 10000;

const session = await client.createSession({
  hooks: {
    onPostToolUse: async (input) => {
      const text = input.toolResult.textResultForLlm;

      if (text.length > MAX_RESULT_LENGTH) {
        return {
          modifiedResult: {
            ...input.toolResult,
            textResultForLlm: text.substring(0, MAX_RESULT_LENGTH) + "...",
          },
          additionalContext: `Note: Result was truncated from ${text.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: Date;
  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