customize-sdk-hooks

📁 speakeasy-api/agent-skills 📅 Jan 29, 2026
4
总安装量
2
周安装量
#49740
全站排名
安装命令
npx skills add https://github.com/speakeasy-api/agent-skills --skill customize-sdk-hooks

Agent 安装分布

github-copilot 2
amp 1
opencode 1
kimi-cli 1
codex 1

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.ts to 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:

  1. Create your hook implementation file in src/hooks/
  2. Implement the appropriate interface from src/hooks/types.ts
  3. Register it in src/hooks/registration.ts
  4. 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

  1. Keep hooks focused: Each hook should address a single concern. Use separate hooks for user-agent, telemetry, and auth rather than one monolithic hook.

  2. Clone responses before reading the body: The Response.body stream 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
    }
    
  3. 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;
    }
    
  4. Test hooks independently: Write unit tests for hooks in isolation by constructing mock Request/Response objects and hook contexts.

  5. 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 AfterSuccessHook unless 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., registerBeforeRequestHook vs registerAfterSuccessHook)
  • Check that initHooks is exported and follows the expected signature

Response body is empty or already consumed

  • You are reading response.body or calling response.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.ts is never overwritten after initial generation
  • types.ts IS overwritten — never put custom code there

TypeScript compilation errors after regeneration

  • types.ts may 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 Request or Response object
  • 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.go with registration in hooks/registration.go
  • Python: Hooks are classes in src/hooks/ implementing protocols from src/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.