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.
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 handlerPostToolUseHandler = 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);
}| 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).
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 |
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();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;
},
},
});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;
},
},
});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;
},
},
});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;
},
},
});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;
},
},
});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;
},
},
});-
Return
nullwhen no changes needed - This is more efficient than returning an empty object or the same result. -
Be careful with result modification - Changing results can affect how the model interprets tool output. Only modify when necessary.
-
Use
additionalContextfor hints - Instead of modifying results, add context to help the model interpret them. -
Consider privacy when logging - Tool results may contain sensitive data. Apply redaction before logging.
-
Keep hooks fast - Post-tool hooks run synchronously. Heavy processing should be done asynchronously or batched.