{"meta":{"title":"Post-tool use hook","intro":"The onPostToolUse hook is called after a tool executes successfully. Use it to:","product":"GitHub Copilot","breadcrumbs":[{"href":"/en/copilot","title":"GitHub Copilot"},{"href":"/en/copilot/how-tos","title":"How-tos"},{"href":"/en/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/en/copilot/how-tos/copilot-sdk/hooks","title":"Use hooks"},{"href":"/en/copilot/how-tos/copilot-sdk/hooks/post-tool-use","title":"Post Tool Use"}],"documentType":"article"},"body":"# Post-tool use hook\n\nThe onPostToolUse hook is called after a tool executes successfully. Use it to:\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n* Transform or filter tool results\n* Log tool execution for auditing\n* Add context based on results\n* Suppress results from the conversation\n\n> **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 `{ sessionId, toolName, toolArgs, error, timestamp, workingDirectory }` — the `error` field is a string extracted from the tool's failure result — and may return `{ additionalContext: string }` to inject extra guidance for the model (e.g. retry hints). See the [Session hooks](/en/copilot/how-tos/copilot-sdk/hooks/hooks-overview) for the full list. <a id=\"failure-variant\"></a>\n\n## Hook signature\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\ntype PostToolUseHandler = (\n  input: PostToolUseHookInput,\n  invocation: HookInvocation,\n) => Promise<PostToolUseHookOutput | null | undefined>;\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nPostToolUseHandler = Callable[\n    [PostToolUseHookInput, dict[str, str]],\n    Awaitable[PostToolUseHookOutput | None]\n]\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\ntype PostToolUseHandler func(\n    input PostToolUseHookInput,\n    invocation HookInvocation,\n) (*PostToolUseHookOutput, error)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\npublic delegate Task<PostToolUseHookOutput?> PostToolUseHandler(\n    PostToolUseHookInput input,\n    HookInvocation invocation);\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n```java\n@FunctionalInterface\npublic interface PostToolUseHandler {\n    CompletableFuture<PostToolUseHookOutput> handle(\n        PostToolUseHookInput input,\n        HookInvocation invocation);\n}\n```\n\n</div>\n\n</div>\n\n## Input\n\n| Field              | Type               | Description                            |\n| ------------------ | ------------------ | -------------------------------------- |\n| `timestamp`        | SDK timestamp type | When the hook was triggered            |\n| `workingDirectory` | string             | Current working directory              |\n| `toolName`         | string             | Name of the tool that was called       |\n| `toolArgs`         | object             | Arguments that were passed to the tool |\n| `toolResult`       | object             | Result returned by the tool            |\n\n## Output\n\nReturn `null` or `undefined` to pass through the result unchanged. Otherwise, return an object with any of these fields:\n\n| Field               | Type    | Description                                  |\n| ------------------- | ------- | -------------------------------------------- |\n| `modifiedResult`    | object  | Modified result to use instead of original   |\n| `additionalContext` | string  | Extra context injected into the conversation |\n| `suppressOutput`    | boolean | If true, result won't appear in conversation |\n\n## Examples\n\n### Log all tool results\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input, invocation) => {\n      console.log(`[${invocation.sessionId}] Tool: ${input.toolName}`);\n      console.log(`  Args: ${JSON.stringify(input.toolArgs)}`);\n      console.log(`  Result: ${JSON.stringify(input.toolResult)}`);\n      return null; // Pass through unchanged\n    },\n  },\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot.session import PermissionHandler\n\nasync def on_post_tool_use(input_data, invocation):\n    print(f\"[{invocation['session_id']}] Tool: {input_data['toolName']}\")\n    print(f\"  Args: {input_data['toolArgs']}\")\n    print(f\"  Result: {input_data['toolResult']}\")\n    return None  # Pass through unchanged\n\nsession = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={\"on_post_tool_use\": on_post_tool_use})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\nsession, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{\n    Hooks: &copilot.SessionHooks{\n        OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {\n            fmt.Printf(\"[%s] Tool: %s\\n\", inv.SessionID, input.ToolName)\n            fmt.Printf(\"  Args: %v\\n\", input.ToolArgs)\n            fmt.Printf(\"  Result: %v\\n\", input.ToolResult)\n            return nil, nil\n        },\n    },\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    Hooks = new SessionHooks\n    {\n        OnPostToolUse = (input, invocation) =>\n        {\n            Console.WriteLine($\"[{invocation.SessionId}] Tool: {input.ToolName}\");\n            Console.WriteLine($\"  Args: {input.ToolArgs}\");\n            Console.WriteLine($\"  Result: {input.ToolResult}\");\n            return Task.FromResult<PostToolUseHookOutput?>(null);\n        },\n    },\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n<!-- docs-validate: skip -->\n\n```java\nimport com.github.copilot.*;\nimport com.github.copilot.rpc.*;\nimport java.util.concurrent.CompletableFuture;\n\nvar hooks = new SessionHooks()\n    .setOnPostToolUse((input, invocation) -> {\n        System.out.println(\"[\" + invocation.getSessionId() + \"] Tool: \" + input.getToolName());\n        System.out.println(\"  Args: \" + input.getToolArgs());\n        System.out.println(\"  Result: \" + input.getToolResult());\n        return CompletableFuture.completedFuture(null);\n    });\n\nvar session = client.createSession(\n    new SessionConfig()\n        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n        .setHooks(hooks)\n).get();\n```\n\n</div>\n\n</div>\n\n### Redact sensitive data\n\n```typescript\nconst SENSITIVE_PATTERNS = [\n  /api[_-]?key[\"\\s:=]+[\"']?[\\w-]+[\"']?/gi,\n  /password[\"\\s:=]+[\"']?[\\w-]+[\"']?/gi,\n  /secret[\"\\s:=]+[\"']?[\\w-]+[\"']?/gi,\n];\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      if (typeof input.toolResult === \"string\") {\n        let redacted = input.toolResult;\n        for (const pattern of SENSITIVE_PATTERNS) {\n          redacted = redacted.replace(pattern, \"[REDACTED]\");\n        }\n\n        if (redacted !== input.toolResult) {\n          return { modifiedResult: redacted };\n        }\n      }\n      return null;\n    },\n  },\n});\n```\n\n### Truncate large results\n\n```typescript\nconst MAX_RESULT_LENGTH = 10000;\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      const resultStr = JSON.stringify(input.toolResult);\n\n      if (resultStr.length > MAX_RESULT_LENGTH) {\n        return {\n          modifiedResult: {\n            truncated: true,\n            originalLength: resultStr.length,\n            content: resultStr.substring(0, MAX_RESULT_LENGTH) + \"...\",\n          },\n          additionalContext: `Note: Result was truncated from ${resultStr.length} to ${MAX_RESULT_LENGTH} characters.`,\n        };\n      }\n      return null;\n    },\n  },\n});\n```\n\n### Add context based on results\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      // If a file read returned an error, add helpful context\n      if (input.toolName === \"read_file\" && input.toolResult?.error) {\n        return {\n          additionalContext:\n            \"Tip: If the file doesn't exist, consider creating it or checking the path.\",\n        };\n      }\n\n      // If shell command failed, add debugging hint\n      if (input.toolName === \"shell\" && input.toolResult?.exitCode !== 0) {\n        return {\n          additionalContext:\n            \"The command failed. Check if required dependencies are installed.\",\n        };\n      }\n\n      return null;\n    },\n  },\n});\n```\n\n### Filter error stack traces\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      if (input.toolResult?.error && input.toolResult?.stack) {\n        // Remove internal stack trace details\n        return {\n          modifiedResult: {\n            error: input.toolResult.error,\n            // Keep only first 3 lines of stack\n            stack: input.toolResult.stack.split(\"\\n\").slice(0, 3).join(\"\\n\"),\n          },\n        };\n      }\n      return null;\n    },\n  },\n});\n```\n\n### Audit trail for compliance\n\n```typescript\ninterface AuditEntry {\n  timestamp: Date;\n  sessionId: string;\n  toolName: string;\n  args: unknown;\n  result: unknown;\n  success: boolean;\n}\n\nconst auditLog: AuditEntry[] = [];\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        toolName: input.toolName,\n        args: input.toolArgs,\n        result: input.toolResult,\n        success: !input.toolResult?.error,\n      });\n\n      // Optionally persist to database/file\n      await saveAuditLog(auditLog);\n\n      return null;\n    },\n  },\n});\n```\n\n### Suppress noisy results\n\n```typescript\nconst NOISY_TOOLS = [\"list_directory\", \"search_codebase\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      if (NOISY_TOOLS.includes(input.toolName)) {\n        // Summarize instead of showing full result\n        const items = Array.isArray(input.toolResult)\n          ? input.toolResult\n          : input.toolResult?.items || [];\n\n        return {\n          modifiedResult: {\n            summary: `Found ${items.length} items`,\n            firstFew: items.slice(0, 5),\n          },\n        };\n      }\n      return null;\n    },\n  },\n});\n```\n\n## Best practices\n\n1. **Return `null` when no changes needed** - This is more efficient than returning an empty object or the same result.\n\n2. **Be careful with result modification** - Changing results can affect how the model interprets tool output. Only modify when necessary.\n\n3. **Use `additionalContext` for hints** - Instead of modifying results, add context to help the model interpret them.\n\n4. **Consider privacy when logging** - Tool results may contain sensitive data. Apply redaction before logging.\n\n5. **Keep hooks fast** - Post-tool hooks run synchronously. Heavy processing should be done asynchronously or batched.\n\n## See also\n\n* [Use hooks](/en/copilot/how-tos/copilot-sdk/hooks)\n* [Pre-tool use hook](/en/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)\n* [Error handling hook](/en/copilot/how-tos/copilot-sdk/hooks/error-handling)"}