customize-sdk-hooks
npx skills add https://github.com/speakeasy-api/agent-skills --skill customize-sdk-hooks
Agent 安装分布
Skill 文档
Customize SDK Hooks
When to Use
Use this skill when you need to add custom code logic to the generated SDK:
- Add custom headers (User-Agent, correlation IDs) to every SDK request via code
- Implement telemetry, logging, or observability at the SDK level
- Add custom authentication logic (HMAC signatures, token refresh) that runs in SDK code
- Transform responses or errors before they reach the caller
- Implement custom request/response middleware
- User says: “SDK hooks”, “add custom logic”, “intercept requests with code”, “HMAC signing hook”, “telemetry in SDK”
NOT for:
- OpenAPI spec modifications (see
manage-openapi-overlays) - Runtime SDK client config (see
configure-sdk-options)
Inputs
- Hook type: Which lifecycle event to intercept (init, before request, after success, after error)
- SDK language: The target language of the generated SDK (TypeScript, Go, Python, Java, C#, Ruby, PHP, etc.)
- Custom logic: The behavior to inject at the hook point
Outputs
- Hook implementation file(s) in
src/hooks/(or language equivalent) - Updated
src/hooks/registration.tsto register the new hook - The hook is preserved across SDK regenerations
Prerequisites
- A Speakeasy-generated SDK (any supported language)
- Understanding of the SDK’s request/response lifecycle
- The
src/hooks/directory exists in the generated SDK (created by default)
Hook Types
| Hook | When Called | Common Use Cases |
|---|---|---|
SDKInitHook |
SDK client initialization | Configure defaults, validate config, set base URL |
BeforeCreateRequestHook |
Before the HTTP request object is created | Modify input parameters, inject defaults |
BeforeRequestHook |
Before the HTTP request is sent | Add headers, logging, telemetry, sign requests |
AfterSuccessHook |
After a successful HTTP response | Transform response, emit warnings, log metrics |
AfterErrorHook |
After an HTTP error response | Error transformation, retry logic, error logging |
Hook Interfaces (TypeScript)
// SDKInitHook
interface SDKInitHook {
sdkInit(opts: SDKInitOptions): SDKInitOptions;
}
// BeforeCreateRequestHook
interface BeforeCreateRequestHook {
beforeCreateRequest(hookCtx: BeforeCreateRequestHookContext, input: any): any;
}
// BeforeRequestHook
interface BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request;
}
// AfterSuccessHook
interface AfterSuccessHook {
afterSuccess(
hookCtx: AfterSuccessHookContext,
response: Response
): Response;
}
// AfterErrorHook
interface AfterErrorHook {
afterError(
hookCtx: AfterErrorHookContext,
response: Response | null,
error: unknown
): { response: Response | null; error: unknown };
}
Directory Structure
src/
hooks/
types.ts # Generated - DO NOT EDIT (hook interfaces/types)
registration.ts # Custom - YOUR registrations (preserved on regen)
custom_useragent.ts # Custom - your hook implementations
telemetry.ts # Custom - your hook implementations
Key rule: registration.ts and any custom hook files you create are preserved
during SDK regeneration. The types.ts file is regenerated and should not be modified.
Registration Pattern
All hooks are registered in src/hooks/registration.ts. This file is created once
by the generator and never overwritten. You add your hooks here:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
import { TelemetryHook } from "./telemetry.js";
/*
* This file is only ever generated once on the first generation and then is free
* to be modified. Any hooks you wish to add should be registered in the
* initHooks function. Feel free to define them in this file or in separate files
* in the hooks folder.
*/
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new CustomUserAgentHook());
hooks.registerAfterSuccessHook(new TelemetryHook());
}
Command
To add a hook to a Speakeasy-generated SDK:
- Create your hook implementation file in
src/hooks/ - Implement the appropriate interface from
src/hooks/types.ts - Register it in
src/hooks/registration.ts - Regenerate the SDK — your hooks are preserved
# After adding hooks, regenerate safely
speakeasy generate sdk -s openapi.yaml -o . -l typescript
# Your registration.ts and custom hook files are untouched
Examples
Example 1: Custom User-Agent Hook
Add a custom User-Agent header to every outgoing request.
// src/hooks/custom_useragent.ts
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
export class CustomUserAgentHook implements BeforeRequestHook {
private userAgent: string;
constructor(appName: string, appVersion: string) {
this.userAgent = `${appName}/${appVersion}`;
}
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
// Clone the request to add the custom header
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("User-Agent", this.userAgent);
// Optionally append the existing User-Agent
const existing = request.headers.get("User-Agent");
if (existing) {
newRequest.headers.set(
"User-Agent",
`${this.userAgent} ${existing}`
);
}
return newRequest;
}
}
Register it:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(
new CustomUserAgentHook("my-app", "1.0.0")
);
}
Example 2: Custom Security Hook (HMAC Signing)
For APIs requiring HMAC signatures or custom authentication that cannot be
expressed in the OpenAPI spec, combine an overlay with a BeforeRequestHook.
Step 1: Use an overlay to mark the security scheme so Speakeasy generates the hook point:
# overlay.yaml
overlay: 1.0.0
info:
title: Add HMAC security
actions:
- target: "$.components.securitySchemes"
update:
hmac_auth:
type: http
scheme: custom
x-speakeasy-custom-security: true
Step 2: Implement the signing hook:
// src/hooks/hmac_signing.ts
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
import { createHmac } from "crypto";
export class HmacSigningHook implements BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
const timestamp = Date.now().toString();
const secret = hookCtx.securitySource?.apiSecret;
if (!secret) {
throw new Error("API secret is required for HMAC signing");
}
const signature = createHmac("sha256", secret)
.update(`${request.method}:${request.url}:${timestamp}`)
.digest("hex");
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("X-Timestamp", timestamp);
newRequest.headers.set("X-Signature", signature);
return newRequest;
}
}
Register it:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { HmacSigningHook } from "./hmac_signing.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new HmacSigningHook());
}
Best Practices
-
Keep hooks focused: Each hook should address a single concern. Use separate hooks for user-agent, telemetry, and auth rather than one monolithic hook.
-
Clone responses before reading the body: The
Response.bodystream can only be consumed once. Always clone before reading:afterSuccess(hookCtx: AfterSuccessHookContext, response: Response): Response { // CORRECT: clone before reading const cloned = response.clone(); cloned.json().then((data) => console.log("Response:", data)); return response; // return the original, unconsumed } -
Fire-and-forget for telemetry: Do not block the request pipeline for non-critical operations like logging or metrics:
beforeRequest(hookCtx: BeforeRequestHookContext, request: Request): Request { // Fire-and-forget: do not await void fetch("https://telemetry.example.com/events", { method: "POST", body: JSON.stringify({ operation: hookCtx.operationID }), }); return request; } -
Test hooks independently: Write unit tests for hooks in isolation by constructing mock
Request/Responseobjects and hook contexts. -
Use
hookCtx.operationID: The hook context provides the current operation ID, which is useful for per-operation behavior, logging, and metrics.
What NOT to Do
- Do NOT edit
types.ts: This file is regenerated. Your changes will be lost. - Do NOT consume the response body without cloning: This causes downstream failures because the body stream is exhausted.
- Do NOT perform blocking I/O in hooks: Long-running operations (network calls, file I/O) in hooks will degrade SDK performance. Use fire-and-forget patterns for non-critical work.
- Do NOT throw errors in
AfterSuccessHookunless you intend to convert a success into a failure. Throwing in hooks disrupts the normal flow. - Do NOT store mutable shared state in hooks without synchronization. Hooks may be called concurrently in multi-threaded environments.
- Do NOT duplicate logic that belongs in an OpenAPI overlay. If you need to modify the API spec (add security schemes, change parameters), use an overlay instead of a hook.
Troubleshooting
Hook is not being called
- Verify the hook is registered in
src/hooks/registration.ts - Confirm you are registering for the correct hook type (e.g.,
registerBeforeRequestHookvsregisterAfterSuccessHook) - Check that
initHooksis exported and follows the expected signature
Response body is empty or already consumed
- You are reading
response.bodyor callingresponse.json()without cloning first - Always use
response.clone()before consuming the body, then return the original
Hooks lost after regeneration
- Custom hook files in
src/hooks/are preserved, but only if they are separate files registration.tsis never overwritten after initial generationtypes.tsIS overwritten — never put custom code there
TypeScript compilation errors after regeneration
types.tsmay have updated interfaces. Check for breaking changes in hook signatures- Update your hook implementations to match the new interface definitions
Hook causes request failures
- Ensure you are returning a valid
RequestorResponseobject - Check that cloned requests preserve the original body and headers
- Verify any injected headers have valid values (no undefined or null)
Other Languages
While the examples above are in TypeScript, Speakeasy SDK hooks are available across all supported languages:
- Go: Hooks implement interfaces in
hooks/hooks.gowith registration inhooks/registration.go - Python: Hooks are classes in
src/hooks/implementing protocols fromsrc/hooks/types.py - Java: Hooks implement interfaces from
hooks/SDKHooks.java - C#: Hooks implement interfaces from
Hooks/SDKHooks.cs - Ruby: Hooks use Sorbet-typed classes with Faraday middleware patterns
- PHP: Hooks use PSR-7 request/response interfaces with Guzzle middleware
The hook types, lifecycle, and registration pattern are consistent across all
languages. Refer to the generated types file in your SDK for language-specific
interface definitions.