integrate-arcjet-guard-cloudflare-think
Skill4 starsChanged 6 months agoApache-2.0View on GitHub
Integrate Arcjet Guard into Cloudflare Think — delegate beforeToolCall to guardHooks so a DENY substitutes or blocks the tool, and read a caller-owned id via cloudflareThinkContext. Use when asked to add Arcjet to @cloudflare/think, Cloudflare Agents Think, rate limit those tools, screen inbound messages, or block prompt injection / PII. This is Cloudflare Think, not the Vercel AI SDK and not needsApproval HITL.
- Reads credentials
- Installs packages
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- skill-creatorSkill
- arcjetSkill
- integrate-arcjet-guard-agent-framework-goSkill
- integrate-arcjet-guard-claude-agent-sdk-pySkill
- integrate-arcjet-guard-claude-managed-agents-pySkill
- integrate-arcjet-guard-crewaiSkill
- integrate-arcjet-guard-google-adk-pySkill
- integrate-arcjet-guard-langchain-pySkill
- integrate-arcjet-guard-openai-agents-pySkill
- integrate-arcjet-guard-strands-agents-pySkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 4 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 51
Do not invent a second slug and do not overwrite any other /guards/...
- Distinctive: line 99
Check installed types — do not pass fields a helper does not declare.
- Distinctive: line 119
Ask only what you cannot infer from the code.
- Very specific: line 140
This is not the Vercel AI SDK: Do not import @arcjet/guard/vercel-ai/v7 and do not wrap Think tools with guardTool from that namespace.
- Very specific: line 157
Correlation is read, never minted: Never Durable Object id, never toolCallId, never requestId, never traceId.
- Distinctive: line 172
Use the package manager the project already uses — do not pin a git SHA:
- Very specific: line 236
If the subclass already implements beforeToolCall, call hooks.beforeToolCall(ctx) and return its ToolCallDecision.
- Distinctive: line 283
Do not invent a correlation id per turn.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Integrate Arcjet Guard into Cloudflare Think
@arcjet/guard/cloudflare-think/v0 wraps the agent's existing Arcjet
client. It never talks to the Arcjet API itself. Shared Guard
fundamentals (client, rules, labels, decisions, capture, registration)
live in
../arcjet/references/guards_javascript.md.
Load that reference for anything that is not Think-specific.
Official @cloudflare/think >=0.3.0 <1 only — that is the first
release whose beforeToolCall returns a functional ToolCallDecision
(allow / block / substitute) and wraps every server-side tool's
execute so the hook runs first. There is no /v1 until Think ships
1.x. This is not the Vercel AI SDK (ai /
@arcjet/guard/vercel-ai/v7). Think owns streamText internally; do
not also wrap its tools with the AI SDK mix-in. Not TanStack Start HTTP
protect(). Not Cloudflare Workers HTTP protect() (@arcjet/node /
request-based).
Exports: guardHooks, cloudflareThinkContext. There is no
guardTool and no guardThink mixin. Skip is the
beforeToolCall return, not throw-from-execute. There is no
guardInbound and no guardApproval. No unversioned
@arcjet/guard/cloudflare-think alias.
Two surfaces, one decision rule:
- Tool calls →
guardHooks. Returns a{ beforeToolCall }object theThinksubclass delegates to. Delivery is a ThinkToolCallDecision: substitute ({ action: "substitute", output: ArcjetDenialResult }) so the model sees the payload, or block ({ action: "block", reason }) so the model sees a reason string.void/{ action: "allow" }executes. Do not throw — a throw is a raw exception /onChatError, not a denial. - Correlation →
cloudflareThinkContextreads a caller-owned id. It never mints. It never reads Durable Object ids,toolCallId,requestId, ortraceId.
Docs: https://docs.arcjet.com/guards/cloudflare-think/
Do not invent a second slug and do not overwrite any other
/guards/... page. Example (the main tree 404s; no example PR
is open, so cite this branch until
examples/cloudflare-think-agent exists on main):
examples/cloudflare-think-agent
(no verify fixture). Do not invent a second example name.
The gate is beforeToolCall block / substitute
Think wraps every server-side tool's execute and consults
beforeToolCall first (subclass hook, then extensions). Returning
block or substitute skips execute. Fail closed: always return
one of those on error — do not return void (that executes the
tool) and do not throw. Default DENY is substitute with
ArcjetDenialResult so the model sees { arcjetDenied: true, … }.
onDeny: "block" uses { action: "block", reason } instead and
drops the structured fields. onDeny: "block" applies to real
DENY only; unavailable still fail-closes with substitute.
Core guard() still fails open (hasFailedOpen()).
Client tools (no server execute) never enter this hook. Workspace
tools that Think auto-merges do, if they have execute.
needsApproval is not a policy gate
AI SDK needsApproval / confirmation-style tools / human resume
are human-in-the-loop. After a human yes, Guard still runs on the
tool call. Same trap as TanStack needsApproval, Mastra
requireApproval, Claude canUseTool, LangGraph interrupt(),
OpenAI Agents needsApproval, Genkit interrupt(), and Google ADK
requireConfirmation. There is no guardApproval.
Screen inbound before chat() / submitMessages()
There is no guardInbound. beforeTurn / beforeStep tune the
model call; they are not this policy gate. Call arcjet.guard() in
the application (or at the start of beforeTurn and act on the
decision) before the turn starts. Core guard() fails open:
ALLOW is not proof the rules ran. Gate on
decision.hasFailedOpen() if this call site must fail closed;
guardHooks already defaults to that.
actor / inputs — omit and remote rules never fire
A remote policy that declares actor or typed inputs only
evaluates those values when this call sends them. Pass actor and
inputs on guardHooks (and on inbound arcjet.guard()). Import
policyInput from @arcjet/guard, not from the adapter path.
Check installed types — do not pass fields a helper does not
declare.
- Omit
actor/inputsand a remote policy that requires them never fires. The guard call still happens; the remote rules just have nothing to read. - A resolver that throws fail-closes under the default
onGuardError: "deny"(unavailable substitute — nevervoid). actoris the authenticated caller, never a model-produced tool argument.inputon the Think envelope is model-controlled. Deriveactorfrom authenticated server-side context (a signed session, verified token, or Durable Object state you already trust) — not fromctx.input.
rules / action / metadata see { toolName, input }. actor
/ inputs resolvers also receive the Think beforeToolCall
context as the second argument.
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
guardHooks. Client tools with noexecuteare out of scope. - What limits? (e.g. "10 lookups/min per user" →
tokenBucket.) - Who is the user for metadata — an opaque user/tenant ID
(never PII)? Default: none. That same authenticated id is
actor. Put the conversation / session id you already have onguardHooks({ sessionId }). That id is the correlation id, not the user. Do not use the Durable Object id ortoolCallId. - Is an Arcjet outage unacceptable? Every helper defaults to
onGuardError: "deny". Ask explicitly about inbound screening before the turn: failing closed there means the agent does not answer, so"allow"is a routine and legitimate choice at that one call site.
The things readers get wrong
- This is not the Vercel AI SDK. Do not import
@arcjet/guard/vercel-ai/v7and do not wrap Think tools withguardToolfrom that namespace. Think already ownsstreamText. - There is no
guardInbound. Screen with coreguard()beforechat()/submitMessages(). needsApprovalis HITL, not policy.- Fail closed = always return
blockorsubstituteon error.voidexecutes the tool. A throw is not a denial. Unavailable fail-closes with substitute, even whenonDeny: "block". - Default DENY is substitute (structured
ArcjetDenialResult).blockis the reason-string path and drops the fields. It applies to real DENY only. guardHooksis not a mixin. DelegatebeforeToolCalltohooks.beforeToolCall(ctx). There is noguardThink(Think, …).- Correlation is read, never minted. Never Durable Object id,
never
toolCallId, neverrequestId, nevertraceId. - The import path is versioned.
@arcjet/guard/cloudflare-think/v0resolves;@arcjet/guard/cloudflare-thinkdoes not. - Key rate limits on the authenticated caller, not a
model-supplied order id. Same for
actor. - Omit
actor/inputsand remote rules never fire. A resolver throw fail-closes. ImportpolicyInputfrom@arcjet/guard. - Do not hand-wrap every Think tool with raw
guard().
Step 1: Install and find the guard client
Install published @arcjet/guard and the @cloudflare/think peer.
Use the package manager the project already uses — do not pin a
git SHA:
npm install @arcjet/guard @cloudflare/think
pnpm add @arcjet/guard @cloudflare/thinkPeer range is @cloudflare/think >=0.3.0 <1. The versioned path
@arcjet/guard/cloudflare-think/v0 resolves; the unversioned
@arcjet/guard/cloudflare-think path does not. If the agent has
no guard client yet, launch one once at module scope:
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });Step 2: Gate tool calls — delegate beforeToolCall
Pass the current conversation's authenticated caller into
guardHooks. A Durable Object is often one-per-conversation — still
do not use this.ctx.id / this.name as actor or correlation.
import { Think } from "@cloudflare/think";
import { launchArcjet, policyInput, tokenBucket } from "@arcjet/guard";
import { guardHooks } from "@arcjet/guard/cloudflare-think/v0";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const lookupLimit = tokenBucket({
bucket: "lookups",
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
// From auth middleware or Durable Object state you already trust.
// Not this.ctx.id / this.name, and not a model-produced argument.
const userId = authenticatedUserId;
const conversationId = authenticatedConversationId;
const hooks = guardHooks(arcjet, {
action: ({ toolName }) => `${toolName}.invoked`,
// Authenticated caller — never a model-produced order id / ctx.input.
actor: userId,
inputs: ({ toolName }) => ({
tool: policyInput.server.string(toolName),
}),
// Keyed on the authenticated caller, not the model-supplied order id.
rules: () => [lookupLimit({ key: userId, requested: 1 })],
sessionId: conversationId,
onGuardError: "deny",
// default DENY is substitute (ArcjetDenialResult). onDeny: "block"
// uses { action: "block", reason } on real DENY only. Unavailable
// still substitutes.
});
export class SupportAgent extends Think<Env> {
// needsApproval on a tool is HITL — not this policy gate
override beforeToolCall = hooks.beforeToolCall;
}If the subclass already implements beforeToolCall, call
hooks.beforeToolCall(ctx) and return its ToolCallDecision. Do
not void past a DENY and do not also wrap with
@arcjet/guard/vercel-ai/v7.
Omit rules to submit none — the guard call still happens. ALLOW
captures outcome: "success" when the policy lets the tool run,
not when execute finishes.
Step 3: Screen inbound before the turn
import { detectPromptInjection, policyInput } from "@arcjet/guard";
import { cloudflareThinkContext } from "@arcjet/guard/cloudflare-think/v0";
const inbound = detectPromptInjection();
const decision = await arcjet.guard({
label: "message.received",
actor: userId,
inputs: { content: policyInput.server.string(userText) },
rules: [inbound(userText)],
...cloudflareThinkContext({ context: { sessionId: conversationId } }),
});
if (decision.conclusion === "DENY") {
throw new Error("message blocked");
}
if (decision.hasFailedOpen()) {
throw new Error("inbound guard unavailable");
}
// Then chat() / submitMessages() / the WebSocket turn.There is no guardInbound. guard() fails open — always check
hasFailedOpen().
Step 4: Correlation
cloudflareThinkContext reads a caller-owned id. Preference:
correlationId, then sessionId, then conversationId on a
caller-owned wrap (cloudflareThinkContext({ context: appContext })),
then copies on a bare app object, then guardHooks({ sessionId }) /
init.correlationId. It never mints. It never reads the Durable
Object id / this.ctx.id / this.name. It never reads toolCallId,
requestId, traceId, or stream ids Think or the AI SDK mint. A
beforeToolCall context that has toolCallId and toolName is
Think's envelope — top-level sessionId on that object is ignored.
Do not invent a correlation id per turn. If nothing valid remains,
the call is uncorrelated rather than joined to a generated id.
Verify the integration
npm run typecheck(or the project's type-check) passes.- Exercise inbound PI (before the turn, including
hasFailedOpen()), a substitute-deny (model seesArcjetDenialResult), a block-deny (reason string; real DENY only), unavailable +onDeny: "block"still substitute, void execute, no-throw, never-mint, omittedactor/inputs(remote rules silent), a resolver throw (fail-closed substitute), and fail-closed (an unreachable guard → substitute, never void). ConfirmneedsApprovalis never treated as the gate and that tools are not also wrapped with@arcjet/guard/vercel-ai/v7. - Confirm in the Arcjet Console / CLI that decisions share the
caller-owned session / conversation id — not a Durable Object id
or
toolCallId— and thatactoris the authenticated caller. - Manual E2E with a real
ARCJET_KEYis still-to-verify until you run it.
Worked example:
examples/cloudflare-think-agent
(no verify fixture; main does not have this tree yet). Do not
invent a second example name. Do not add an example in this skills
repo.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

