@runlayer/hooks-sdk
v0.2.0
Published
Runlayer Hooks TypeScript SDK
Readme
Runlayer Hooks TypeScript SDK
First-party Hooks TypeScript SDK for wiring agent runtimes into Runlayer governance, telemetry, and hook enforcement.
Phase 1 supports Claude Agent SDK hooks plus structural adapters for common TypeScript agent tool APIs:
- lifecycle telemetry for session, prompt, and stop events
- pre-tool enforcement with argument rewriting
- post-tool output scanning and blocking
- failed tool telemetry
- direct MCP source enforcement
- preflight session emission for ingestion checks
- transcript-bearing
Stopemission for completed assistant messages - Vercel AI SDK tool wrappers
- OpenAI Agents SDK function tool wrappers
- Google ADK
FunctionTooloption wrappers
Copy Prompt
Read the Runlayer SDK docs in https://docs.runlayer.com/runlayer-hooks-typescript-sdk and update my agent code to integrate with Runlayer's hook enforcement and telemetry pipelineInstall
pnpm add @runlayer/hooks-sdknpm install @runlayer/hooks-sdkInstall the 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 |
| Vercel OpenAI provider examples | @ai-sdk/openai | 3.0.69 |
| Google ADK tools | @google/adk | 1.2.0 |
| LangChain MCP examples | langchain | 1.4.4 |
| LangChain OpenAI examples | @langchain/openai | 1.4.7 |
| LangChain core types | @langchain/core | 1.1.48 |
Configure
export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
export RUNLAYER_API_KEY="rl_..."For cloud installations that authenticate with a shared organization API key
(rl_org_...), also name the Runlayer user the deployment runs as so sessions
and enforcement are attributed to it:
export RUNLAYER_API_KEY="rl_org_..."
export RUNLAYER_USER_EMAIL="[email protected]"The identity can be a service user provisioned just for the deployment; events
sent before that user exists in the workspace are buffered and replayed once it
does. The organization key must carry the AI Watch scan role. userEmail is
also available as a RunlayerClient constructor option; it applies only to API
key auth (agent accounts express the acting user via subjectToken).
For agent account authentication, set client credentials instead:
export RUNLAYER_BASE_URL="https://your-runlayer-instance.com"
export RUNLAYER_AGENT_CLIENT_ID="client_..."
export RUNLAYER_AGENT_CLIENT_SECRET="..."The SDK exchanges these credentials for a Runlayer bearer token with
client_credentials, then caches and refreshes that token internally. Do not
configure or pass a pre-minted bearer token.
For OBO agent tokens, also set:
export RUNLAYER_AGENT_SUBJECT_TOKEN="[email protected]"
export RUNLAYER_AGENT_SUBJECT_TOKEN_TYPE="urn:runlayer:token-type:user-email"RunlayerClient.fromEnv() defaults to client: "typescript-sdk" so phase 1
events are attributed to the first-party Hooks TypeScript SDK in Runlayer.
Enable the TypeScript SDK session monitoring client in Runlayer workspace
settings before testing: Settings → General → TypeScript SDK.
Optional runtime controls:
| Variable | Purpose |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| RUNLAYER_HOOK_TIMEOUT_MS | Hook request timeout. Defaults to 10000. |
| RUNLAYER_HOOK_MAX_TOOL_OUTPUT_BYTES | Maximum serialized tool output sent to Runlayer. Defaults to 65536. |
| RUNLAYER_HOOK_ENFORCEMENT_FAILURE_MODE | Defaults to closed; set to open only if tool calls should continue when Runlayer is unreachable. |
| RUNLAYER_ALLOW_INSECURE_TRANSPORT=1 | Allow non-HTTPS RUNLAYER_BASE_URL for local development. |
Enforcement calls fail closed by default. Lifecycle telemetry is best-effort unless a helper documents strict behavior. Tool output is capped before upload, and failed tool details include bounded stdout/stderr/output fields with truncation metadata.
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
toolUrl points at a Runlayer proxy URL. Third-party MCP tools stay enforced by
default. Use shouldEnforceTool(...) or toolEnforcement when building a
custom TypeScript adapter. Additional ignoredMcpServerNames are added to the
default Runlayer self-MCP skip list; set skipRunlayerSelfMcp: false only if you
need to enforce those self-MCP names. If you call shouldEnforceTool(...)
directly with absolute self-hosted Runlayer MCP proxy URLs, pass
runlayerBaseUrl; RunlayerClient does this automatically.
Claude Agent SDK Hooks
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",
options: {
hooks: createClaudeAgentSdkHooks(runlayer, { includeStop: false }),
thinking: { type: "adaptive" },
},
})) {
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-agent-sdk",
sessionId,
transcriptLines,
});
}Use includeStop: false when you emit a transcript-bearing Stop manually.
That lets Runlayer extract assistant thinking/reasoning blocks from the
transcript instead of receiving a bare lifecycle stop.
Preflight
Save this as runlayer-preflight.mjs:
import { RunlayerClient, sendRunlayerPreflight } from "@runlayer/hooks-sdk";
const result = await sendRunlayerPreflight(RunlayerClient.fromEnv(), {
prompt: "Runlayer ingestion preflight",
});
console.log(result.sessionUrl);Run preflight before the first real model call:
node runlayer-preflight.mjsThe result includes sessionUrl, runlayerSessionId, sessionId, and
externalSessionId. Use sessionUrl or runlayerSessionId to find the session
in Runlayer. If the workspace has not enabled SDK session monitoring, preflight
throws RunlayerPreflightError with
SDK session monitoring is not enabled for this workspace.
Troubleshooting: sessions not appearing
Lifecycle hooks send events best-effort and do not throw on their own. If the
server accepts an event but does not record it, it responds with
status: "ignored" and a reason. The client logs a one-time console.warn
for the actionable reasons so a misconfiguration is not silent:
client_not_enabled— the SDK session-monitoring client is off for this workspace. Enable the client (and any required SDK toggle) in Runlayer settings. Note this is a separate switch from tool enforcement, so both must be on to get full session + tool telemetry.actor_unresolved— the request authenticated but Runlayer could not map it to a user/agent. With an organization API key, setRUNLAYER_USER_EMAIL(or theuserEmailclient option) to the Runlayer user the deployment runs as; events are buffered until that user exists in the workspace. Otherwise use a personal API key (RUNLAYER_API_KEY) or agent-account credentials (RUNLAYER_AGENT_CLIENT_ID/RUNLAYER_AGENT_CLIENT_SECRET).
Set RUNLAYER_HOOK_DEBUG=1 to also log transient ignore reasons and network
failures. Run sendRunlayerPreflight() for an explicit pass/fail check.
Tool Wrapping
Use runTool when you are invoking tools yourself rather than through a Claude
Agent SDK hook.
const output = await runlayer.runTool({
execute: async (toolInput) => {
return runLocalTool(toolInput);
},
sessionId: "session-id",
toolInput: { command: "cat README.md" },
toolName: "Bash",
toolType: "shell",
});Pass toolUrl when wrapping MCP tools manually. Runlayer proxy URLs such as
/api/v1/proxy/<id>/mcp, /api/v1/proxy/plugins/<id>/mcp,
/api/v1/proxy/skills/<id>/mcp, and /api/v1/proxy/agent-account/<id>/mcp are
skipped automatically because the proxy already runs MCP policy and scanner
enforcement.
Vercel AI SDK Tools
Wrap a Vercel AI SDK tool set before passing it to generateText,
streamText, or a ToolLoopAgent.
import { streamText, tool } from "ai";
import { withRunlayerVercelAiTools, RunlayerClient } 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,
});The adapter preserves the Vercel execution options and forwards toolCallId to
Runlayer as toolUseId. It supports concrete or promise-like tool outputs;
AsyncIterable streaming tool outputs are not wrapped by this adapter.
OpenAI Agents SDK Tools
Wrap function tool options before passing them to OpenAI Agents SDK's tool(...)
helper.
import { tool } from "@openai/agents";
import {
withRunlayerOpenAIAgentsTool,
RunlayerClient,
} 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",
},
),
);The adapter preserves context and details arguments and reads common
toolCall IDs from details.
Google ADK Tools
Wrap Google ADK FunctionTool options before constructing the tool.
import { FunctionTool } from "@google/adk";
import { withRunlayerGoogleAdkTool, RunlayerClient } 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",
},
),
);Direct MCP Source Enforcement
When the deployment exposes direct MCP source validation, configure the endpoint
path and call enforceMcpSource before executing a direct MCP tool.
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",
});