> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Runlayer Hooks SDK

> Hook enforcement and telemetry for TypeScript and Python agent runtimes

The Runlayer Hooks SDK wires custom agent runtimes into Runlayer's hook
enforcement and telemetry pipeline. It streams an agent's full conversation —
prompts, reasoning, tool calls, and responses — into Runlayer
[Sessions](/platform-sessions), and lets Runlayer observe, rewrite, or block the
tool calls your agent executes directly. The same SDK ships for **TypeScript**
and **Python** — use the language switch on any code sample to flip the whole
page.

<Info>
  This SDK does not replace MCP client setup. To connect an agent framework
  (Claude Agent SDK, Vercel AI SDK, OpenAI Agents SDK, Google ADK) to
  Runlayer-hosted MCP servers, see [Agent Frameworks](/agent-frameworks). Use the
  adapters on this page for tools your own process executes locally.
</Info>

## Copy prompt

Paste this into your coding agent to wire Runlayer into an existing agent project:

```text theme={null}
Read the Runlayer Hooks SDK docs at https://docs.runlayer.com/runlayer-hooks-sdk and update my agent code to integrate with Runlayer's hook enforcement and telemetry pipeline.
```

## Capabilities

* lifecycle telemetry for session, prompt, and stop events
* pre-tool enforcement with optional argument rewriting
* post-tool output scanning and blocking
* failed tool telemetry
* direct MCP source enforcement
* preflight session emission for ingestion checks
* transcript-bearing `Stop` events for assistant reasoning extraction
* framework tool wrappers for Claude Agent SDK, Vercel AI SDK, OpenAI Agents
  SDK, and Google ADK

## Getting started

<Steps>
  <Step title="Install the package">
    <Tabs>
      <Tab title="TypeScript">
        <CodeGroup>
          ```bash pnpm theme={null}
          pnpm add @runlayer/hooks-sdk
          ```

          ```bash npm theme={null}
          npm install @runlayer/hooks-sdk
          ```
        </CodeGroup>

        Install the agent framework package you use separately, such as
        `@anthropic-ai/claude-agent-sdk`, `ai`, `@openai/agents`, or `@google/adk`.

        Latest verified TypeScript framework versions as of 2026-06-17:

        | Surface                 | Package                          | Version   |
        | ----------------------- | -------------------------------- | --------- |
        | Claude Agent SDK hooks  | `@anthropic-ai/claude-agent-sdk` | `0.3.172` |
        | OpenAI Agents SDK tools | `@openai/agents`                 | `0.11.6`  |
        | Vercel AI SDK tools     | `ai`                             | `6.0.200` |
        | Google ADK tools        | `@google/adk`                    | `1.2.0`   |
      </Tab>

      <Tab title="Python">
        ```bash theme={null}
        uv add runlayer-hooks-sdk
        ```

        For Claude Agent SDK hooks, also install:

        ```bash theme={null}
        uv add claude-agent-sdk==0.1.80
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Create credentials">
    Create a Runlayer user API key in **Settings → Personal API keys**, then set:

    ```bash theme={null}
    export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
    export RUNLAYER_API_KEY="rl_..."
    ```

    For custom or cloud installations that share one organization API key
    (**Settings → Organization API keys**, with the AI Watch scan role), also
    name the Runlayer user each deployment runs as:

    ```bash theme={null}
    export RUNLAYER_API_KEY="rl_org_..."
    export RUNLAYER_USER_EMAIL="svc-my-agent@your-company.com"
    ```

    Sessions and enforcement are attributed to that user. The identity can be a
    service user provisioned just for the deployment; events sent before the
    user exists in the workspace are buffered and replayed once it does.

    To authenticate as an [agent account](/platform-agent-accounts) instead, set:

    ```bash theme={null}
    export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
    export RUNLAYER_AGENT_CLIENT_ID="client_..."
    export RUNLAYER_AGENT_CLIENT_SECRET="..."
    ```

    The SDK mints an agent token with `client_credentials` and, when an OBO
    subject is configured, exchanges it via RFC 8693 token exchange
    (`urn:ietf:params:oauth:grant-type:token-exchange`); both tokens are cached
    and refreshed internally. Do not configure or pass a pre-minted bearer
    token.

    For <Tooltip tip="On-behalf-of — the agent acts with a specific user's identity">OBO</Tooltip> agent tokens, also set:

    ```bash theme={null}
    export RUNLAYER_AGENT_SUBJECT_TOKEN="user@example.com"
    export RUNLAYER_AGENT_SUBJECT_TOKEN_TYPE="urn:runlayer:token-type:user-email"
    ```
  </Step>

  <Step title="Enable the SDK session monitoring client">
    The SDK attributes events to the first-party Hooks SDK client
    (`typescript-sdk` or `python-sdk`). In Runlayer, open **Settings → General**
    and enable the matching SDK session monitoring client before testing. If the
    client option is missing, ask a Runlayer admin to enable SDK session
    monitoring for the workspace.
  </Step>

  <Step title="Run preflight">
    Verify that Runlayer receives hook events before invoking a real model.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { RunlayerClient, sendRunlayerPreflight } from "@runlayer/hooks-sdk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const result = await sendRunlayerPreflight(runlayer, {
        prompt: "Runlayer ingestion preflight",
      });

      console.log(`Runlayer session: ${result.sessionUrl}`);
      ```

      ```python Python theme={null}
      from runlayer_sdk import RunlayerClient, send_runlayer_preflight

      runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

      result = send_runlayer_preflight(runlayer, prompt="Runlayer ingestion preflight")

      print(result.session_id)
      ```
    </CodeGroup>

    Preflight sends a strict `SessionStart`, `UserPromptSubmit`, and `Stop`
    sequence for a synthetic session. If any request fails, or if session
    monitoring is not enabled for this workspace, the helper throws. Open the
    printed session to confirm it appears in Runlayer.
  </Step>
</Steps>

## Configuration

Optional environment variables (identical across languages):

| Variable                                      | Purpose                                                                                                                                                                         |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RUNLAYER_AGENT_CLIENT_ID`                    | Agent account Client ID for `client_credentials` token exchange.                                                                                                                |
| `RUNLAYER_AGENT_CLIENT_SECRET`                | Agent account Client Secret for `client_credentials` token exchange.                                                                                                            |
| `RUNLAYER_AGENT_SUBJECT_TOKEN`                | Optional user identity for OBO agent token exchange.                                                                                                                            |
| `RUNLAYER_AGENT_SUBJECT_TOKEN_TYPE`           | Token type for `RUNLAYER_AGENT_SUBJECT_TOKEN`; use `urn:runlayer:token-type:user-id`, `urn:runlayer:token-type:user-email`, or `urn:ietf:params:oauth:token-type:access_token`. |
| `RUNLAYER_HOOK_CLIENT`                        | Override the client name sent to Runlayer. Only use this when your deployment supports a dedicated SDK client name.                                                             |
| `RUNLAYER_HOOK_DEBUG=1`                       | Log non-strict hook event failures to stderr.                                                                                                                                   |
| `RUNLAYER_HOOK_TIMEOUT_MS`                    | Request timeout for Runlayer hook calls.                                                                                                                                        |
| `RUNLAYER_HOOK_MAX_TOOL_OUTPUT_BYTES`         | Maximum serialized tool output sent to Runlayer. Defaults to `65536`; larger values are truncated with a marker.                                                                |
| `RUNLAYER_HOOK_ENFORCEMENT_FAILURE_MODE`      | Enforcement outage behavior. Defaults to `closed`; set to `open` only if your agent should continue when Runlayer cannot be reached.                                            |
| `RUNLAYER_ALLOW_INSECURE_TRANSPORT=1`         | Allow a non-HTTPS `RUNLAYER_BASE_URL` for local development only.                                                                                                               |
| `RUNLAYER_DIRECT_MCP_SOURCE_ENFORCEMENT_PATH` | Endpoint path for direct MCP source validation, when enabled by your deployment.                                                                                                |

<Note>
  Default request timeouts differ by SDK. TypeScript uses a single `10000` ms
  default. Python uses `30000` ms for enforcement and tool lifecycle hooks,
  `5000` ms for lifecycle event hooks, and `10000` ms for direct endpoint calls.
  Override either with `RUNLAYER_HOOK_TIMEOUT_MS`.
</Note>

## Runtime safety

Runlayer enforcement is <Tooltip tip="If Runlayer cannot be reached, the call is blocked rather than allowed through">fail-closed</Tooltip> by default. If pre-tool, post-tool, or direct
MCP source enforcement cannot reach Runlayer, the SDK treats the call as blocked.
Lifecycle telemetry is best-effort unless a helper documents strict behavior, such
as preflight and transcript `Stop` emission.

Every hook request uses a timeout, and tool output is capped before upload. Failed
tool outputs include bounded `stdout`, `stderr`, and `output` fields with
truncation metadata when available, so Runlayer can scan failure context without
the SDK uploading unbounded logs.

The SDK skips pre-tool and post-tool enforcement for Runlayer's own MCP server
names (`runlayer`, `runlayer-plugin`, and `onelayer`) and for MCP calls whose tool
URL points at a Runlayer proxy URL. Third-party MCP tools remain enforced by
default. For custom runtimes, use the exported `shouldEnforceTool` /
`should_enforce_tool` helper or the client's tool-enforcement option so new
adapters make the same decision consistently. The base URL must use `https://`
unless insecure transport is explicitly enabled for a trusted local endpoint.

## Framework adapters

Wrap the tools your runtime executes so Runlayer can enforce them. Pick your
harness — the language toggle inside each example still controls the whole page.

<Tabs>
  <Tab title="Claude Agent SDK" icon="robot">
    Pass Runlayer hooks into Claude Agent SDK's `query` options.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { query } from "@anthropic-ai/claude-agent-sdk";
      import {
        claudeAgentSdkAssistantMessageToTranscriptLine,
        createClaudeAgentSdkHooks,
        emitClaudeAgentSdkTranscriptStop,
        RunlayerClient,
      } from "@runlayer/hooks-sdk/claude-agent-sdk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const transcriptLines: string[] = [];
      let sessionId: string | undefined;

      for await (const message of query({
        prompt: "Inspect this workspace and summarize the active project.",
        options: {
          allowedTools: ["Read", "Glob", "Grep", "Bash"],
          hooks: createClaudeAgentSdkHooks(runlayer, { includeStop: false }),
          maxTurns: 4,
          model: "claude-opus-4-6",
        },
      })) {
        if (message.type === "system" && message.subtype === "init") {
          sessionId = message.session_id;
        }
        if (message.type === "assistant") {
          transcriptLines.push(
            claudeAgentSdkAssistantMessageToTranscriptLine(message.message),
          );
        }
      }

      if (sessionId) {
        await emitClaudeAgentSdkTranscriptStop(runlayer, {
          model: "claude-opus-4-6",
          sessionId,
          transcriptLines,
        });
      }
      ```

      ```python Python theme={null}
      import anyio
      from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, SystemMessage, query
      from runlayer_sdk import RunlayerClient
      from runlayer_sdk.claude_agent_sdk import (
          claude_agent_sdk_assistant_message_to_transcript_line,
          create_claude_agent_sdk_hooks,
          emit_claude_agent_sdk_transcript_stop,
      )


      async def main() -> None:
          runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")
          model = "claude-opus-4-6"
          transcript_lines: list[str] = []
          session_id: str | None = None

          options = ClaudeAgentOptions(
              allowed_tools=["Read", "Glob", "Grep", "Bash"],
              hooks=create_claude_agent_sdk_hooks(runlayer, include_stop=False),
              max_turns=4,
              model=model,
          )

          async for message in query(prompt="Inspect this workspace.", options=options):
              if isinstance(message, SystemMessage) and message.subtype == "init":
                  session_id = message.session_id
              if isinstance(message, AssistantMessage):
                  transcript_lines.append(
                      claude_agent_sdk_assistant_message_to_transcript_line(message.message)
                  )

          if session_id:
              emit_claude_agent_sdk_transcript_stop(
                  runlayer, model=model, session_id=session_id, transcript_lines=transcript_lines
              )


      anyio.run(main)
      ```
    </CodeGroup>

    Use `includeStop: false` / `include_stop=False` when you emit a
    transcript-bearing `Stop` manually. That lets Runlayer extract assistant
    `thinking` or `reasoning` blocks from the completed transcript instead of
    receiving a bare lifecycle stop.

    <Note>
      Each `query()` call starts a new Claude Agent SDK session with a fresh
      `session_id`, and Runlayer records each session id as a separate
      session. To keep a multi-prompt conversation in a single Runlayer
      session, resume the previous session on follow-up calls (leave
      `forkSession` / `fork_session` unset so the session id is preserved):
    </Note>

    <CodeGroup>
      ```typescript TypeScript theme={null}
      options: {
        hooks: createClaudeAgentSdkHooks(runlayer, { includeStop: false }),
        resume: sessionId, // session_id captured from the previous run's init message
      }
      ```

      ```python Python theme={null}
      options = ClaudeAgentOptions(
          hooks=create_claude_agent_sdk_hooks(runlayer, include_stop=False),
          resume=session_id,  # session_id captured from the previous run's init message
      )
      ```
    </CodeGroup>

    Emit the transcript `Stop` after each run as usual — repeated `Stop`
    events with the same session id land in the same Runlayer session.
  </Tab>

  <Tab title="Vercel AI SDK" icon="bolt">
    Wrap a Vercel AI SDK tool set before passing it to `generateText`,
    `streamText`, or a `ToolLoopAgent`.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { streamText, tool } from "ai";
      import {
        RunlayerClient,
        withRunlayerVercelAiTools,
      } from "@runlayer/hooks-sdk/vercel-ai-sdk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const tools = withRunlayerVercelAiTools(
        {
          getWeather: tool({
            description: "Get weather for a city",
            inputSchema: {
              type: "object",
              properties: { city: { type: "string" } },
              required: ["city"],
            },
            execute: async ({ city }) => ({ forecast: `sunny in ${city}` }),
          }),
        },
        { client: runlayer, sessionId: "session-id" },
      );

      await streamText({ model, prompt: "Check the weather in Paris", tools });
      ```

      ```python Python theme={null}
      from runlayer_sdk import RunlayerClient, with_runlayer_vercel_ai_tools

      runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

      tools = with_runlayer_vercel_ai_tools(
          {
              "getWeather": {
                  "description": "Get weather for a city",
                  "inputSchema": {
                      "type": "object",
                      "properties": {"city": {"type": "string"}},
                      "required": ["city"],
                  },
                  "execute": lambda tool_input, execution_options=None: {
                      "forecast": f"sunny in {tool_input['city']}",
                  },
              },
          },
          {"client": runlayer, "session_id": "session-id"},
      )
      ```
    </CodeGroup>

    <Note>
      The Python adapters wrap **dict-shaped** tool definitions, not native
      `FunctionTool` or callable objects. For native callables, call
      `RunlayerClient.run_tool(...)` directly inside the callable.
    </Note>
  </Tab>

  <Tab title="OpenAI Agents SDK" icon="brain">
    Wrap function tool options before passing them to OpenAI Agents SDK's
    `tool(...)` helper.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { tool } from "@openai/agents";
      import {
        RunlayerClient,
        withRunlayerOpenAIAgentsTool,
      } from "@runlayer/hooks-sdk/openai-agents-sdk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const lookupTicket = tool(
        withRunlayerOpenAIAgentsTool(
          {
            name: "lookup_ticket",
            description: "Look up a support ticket",
            parameters: {
              type: "object",
              properties: { ticketId: { type: "string" } },
              required: ["ticketId"],
            },
            execute: async ({ ticketId }) => `ticket:${ticketId}`,
          },
          { client: runlayer, sessionId: "session-id" },
        ),
      );
      ```

      ```python Python theme={null}
      from runlayer_sdk import RunlayerClient, with_runlayer_openai_agents_tool

      runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

      lookup_ticket = with_runlayer_openai_agents_tool(
          {
              "name": "lookup_ticket",
              "description": "Lookup a ticket",
              "parameters": {"type": "object"},
              "execute": lambda tool_input, context=None, details=None: {
                  "ticket_id": tool_input["ticket_id"],
              },
          },
          {"client": runlayer, "session_id": "session-id"},
      )
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Google ADK" icon="cubes">
    Wrap Google ADK `FunctionTool` options before constructing the tool.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { FunctionTool } from "@google/adk";
      import {
        RunlayerClient,
        withRunlayerGoogleAdkTool,
      } from "@runlayer/hooks-sdk/google-adk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const searchTickets = new FunctionTool(
        withRunlayerGoogleAdkTool(
          {
            name: "search_tickets",
            description: "Search support tickets",
            parameters: {
              type: "object",
              properties: { query: { type: "string" } },
              required: ["query"],
            },
            execute: async ({ query }) => ({ query }),
          },
          { client: runlayer, sessionId: "session-id" },
        ),
      );
      ```

      ```python Python theme={null}
      from runlayer_sdk import RunlayerClient, with_runlayer_google_adk_tool

      runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

      search_tickets = with_runlayer_google_adk_tool(
          {
              "name": "search_tickets",
              "description": "Search support tickets",
              "parameters": {
                  "type": "object",
                  "properties": {"query": {"type": "string"}},
                  "required": ["query"],
              },
              "execute": lambda tool_input, tool_context=None: {"query": tool_input["query"]},
          },
          {"client": runlayer, "session_id": "session-id"},
      )
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Custom runtime" icon="wrench">
    If your runtime executes tools outside a supported framework, wrap those
    calls with `runTool` / `run_tool`.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      import { RunlayerClient } from "@runlayer/hooks-sdk";

      const runlayer = RunlayerClient.fromEnv({ clientVersion: "my-agent/1.0.0" });

      const output = await runlayer.runTool({
        execute: async (toolInput) => runLocalTool(toolInput),
        sessionId: "session-id",
        toolInput: { command: "cat README.md" },
        toolName: "Bash",
        toolType: "shell",
      });
      ```

      ```python Python theme={null}
      from runlayer_sdk import RunlayerClient

      runlayer = RunlayerClient.from_env(client_version="my-agent/1.0.0")

      output = runlayer.run_tool(
          execute=run_local_tool,
          session_id="session-id",
          tool_input={"command": "cat README.md"},
          tool_name="Bash",
          tool_type="shell",
      )
      ```
    </CodeGroup>

    Runlayer can return modified arguments from the pre-tool hook; the SDK passes
    them to `execute`. If Runlayer denies the call or blocks the output, the SDK
    throws `RunlayerBlockedError`. Python runtimes with other dict-shaped tool
    APIs can use `run_runlayer_adapter_tool(...)` for the same skip rules.
  </Tab>
</Tabs>

## Direct MCP source enforcement

Use direct MCP source enforcement when a custom agent can call MCP servers
without going through the Runlayer proxy.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const runlayer = RunlayerClient.fromEnv({
    directMcpSourceEnforcementPath: "/api/v1/hooks/direct-mcp-source",
  });

  await runlayer.enforceMcpSource({
    generationId: "generation-id",
    sessionId: "session-id",
    toolName: "mcp__github__list_repos",
    url: "https://tenant.runlayer.com/api/v1/proxy/server-id/mcp",
  });
  ```

  ```python Python theme={null}
  runlayer = RunlayerClient.from_env(
      direct_mcp_source_enforcement_path="/api/v1/hooks/cursor",
  )

  runlayer.enforce_mcp_source(
      generation_id="generation-id",
      session_id="session-id",
      tool_name="mcp__github__list_repos",
      url="https://tenant.runlayer.com/api/v1/proxy/server-id/mcp",
  )
  ```
</CodeGroup>

If Runlayer denies the source, the call throws `RunlayerBlockedError`.

<Note>
  The Python direct MCP source route posts to the Cursor hook route and
  currently requires user API-key auth. Configure `RUNLAYER_API_KEY` for this
  client; agent-account bearer auth is supported for lifecycle and tool hooks,
  but not this direct route.
</Note>

## API reference

<Tabs>
  <Tab title="TypeScript">
    ### `RunlayerClient.fromEnv(options?)`

    Creates a client from `RUNLAYER_BASE_URL` plus one auth source:
    `RUNLAYER_API_KEY` or `RUNLAYER_AGENT_CLIENT_ID` / `RUNLAYER_AGENT_CLIENT_SECRET`.
    With API key auth, `RUNLAYER_USER_EMAIL` names the user attributed when the
    key is an organization key.

    <ParamField path="client" type="string">
      Client name sent to Runlayer. Defaults to `RUNLAYER_HOOK_CLIENT` or `typescript-sdk`.
    </ParamField>

    <ParamField path="clientVersion" type="string">
      Version string for your agent runtime; used for audit-log attribution.
    </ParamField>

    <ParamField path="enforcementFailureMode" type="string" default="closed">
      `closed` blocks on enforcement outages; `open` allows on outages.
    </ParamField>

    <ParamField path="timeoutMs" type="number" default="10000">
      Per-request timeout in milliseconds.
    </ParamField>

    <ParamField path="toolEnforcement" type="object">
      Tool filtering shared by generic wrappers and framework adapters.
    </ParamField>

    * `createClaudeAgentSdkHooks(client, options?)` — Claude Agent SDK hook map.
    * `sendRunlayerPreflight(client, options?)` — strict synthetic lifecycle sequence.
    * `emitClaudeAgentSdkTranscriptStop(client, options)` — strict transcript `Stop`.
    * `runTool(options)` — run a custom tool through pre/post enforcement.
    * `withRunlayerVercelAiTool` / `withRunlayerVercelAiTools` / `withRunlayerOpenAIAgentsTool` / `withRunlayerGoogleAdkTool` — adapter helpers.
    * `enforceMcpSource(options)` — validate a direct MCP source.
  </Tab>

  <Tab title="Python">
    ### `RunlayerClient.from_env(**options)`

    Creates a client from `RUNLAYER_BASE_URL` plus one auth source:
    `RUNLAYER_API_KEY` or `RUNLAYER_AGENT_CLIENT_ID` / `RUNLAYER_AGENT_CLIENT_SECRET`.
    With API key auth, `RUNLAYER_USER_EMAIL` names the user attributed when the
    key is an organization key.

    <ParamField path="client" type="string">
      Client name sent to Runlayer. Defaults to `RUNLAYER_HOOK_CLIENT` or `python-sdk`.
    </ParamField>

    <ParamField path="client_version" type="string">
      Version string for your agent runtime; used for audit-log attribution.
    </ParamField>

    <ParamField path="enforcement_failure_mode" type="string" default="closed">
      `closed` blocks on enforcement outages; `open` allows on outages.
    </ParamField>

    <ParamField path="timeout_ms" type="number">
      Per-request timeout override. Defaults: 30000 ms enforcement/tool, 5000 ms lifecycle, 10000 ms direct.
    </ParamField>

    <ParamField path="tool_enforcement" type="object">
      Filter config for ignored tools, MCP server names, proxy URLs, and custom predicates.
    </ParamField>

    * `create_claude_agent_sdk_hooks(client, options=None, *, include_stop=None)` — Claude Agent SDK hook map.
    * `send_runlayer_preflight(client, **options)` — strict synthetic lifecycle sequence.
    * `emit_claude_agent_sdk_transcript_stop(client, **options)` — strict transcript `Stop`.
    * `RunlayerClient.run_tool(**options)` — run a custom tool through pre/post enforcement.
    * `with_runlayer_vercel_ai_tool` / `with_runlayer_vercel_ai_tools` / `with_runlayer_openai_agents_tool` / `with_runlayer_google_adk_tool` — adapter helpers.
    * `RunlayerClient.enforce_mcp_source(**options)` — validate a direct MCP source.
  </Tab>
</Tabs>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Preflight says SDK session monitoring is not enabled">
    Enable the matching SDK session monitoring client (**TypeScript SDK** or
    **Python SDK**) in **Settings → General**, then rerun preflight. If you
    cannot see the client option, ask a Runlayer admin to enable SDK session
    monitoring for the workspace.
  </Accordion>

  <Accordion title="Preflight prints an ID, but I cannot find the session">
    Use the session URL first. If you only have an ID, use the Runlayer session
    ID in the Sessions URL or search field. The synthetic external session ID
    emitted by your agent is useful for logs but may not be the best lookup key.
  </Accordion>

  <Accordion title="The setup script succeeds, but no tool calls are enforced">
    Confirm you constructed the client with the same credentials used for
    preflight, and wrap local tool execution with a framework adapter or
    `runTool` / `run_tool`. Preflight only proves lifecycle ingestion; it does
    not wrap your tools by itself.
  </Accordion>

  <Accordion title="Events are ignored with reason actor_unresolved">
    The request authenticated, but Runlayer could not map it to a user. With an
    organization API key, set `RUNLAYER_USER_EMAIL` (or the `userEmail` /
    `user_email` client option) to the Runlayer user the deployment runs as.
    If that user does not exist in the workspace yet, session events are
    buffered and replayed once it is created; tool enforcement stays inactive
    until then. Otherwise use a personal API key or agent-account credentials.
  </Accordion>
</AccordionGroup>
