arcjet/arcjet-js/integrate-arcjet-guard-tanstack-ai
Claude Code and agent skill by arcjet/arcjet-js
- community
Checking that the SKILL.md file is still in the repository.
Integrate Arcjet security into a TanStack AI chat() app using @arcjet/guard — put guardMiddleware first on chat({ middleware }) so onBeforeToolCall gates tools, and read a caller-owned id from chat({ context }). Use when asked to add Arcjet to TanStack AI, rate limit its tools, screen inbound messages, or block prompt injection / PII. This is TanStack AI, not the Vercel AI SDK.
Install
npx vskill@latest install arcjet/arcjet-js/integrate-arcjet-guard-tanstack-aiSKILL.md
Integrate Arcjet Guard into TanStack AI
@arcjet/guard's TanStack AI v0 namespace wraps the agent's existing Arcjet client. It never talks to the Arcjet API itself. Two surfaces, one decision rule:
- Tool calls →
guardMiddleware(). Achat({ middleware })
middleware whose onBeforeToolCall is the chat()-wide gate. Default DENY is { type: "skip", result: ArcjetDenialResult } so the tool never runs and the model sees the payload. Optional onDeny: "abort" returns { type: "abort", reason } and stops the run — the model does not get ArcjetDenialResult. Do not throw from the hook. Tools already branded by a sibling guardTool are skipped so Guard is not double-called. Inbound guard() before chat() does not brand tools and does not skip this gate.
- Correlation →
tanstackAiContext()reads a caller-owned id from
the helper options or chat({ context }). It never mints a new id. It never reads ctx.threadId (TanStack auto-generates it). It never reads traceId / requestId / streamId.
There is no `guardTool`. A throw from execute is swallowed into { error } and is not a usable deny envelope. There is no guardInbound, no guardApproval, and nothing named contentGuardMiddleware (TanStack already has that name).
This namespace is TanStack AI `chat({ middleware })` + `onBeforeToolCall`. Not the Vercel AI SDK. Do not also wrap with @arcjet/guard/vercel-ai/v7 or @arcjet/guard/claude-managed-agents/v0. Client tools and provider-native tools with no local execute are out of scope.
Docs live at docs.arcjet.com/guards/tanstack-ai/. Do not overwrite any other /guards/... slug.
Put Arcjet first in the middleware array. onBeforeToolCall is first-win; if toolCacheMiddleware (or anything else) skips first, Guard never runs.
Screen inbound before chat() — there is no inbound hook.
There is no first-class inbound channel, so there is no guardInbound. Put prompt-injection (and other inbound rules) in the application before chat(). Call guard() directly. guard() fails open — callers must check hasFailedOpen(). TanStack's contentGuardMiddleware redacts the stream; it is not this policy gate.
needsApproval / defineInterrupt / onInterruptBoundary is HITL, not a policy gate.
needsApproval / defineInterrupt / onInterruptBoundary is human-in-the-loop. After a human yes, Guard still runs on the tool call. Same trap as Mastra requireApproval, Claude canUseTool, LangGraph interrupt(), Genkit toolApproval, OpenAI Agents needsApproval, LangChain humanInTheLoopMiddleware, Google ADK requireConfirmation, and Cloudflare Think needsApproval. There is no guardApproval.
Questions to ask the human first
Ask only what you cannot infer from the code; suggest defaults.
- Which tools are risky (external side effects, irreversible, spends
money, sends messages)? Those are gated by guardMiddleware.
- What limits? (e.g. "10 lookups/min per order" →
tokenBucket.) - Who is the user for metadata — an opaque user/tenant ID (never PII)?
Default: none. Pass it via metadata on the policy. Put the conversation / session id you already have on chat({ context: { sessionId } }). That id is the correlation id. Do not use ctx.threadId.
- Is an Arcjet outage unacceptable? Every helper defaults to
onGuardError: "deny". Ask explicitly about inbound screening before chat(): failing closed there means the chat does not run for the duration of the outage, so "allow" is a routine and legitimate choice at that one call site. guard() itself still fails open — check hasFailedOpen().
The six things readers get wrong
- There is no `guardInbound`. Screen prompt injection before
chat() with guard(). Check hasFailedOpen(). contentGuardMiddleware is not Guard.
- **
needsApproval/defineInterrupt/onInterruptBoundaryis not
a policy gate.** It is HITL. After a human yes, Guard still runs.
- The import path is versioned and there is no alias.
@arcjet/guard/tanstack-ai/v0. @arcjet/guard/tanstack-ai does not resolve. There is no /v1 until TanStack AI ships 1.x. Docs are /guards/tanstack-ai/.
- Correlation is read, never minted. Do not call
createAgentContext inside a middleware callback — that generates a second id and splits the Sequence. Put the id you already chose on chat({ context }). Do not read ctx.threadId, traceId, requestId, or streamId.
- Put Arcjet first.
onBeforeToolCallis first-win. If
toolCacheMiddleware skips first, Guard never runs.
- **Do not add
guardTooland do not double-wrap with
@arcjet/guard/vercel-ai/v7.** TanStack AI is not the Vercel AI SDK. A throw from execute becomes { error }.
Step 1: Install and find the guard client
Install @arcjet/guard (required), plus @tanstack/ai (optional peer, needed for @arcjet/guard/tanstack-ai/v0). Always use the versioned path: @arcjet/guard/tanstack-ai/v0 resolves; @arcjet/guard/tanstack-ai throws ERR_PACKAGE_PATH_NOT_EXPORTED. The peer range is >=0.8.0 <1. Node 22+ — do not bump Node for this adapter.
If the agent has no guard client yet, launch one once at module scope:
Step 2: Gate tool calls — Arcjet first
- Omit
rulesto submit none. The guard call still happens. - Optional
actorandinputs(static, or a resolver over this adapter's native call — parsed input plus trusted runtime/context) are forwarded on the
guard call so a remote policy that declares those names can evaluate. Build each input with policyInput. A resolver that throws or returns a value Guard cannot use is left out of the guard call, and onGuardError decides.
- On DENY the original
executenever runs. Default delivery is
{ type: "skip", result: { arcjetDenied: true, reason, message, retryable } }.
onDeny: "abort"stops the chat run with{ type: "abort", reason }
(the denial message string). The model does not get ArcjetDenialResult. Prefer default skip when the model should see the payload. onDeny: "abort" applies to real DENY only; unavailable stays skip.
- Default
onGuardError: "deny"blocks the tool if Arcjet is unreachable,
or if an action, rules, metadata or sessionId callback throws or returns a value Guard cannot use. A failed callback never skips the guard call: Guard is called without its value (a failed action falls back to "tool.invoked"), so remote policy still evaluates the call and a DENY stands.
- ALLOW captures
outcome: "success"when the policy lets the tool
run, not when execute finishes. onBeforeToolCall cannot wrap the tool; a later tool throw does not flip that capture.
- Tools already branded by a sibling
guardToolskip the middleware
so Guard is not double-called. This namespace has no guardTool. Inbound guard() before chat() does not stamp that brand.
Step 3: Screen inbound before chat
There is no guardInbound. guard() fails open — always check hasFailedOpen().
Step 4: Correlation
Put the id you already have on chat({ context }):
Preference order: context.correlationId, then context.sessionId, then context.conversationId, then init.sessionId / init.correlationId. If none is a valid 1–256 printable-ASCII string, the call is uncorrelated rather than joined to a generated id nobody has.
Pass a caller-owned bag as tanstackAiContext({ context: appContext }) or chat({ context: appContext }). A bare object that also has string requestId and streamId looks like TanStack's middleware envelope, so top-level sessionId on that object is ignored.
Never mint a new id. Never read ctx.threadId (TanStack auto-generates it). Never read traceId / requestId / streamId. needsApproval resumes after a human yes — Guard still runs on the tool call. Do not treat the interrupt or its resume value as correlation.
Verify the integration
npm run typecheckpasses.- Exercise inbound PI (before
chat(), includinghasFailedOpen()),
a middleware skip-deny, an abort-deny, abort+unavailable still skip, first-win ordering (Arcjet before toolCacheMiddleware), no-throw, never-mint, and fail-closed (an unreachable guard). Confirm the denial is { type: "skip", result } (or abort) and the run is not an interrupt.
- Confirm in the Arcjet dashboard that decisions share the
caller-owned session id as their correlation id — not threadId.
- Manual E2E with a real
ARCJET_KEYis still-to-verify until you run it.
A full working demo will land in `arcjet/examples` `tanstack-agent` as a later follow-up. Do not add an example under examples/ in the JS SDK repo.
Note: capture events are fire-and-forget and batched, so events can lag the decisions they accompany by a few seconds. A dropped event is diagnosed, never thrown.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingarcjet-guard/skills/integrate-arcjet-guard-tanstack-ai/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/arcjet/arcjet-js/integrate-arcjet-guard-tanstack-ai)