#Openclaw guardrail
1 messages · Page 1 of 1 (latest)
Not as a single built-in “required parameter on every agent call” switch, no.
Closest options:
- Fixed value every time: put it in prompt/context, for example via
AGENTS.mdor a channel/chatsystemPrompt. - Dynamic value per message like the sender’s phone number: use a hook/plugin and inject it before prompt build. The relevant field is
senderE164/SenderE164(best effort). - Media model CLI templates only: there is already a
{{SenderE164}}template variable, but that is not a universal chat-agent parameter.
Good docs:
- https://docs.openclaw.ai/concepts/system-prompt
- https://docs.openclaw.ai/concepts/agent-loop
- https://docs.openclaw.ai/automation/hooks
- https://docs.openclaw.ai/gateway/configuration-reference
So, short version:
- simple config for all normal agent turns: no
- possible with prompt injection/hook/plugin: yes
If you want, tell me whether you mean:
- a hardcoded phone number, or
- the current sender’s phone number,
and which channel you use, and I’ll point you to the exact setup.
Current sender phonr number
Yep, if the channel actually has a phone number.
What you want is:
senderE164in plugin hook metadataSenderE164in some template/internal contexts
Important catch:
- WhatsApp / Signal / some phone-based channels: usually yes, best effort
- Discord: no, there is no sender phone number to pass through
So the real answer is:
- Current sender phone number on phone-based channels: possible
- Force it into every normal model call with one config flag: no
- Do it via hook/plugin that injects
senderE164into prompt/body: yes
Docs:
- https://docs.openclaw.ai/automation/hooks
- https://docs.openclaw.ai/concepts/agent-loop
- https://docs.openclaw.ai/gateway/configuration-reference
If you want, I can sketch the exact hook pattern for WhatsApp specifically.
My proxy fallback implemented to route messages received from openclaw by whatsapp for an external of agent of mine follows this approach:
It accomplishes this through a strict, three-layer defense:
​1. Relying Only on Trusted Data
To figure out who is sending the message, the code completely ignores the actual text the user typed. Instead, it looks exclusively at secure, hidden metadata attached to the request (the X-Phone HTTP header or the system-level body.user field). Because the user cannot alter these background channels, the system can reliably extract the real phone number and use it to look up the person's true user_id in the database.
​2. Scrubbing the Chat History
Even though the system knows the real identity, the AI might still get confused if it reads a message containing fake credentials. To prevent this, the code intercepts the chat messages and runs them through a "scrubber" before the AI sees them. It uses regular expressions to hunt down and delete any typed text that looks like whatsapp_phone:, user_id:, tenant:, or manager:.
​3. Issuing a Locked-Down Key
Once the real user is identified, the proxy requests a temporary, cryptographically secure "bearer token" that is hardcoded to that specific user_id. When the proxy finally forwards the scrubbed chat to the AI, it attaches this specific token. This ensures that even if the AI somehow gets tricked into requesting another person's data, the internal systems will reject the request because the token only has permission to view the current user's files.
​The Fallback Mechanism
If the system cannot find a trusted phone number in the background data, it instantly blocks access. In this specific scenario, it will briefly scan the user's typed message to see if they provided a phone number, but it only uses that number to generate a helpful "Please click this link to verify your device" URL. It never uses that typed number to actually log them in.
Honestly, that’s the right pattern.
I’d frame it like this:
- Do not rely on the model to carry identity
- Resolve identity from trusted channel metadata (
senderE164/ internal sender fields) - Enforce authorization with a scoped backend token
- Treat prompt scrubbing as defense-in-depth, not the security boundary
So yes, your 3-layer approach is solid.
A few important caveats:
-
X-Phoneis only trustworthy if only your proxy can set it
If that header can come from the public edge, sign it or keep that hop internal only. -
Regex scrubbing helps, but it is not real auth
Good for reducing prompt confusion, bad as a hard guarantee. Your scoped bearer token is the actual safety boundary.
-
Fail closed is correct
If trusted sender metadata is missing, block access. -
Keep tokens short-lived and tightly scoped
Bind to user id, tenant, maybe channel/account too, and give them a short TTL. -
Rate-limit the fallback verification flow
Otherwise typed phone numbers can become an enumeration or spam vector.
So my recommendation is:
- Don’t try to force OpenClaw/the model to always “send phone number” as a prompt parameter
- Keep identity outside the model
- Use trusted metadata + backend-scoped auth, exactly like you described
That lines up well with OpenClaw’s security model too, where prompt rules are soft and the real protection comes from hard boundaries. Docs: https://docs.openclaw.ai/gateway/security
If you want, I can help you turn this into a concrete OpenClaw-side design, for example:
- hook/plugin injection using
senderE164 - proxy contract shape
- token claims format
Yeah, but it's not guaranteed that openclaw send to proxy numbers, I can guarantre that with this plugin somehow? Or do I need to move proxy to above openclaw channels instead below it in front of our custom external agent
Thanks — your caveats were useful. Sanity-check before I commit.
Our proxy today sits below OpenClaw, presenting an OpenAI-compatible /v1/chat/completions. It resolves senderE164 → user_id, mints a short-lived HMAC bearer scoped to that user_id, and the backend rejects any bearer whose user_id doesn't match the path (cross-tenant blocked cryptographically — verified).
Problem: From the LLM position, identity intake is fragile. body.user/X-Phone aren't consistently populated, and prompt-scraped markers are injectable so they can't feed auth.
Two options:
- (a) Stay below: Only viable if there's an OpenClaw primitive that guarantees
senderE164reaches the LLM endpoint. - (b) Move in front: Gateway ingests the webhook first, resolves identity from the payload, stashes
conversation_id→ scoped bearer, forwards to OpenClaw. Backend looks up the bearer byconversation_id.
Questions:
- Is there a supported OpenClaw primitive (hook/plugin/header) piping
senderE164reliably into the downstream model call? If yes, (a) wins. - If I move in front, what commonly breaks — signature verification, media URLs, retries, continuation turns?
- Which position is the production norm?
Let me verify the plumbing before I steer you.
Sanity check, I’d not bet prod on (a).
1) Is there a supported primitive that reliably pipes senderE164 into the downstream model call?
Short answer: no, not as a first-class turnkey primitive.
What exists today:
senderE164is available in message/plugin hook metadata- provider calls support:
- static provider headers via
models.providers.*.headers - custom provider hooks like
wrapStreamFnandresolveTransportTurnState
- static provider headers via
What does not exist, as far as the current docs/code show:
- no built-in
x-openclaw-sender-e164style downstream header - no guaranteed automatic forwarding of channel sender identity into the model transport
- no provider hook context that naturally carries
senderE164end-to-end
So option (a) only works if you build a custom bridge yourself. That is possible, but it is bespoke, not a supported primitive.
2) If you move in front, what commonly breaks?
This is the main checklist:
-
Webhook signature verification
- easiest thing to accidentally break if you reserialize body / lose raw headers
-
Media fetch
- expiring media URLs, auth headers, cookies, or provider-specific attachment fetch rules
-
Retries / dedupe
- upstream webhook retries must be idempotent by provider message id
-
Session routing
- this one matters a lot in OpenClaw:
- WhatsApp DMs default to
session.dmScope: "main", which collapses all DMs into one main session unless you change it - for multi-user auth, set:
session.dmScope: "per-channel-peer"orsession.dmScope: "per-account-channel-peer"(better for multi-account)
-
Group chats
- do not key bearer by bare
conversation_idif multiple senders share that conversation - OpenClaw’s default group
scopeisper-sender, which is good, but your bearer cache needs to match that
- do not key bearer by bare
-
Continuation turns
- if auth lives only on the ingress webhook, later turns/retries/background continuations may not have the original payload
- persist auth against the OpenClaw session key, not just the external conversation id
If you go HTTP into Gateway, the supported routing primitive is x-openclaw-session-key. That is much safer than hoping user or prompt content stays aligned.
3) Which position is the production norm?
(b), identity in front, almost always.
Production norm is:
- resolve identity at ingress
- bind auth to session/tool/backend
- keep the model transport layer mostly dumb
That matches OpenClaw’s architecture better too. The provider/model boundary is not the right place to discover end-user identity.
My recommendation
I’d ship:
- (b) move in front
- cache scoped auth by session key, not plain
conversation_id - set
session.dmScope: "per-account-channel-peer" - keep group auth keyed per sender/session, not per room only
So, bluntly: don’t commit to “guaranteed senderE164 reaches downstream model proxy” unless you’re willing to own a custom plugin bridge. For prod, ingress-side identity is the cleaner boundary.
Docs I checked:
- https://docs.openclaw.ai/gateway/openai-http-api
- https://docs.openclaw.ai/gateway/configuration-reference
- https://docs.openclaw.ai/channels/whatsapp
If you want, I can sketch the exact session-keyed bearer design I’d use here.
Which WhatsApp provider? Twilio, Meta Cloud API, or something else? The webhook signature scheme differs (Twilio uses X-Twilio-Signature HMAC, Meta uses X-Hub-Signature-256). Our ingress proxy has to
verify the right one.
OpenClaw’s built-in WhatsApp channel is neither Twilio nor Meta Cloud API.
It is:
- WhatsApp Web via Baileys
- Gateway owns the linked WhatsApp session/socket directly
So for stock OpenClaw WhatsApp:
- there is no Twilio webhook signature
- there is no Meta
X-Hub-Signature-256webhook - your “verify inbound webhook signature” step only applies if you built a custom WhatsApp ingress in front of OpenClaw
Docs: https://docs.openclaw.ai/channels/whatsapp
So the clean distinction is:
- Built-in OpenClaw WhatsApp → Baileys socket, no Twilio/Meta webhook auth layer
- Your custom/provider-managed ingress → verify whatever that upstream provider uses
If you want, I can update the earlier recommendation specifically for the Baileys/OpenClaw-native path, because it changes the ingress picture a bit.
sure, do that
For the Baileys/OpenClaw-native path, I’d change the recommendation a bit:
Recommended shape
Do not add a separate ingress proxy in front.
There is no Twilio/Meta webhook layer to intercept anyway.
Instead:
- Let OpenClaw ingest WhatsApp directly via Baileys
- At the Gateway/plugin layer, read trusted sender metadata (
senderE164) - Resolve
senderE164 -> user_id - Mint/store a short-lived scoped bearer
- Bind that bearer to the OpenClaw session key
- Have your backend/tooling read auth from that session-side binding, not from prompt text and ideally not from the downstream model transport either
Why this is cleaner
With native WhatsApp:
- OpenClaw already has the trusted channel metadata
- there is no webhook signature problem to solve
- the fragile part is only the handoff from channel/session layer to your backend auth
- so the right boundary is inside OpenClaw, above the model provider
Two important config choices
For this to be safe, I’d set:
{
session: {
dmScope: "per-account-channel-peer",
scope: "per-sender"
}
}
Why:
- default WhatsApp DM behavior is
dmScope: "main", which is too loose for per-user auth per-account-channel-peerisolates direct chats properlyscope: "per-sender"keeps group auth from collapsing multiple people into one room-level identity
What I would avoid
I would not:
- rely on prompt markers
- rely on the model to echo identity back
- rely on a bare
conversation_id -> bearermapping for groups - make the downstream OpenAI-compatible proxy the place where user identity is “discovered”
If you absolutely must keep auth below OpenClaw
Then you’re in custom provider plugin / sidecar lookup territory, not a stock OpenClaw primitive.
That can work, but it’s more brittle than binding auth at the Gateway/session layer.
Bottom line
For native OpenClaw WhatsApp, the production-ish path I’d choose is:
Baileys ingress -> OpenClaw plugin/hook resolves sender -> session-keyed scoped bearer -> backend/tool auth
not
Baileys ingress -> prompt/header guessing -> downstream model proxy auth
If you want, I can sketch the exact session-keyed bearer map I’d use for:
- direct chats
- group chats
- bearer TTL / refresh rules
Im writing a plan by myself, but I need to know specifically:
- How plugins are loaded in this self-hosted deployment. Dockerfile +
workspace-colmeia/+workspace-config/structure suggests there's a workspace manifest wiring plugins in. I need the exact shape — JS module? Python? OpenClaw-native TS? - Current session config in workspace-colmeia — is
dmScopealready set, or are we relying on the default "main" (which is the loose one)? - Where provider/tool calls are configured — specifically, where Bee gets called from inside OpenClaw. The plugin needs to attach the bearer to exactly that call site (header, request interceptor, or tool-context injection — depends on OpenClaw's plugin API shape).
- Does OpenClaw expose a session.set(key, value) + session.get(key) API for plugin-to-tool-call state sharing? That's the natural home for the bearer. If not, we fall back to an external KV keyed by
session_key.
Stock OpenClaw, WhatsApp via Baileys, OpenAI-compatible provider. When a plugin's subagent_spawning hook resolves identity from ctx.requesterSessionKey, what field on OpenClaw's outbound provider request lets the receiving endpoint correlate back to that same session? Header, body field, or something else?
I found the relevant transport hook. One more tiny check: whether that metadata object actually goes on the wire, or whether only the headers do.
Short answer: there is no stock outbound field that carries ctx.requesterSessionKey through to your OpenAI-compatible backend.
What OpenClaw exposes on provider requests is, at most:
- native OpenAI/Azure/Codex Responses transport only
- headers:
x-client-request-id: <sessionId>x-openclaw-session-id: <sessionId>x-openclaw-turn-id: <turnId>x-openclaw-turn-attempt: <attempt>
- body:
metadata.openclaw_session_idmetadata.openclaw_turn_idmetadata.openclaw_turn_attempt
- headers:
Important catch:
- that is
sessionId, notrequesterSessionKey - and for a generic OpenAI-compatible base URL, stock OpenClaw does not attach those OpenClaw session headers/metadata at all
So for your exact setup, the receiving endpoint should assume:
- Header carrying requesterSessionKey? no
- Body field carrying requesterSessionKey? no
- Anything equivalent in stock generic OpenAI-compatible mode? effectively no
If you need correlation back to the same OpenClaw session, you need a custom layer, usually one of these:
- provider plugin
wrapStreamFn/ payload hook to inject your own header/body field - session-side binding store keyed by
requesterSessionKey, then send only your own opaque token downstream - custom OpenAI-compatible proxy that OpenClaw talks to, which adds your session correlation fields
So my recommendation is: don’t depend on stock provider requests to carry requesterSessionKey. Add your own explicit correlation field.
api: openai-completions + WhatsApp (Baileys) — is there a config option or channel-plugin flag that populates body.user or a header with senderE164 on outbound provider requests? Or is resolveTransportTurnState only wired for the responses/Azure transports and not generic completions?
Let me verify the last bit so I don’t overstate it: whether there’s any hidden config path that maps sender info into user or provider headers for the completions transport.
Yeah, for api: openai-completions the answer is basically:
- No, there is no stock config or WhatsApp channel flag that auto-populates
body.userwithsenderE164 - No, there is no stock per-message header injection of
senderE164on outbound provider calls - Yes, your reading is right:
resolveTransportTurnStateis wired into the Responses / Azure / WS-native transport path, not the generic chat-completions path
More concretely:
buildOpenAICompletionsParams(...)buildsmodel,messages,stream,tools, etc. but does not setusercreateOpenAICompletionsClient(...)uses configured provider headers, but there’s no built-in dynamic senderE164 mappingmodels.providers.*.headersexists, but that is for static headers, not “inject this WhatsApp sender phone number for each request”
So for Baileys WhatsApp + openai-completions, stock OpenClaw does not forward senderE164 to your provider endpoint in either:
- request body
user - dynamic custom headers