#Does MyClaw support MCP or external tool integration?Can a MyClaw agent call external tools running

1 messages · Page 1 of 1 (latest)

south swan
#

I'm using MyClaw (myclaw.ai) as my main agent platform.
My agent (Benest) runs on MyClaw cloud with MiniMax M2.7.

I want to connect Benest to local tools running on WSL2:

  • Claude Code (/usr/bin/claude)
  • Gemini CLI (/usr/bin/gemini)

Questions:

  1. Does MyClaw support MCP (Model Context Protocol)?
  2. Can a MyClaw agent call external tools on a local machine?
  3. Is there an API or webhook that allows external tools to communicate with a MyClaw agent?
  4. What's the recommended architecture for this kind of integration?
onyx oasisBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

swift nimbusBOT
barren spokeBOT
#

Let me verify the current docs/source before I answer, I don't want to guess on protocol support.

#

I checked the OpenClaw docs. I don’t see any MyClaw-specific docs in the repo, so I’m going to verify the MyClaw side separately instead of pretending I know.

#

Short version: OpenClaw supports the pieces you want, but for MyClaw specifically I can only confirm what the underlying OpenClaw platform supports, not which surfaces MyClaw exposes on hosted instances.

  1. MCP?
    Yes, OpenClaw supports MCP.
    See: https://docs.openclaw.ai/cli/mcp
    Important nuance: openclaw mcp serve is a stdio MCP server, so it is best when a client like Claude Code/Codex connects to OpenClaw. It is not the usual choice for a cloud agent reaching into WSL2.

  2. Can an agent call tools on a local machine?
    Yes, if you run a local OpenClaw Gateway on that machine.
    For Claude Code / Gemini CLI, the right mechanism is usually ACP, not MCP:
    https://docs.openclaw.ai/tools/acp-agents
    That lets OpenClaw host local harnesses like Claude Code and Gemini CLI on WSL2.

  3. API or webhook for external communication?
    Yes. Relevant surfaces:

#
  1. Recommended architecture
    I’d do it like this:

    Benest on MyClaw cloud
    → calls a private endpoint on your local OpenClaw Gateway running in WSL2
    → local Gateway launches Claude Code / Gemini CLI via ACP
    → result comes back to Benest via HTTP response or webhook callback

    So, in practice:

    • Run OpenClaw Gateway locally on WSL2
    • Configure ACP for Claude Code and Gemini CLI
    • Expose the Gateway only over Tailscale/private ingress, not public internet
#
  • Use /hooks/agent or /v1/responses as the cloud-to-local bridge
    • Use MCP only if the client itself can maintain an MCP session

If you want, I can sketch a concrete WSL2 setup for this next, with the exact local Gateway config and which endpoint Benest should call.

south swan
#

Yes, please sketch the concrete WSL2 setup.
Specifically:

  1. Local Gateway config for Claude Code + Gemini CLI via ACP
  2. Which endpoint should Benest (on MyClaw cloud) call to reach the local Gateway?
  3. How to expose the local Gateway securely (Tailscale setup)?
barren spokeBOT
#

I’m going to pull the exact config keys for ACP and the documented Tailscale exposure path so I don’t hand you fake JSON5.

#

Yep. Here’s the concrete setup I’d use.

1) Local WSL2 Gateway config for Claude Code + Gemini CLI via ACP

I’d make two OpenClaw agents that both run through ACP:

  • OpenClaw agent id: claude-local → ACP harness claude
  • OpenClaw agent id: gemini-local → ACP harness gemini

Example openclaw.json5 sketch:

{
  gateway: {
    bind: "loopback",
    auth: {
#
      mode: "token",
      token: "REPLACE_WITH_GATEWAY_TOKEN",
      // stricter: still require token even over Tailscale Serve
      allowTailscale: false,
    },
    tailscale: {
      mode: "serve",
    },
    http: {
      endpoints: {
        responses: { enabled: true }, // optional, only if you want /v1/responses
      },
    },
  },
#
  hooks: {
    enabled: true,
    token: "REPLACE_WITH_HOOK_TOKEN",
    path: "/hooks",
    defaultSessionKey: "hook:benest",
    allowRequestSessionKey: false,
    allowedSessionKeyPrefixes: ["hook:"],
    allowedAgentIds: ["claude-local", "gemini-local"],
  },

  acp: {
    enabled: true,
    backend: "acpx",
    defaultAgent: "claude",
    allowedAgents: ["claude", "gemini"],
#
    maxConcurrentSessions: 4,
    runtime: {
      ttlMinutes: 120,
    },
  },

  agents: {
    list: [
      {
        id: "claude-local",
        runtime: {
          type: "acp",
          acp: {
            agent: "claude",
            backend: "acpx",
#
            mode: "persistent",
            cwd: "/home/youruser/workspaces",
          },
        },
      },
      {
        id: "gemini-local",
        runtime: {
          type: "acp",
          acp: {
            agent: "gemini",
            backend: "acpx",
            mode: "persistent",
            cwd: "/home/youruser/workspaces",
          },
#
        },
      },
    ],
  },

  plugins: {
    entries: {
      acpx: {
        enabled: true,
        config: {
          timeoutSeconds: 180,
          probeAgent: "claude",
          permissionMode: "approve-all",
          nonInteractivePermissions: "fail",
        },
#
      },
    },
  },
}

Important notes:

  • permissionMode: "approve-all" is usually needed for Claude/Gemini ACP runs that write files or exec commands. Otherwise non-interactive sessions can fail.
  • Make sure claude and gemini are both installed and already authenticated inside WSL2.
  • Sanity check with:
    • openclaw --version
    • openclaw status
    • /acp doctor

Docs:

#
    "message": "Analyze repo state and summarize next steps",
    "agentId": "claude-local",
    "timeoutSeconds": 900
  }'

Why I’d start there:

  • simpler than /v1/responses
  • easy to route to claude-local or gemini-local
  • defaultSessionKey: "hook:benest" gives you continuity across calls

Use /v1/responses only if Benest already speaks an OpenAI-style API and you want a more standardized contract.

Docs:

#

Important caveat

Tailscale Serve is tailnet-only.
So if Benest runs on MyClaw cloud, it usually cannot call your Tailscale URL directly unless MyClaw can somehow reach your tailnet.

So the practical architecture is:

Benest (cloud)
→ your small public relay / connector
→ http://127.0.0.1:18789/hooks/agent on WSL2
→ ACP launches Claude/Gemini

I would not expose /hooks/agent or /v1/responses directly to the public internet just to let MyClaw call it. The docs are pretty explicit about keeping those on loopback/tailnet/private ingress.

My actual recommendation

#

Use this split:

  • Tailscale Serve for you
  • /hooks/agent on localhost for the local bridge
  • a tiny public relay/connector between MyClaw cloud and your WSL2 box

If you want, I can sketch the relay pattern next too, for example:

  1. Cloudflare Worker / tiny VPS relay
  2. local polling connector
  3. webhook callback model
south swan
#

I have a Cloudflare account.
Please sketch the relay pattern using Cloudflare Worker:

  • MyClaw cloud agent (Benest) sends request to Cloudflare Worker
  • Cloudflare Worker forwards to WSL2 local Gateway /hooks/agent
  • Response comes back to Benest

Questions:

  1. How does the Cloudflare Worker reach WSL2 on my local machine?
  2. Do I need Cloudflare Tunnel for this?
  3. Basic Worker code sketch?
barren spokeBOT
#

Yep, this pattern can work.

Short answer

  1. How does the Worker reach WSL2?
    It cannot reach your laptop directly. You give it a reachable HTTPS origin, usually via Cloudflare Tunnel.

  2. Do you need Cloudflare Tunnel?
    Practically, yes for this design, unless you already have some other secure public ingress to WSL2.
    Worker alone is not enough.

  3. Basic pattern
    Benest -> Worker -> Cloudflare Tunnel hostname -> local OpenClaw /hooks/agent -> Worker -> Benest


Recommended architecture

#
Benest (MyClaw cloud)
  -> POST https://relay.example.com/benest
  -> Cloudflare Worker validates Benest secret
  -> Worker fetches https://openclaw-origin.example.com/hooks/agent
       with:
       - Cloudflare Access service token headers (optional but recommended)
       - OpenClaw hook bearer token
  -> cloudflared tunnel on WSL2 forwards to http://127.0.0.1:18789
  -> OpenClaw runs claude-local or gemini-local
  -> JSON response goes back through Worker to Benest

Why Tunnel is the right piece

Run cloudflared on the machine that can see 127.0.0.1:18789:

#
  • no router port forwarding
  • no public inbound port on your home network
  • Gateway can stay bind: "loopback"

I’d keep Tailscale for your own admin access, and use Cloudflare Tunnel only for the relay path.


Local side

Your OpenClaw Gateway stays local:

  • gateway.bind: "loopback"
  • hooks.enabled: true
  • dedicated hooks.token
    (docs explicitly say this must be different from gateway.auth.token)
#

Then cloudflared publishes a hostname to that local port.

cloudflared ingress sketch

tunnel: YOUR_TUNNEL_ID
credentials-file: /home/you/.cloudflared/YOUR_TUNNEL_ID.json

ingress:
  - hostname: openclaw-origin.example.com
    service: http://127.0.0.1:18789
  - service: http_status:404

That gives the Worker a stable origin like:

  • https://openclaw-origin.example.com/hooks/agent
#

Strongly recommended extra layer: Cloudflare Access

Do this if you want the tunnel hostname not to be generally callable from the internet.

Pattern:

  • protect openclaw-origin.example.com with Cloudflare Access
  • create a service token
  • store that token in Worker secrets
  • Worker includes:
    • CF-Access-Client-Id
    • CF-Access-Client-Secret

So you get two auth layers:

#
  1. Worker -> origin: Cloudflare Access
  2. origin /hooks/agent: OpenClaw hook token

That’s much better than exposing the tunnel hostname naked.


Worker code sketch

export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }
#
    // 1) Authenticate Benest -> Worker
    const auth = request.headers.get("authorization") || "";
    if (auth !== `Bearer ${env.BENEST_RELAY_TOKEN}`) {
      return new Response(JSON.stringify({ ok: false, error: "unauthorized" }), {
        status: 401,
        headers: { "content-type": "application/json" },
      });
    }

    // 2) Parse + validate input
    let body;
    try {
      body = await request.json();
    } catch {
      return new Response(JSON.stringify({ ok: false, error: "invalid_json" }), {
#
        status: 400,
        headers: { "content-type": "application/json" },
      });
    }

    const allowedAgents = new Set(["claude-local", "gemini-local"]);
    const agentId = allowedAgents.has(body.agentId) ? body.agentId : "claude-local";

    const payload = {
      message: body.message,
      agentId,
      timeoutSeconds: body.timeoutSeconds ?? 900,
    };

    // 3) Forward to local OpenClaw through the tunnel
#
    const headers = {
      "content-type": "application/json",
      "authorization": `Bearer ${env.OPENCLAW_HOOK_TOKEN}`,
    };

    // Optional, but recommended if origin is behind Cloudflare Access
    if (env.CF_ACCESS_CLIENT_ID && env.CF_ACCESS_CLIENT_SECRET) {
      headers["CF-Access-Client-Id"] = env.CF_ACCESS_CLIENT_ID;
      headers["CF-Access-Client-Secret"] = env.CF_ACCESS_CLIENT_SECRET;
    }

    const upstream = await fetch(`${env.OPENCLAW_ORIGIN_URL}/hooks/agent`, {
      method: "POST",
      headers,
      body: JSON.stringify(payload),
#
    });

    const text = await upstream.text();

    return new Response(text, {
      status: upstream.status,
      headers: {
        "content-type": upstream.headers.get("content-type") || "application/json",
      },
    });
  },
};

Worker secrets / vars

#
  • BENEST_RELAY_TOKEN
  • OPENCLAW_ORIGIN_URL = https://openclaw-origin.example.com
  • OPENCLAW_HOOK_TOKEN
  • CF_ACCESS_CLIENT_ID (optional)
  • CF_ACCESS_CLIENT_SECRET (optional)

Important caveat

For slow Claude/Gemini runs, I’d be a little cautious about a fully synchronous Worker round trip. It can work for shorter requests, but for bigger ACP jobs an async job model is safer:

  • Worker accepts request
  • queues/stores job
  • local side processes it
  • Benest polls status or gets callback
#

But for a first version, sync proxying is fine.

My recommendation

If you want the simplest secure v1:

  • Cloudflare Worker = public entrypoint for Benest
  • Cloudflare Tunnel = Worker-to-local path
  • Cloudflare Access = origin protection
  • OpenClaw /hooks/agent = actual local execution API

If you want, I can sketch the full minimal config set next:

  1. openclaw.json5
  2. cloudflared setup commands
  3. Worker wrangler.toml
  4. a sample Benest request body
south swan
#

Yes, please sketch the full minimal config set:

  1. openclaw.json5 (WSL2 side)
  2. cloudflared setup commands
  3. Worker wrangler.toml
  4. Sample Benest request body
barren spokeBOT
#
  1. openclaw.json5 (WSL2 side)
{
  gateway: {
    bind: "loopback",
    auth: {
      mode: "token",
      token: "REPLACE_WITH_GATEWAY_TOKEN",
      allowTailscale: false,
    },
    tailscale: {
      mode: "off", // keep Tailscale for your own admin path if you want, not for the Worker relay
    },
  },
#
  hooks: {
    enabled: true,
    token: "REPLACE_WITH_HOOK_TOKEN", // must be different from gateway.auth.token
    path: "/hooks",
    defaultSessionKey: "hook:benest",
    allowRequestSessionKey: false,
    allowedSessionKeyPrefixes: ["hook:"],
    allowedAgentIds: ["claude-local", "gemini-local"],
  },

  acp: {
    enabled: true,
    backend: "acpx",
    defaultAgent: "claude",
    allowedAgents: ["claude", "gemini"],
#
    maxConcurrentSessions: 4,
    runtime: {
      ttlMinutes: 120,
    },
  },

  agents: {
    list: [
      {
        id: "claude-local",
        runtime: {
          type: "acp",
          acp: {
            agent: "claude",
            backend: "acpx",
#
            mode: "persistent",
            cwd: "/home/youruser/workspaces",
          },
        },
      },
      {
        id: "gemini-local",
        runtime: {
          type: "acp",
          acp: {
            agent: "gemini",
            backend: "acpx",
            mode: "persistent",
            cwd: "/home/youruser/workspaces",
          },
#
        },
      },
    ],
  },

  plugins: {
    entries: {
      acpx: {
        enabled: true,
        config: {
          timeoutSeconds: 180,
          probeAgent: "claude",
          permissionMode: "approve-all",
          nonInteractivePermissions: "fail",
        },
#
      },
    },
  },
}

Quick sanity checks after saving:

openclaw --version
openclaw status
openclaw gateway status

#
  1. cloudflared setup commands

Assuming your public hostname for the tunnel origin is:

  • openclaw-origin.example.com

and your Worker public endpoint is:

  • relay.example.com

Create and run the tunnel from the machine that can reach http://127.0.0.1:18789.

cloudflared tunnel login
cloudflared tunnel create openclaw-origin
cloudflared tunnel route dns openclaw-origin openclaw-origin.example.com
#

Then create ~/.cloudflared/config.yml:

tunnel: REPLACE_WITH_TUNNEL_ID
credentials-file: /home/youruser/.cloudflared/REPLACE_WITH_TUNNEL_ID.json

ingress:
  - hostname: openclaw-origin.example.com
    service: http://127.0.0.1:18789
  - service: http_status:404

Test it in the foreground:

cloudflared tunnel run openclaw-origin
#

Once that works, your Worker will forward to:

  • https://openclaw-origin.example.com/hooks/agent

If you want the safer version later, put Cloudflare Access in front of openclaw-origin.example.com and let only the Worker call it via service token headers.


  1. Worker wrangler.toml
name = "benest-openclaw-relay"
main = "src/index.js"
compatibility_date = "2026-04-23"
workers_dev = false
#
routes = [
  { pattern = "relay.example.com/*", zone_name = "example.com" }
]

[vars]
OPENCLAW_ORIGIN_URL = "https://openclaw-origin.example.com"
DEFAULT_AGENT_ID = "claude-local"

Secrets to set, not in wrangler.toml:

wrangler secret put BENEST_RELAY_TOKEN
wrangler secret put OPENCLAW_HOOK_TOKEN
#

If you add Cloudflare Access on the tunnel origin, also set:

wrangler secret put CF_ACCESS_CLIENT_ID
wrangler secret put CF_ACCESS_CLIENT_SECRET

And use this minimal src/index.js:

export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }
#
    const auth = request.headers.get("authorization") || "";
    if (auth !== `Bearer ${env.BENEST_RELAY_TOKEN}`) {
      return new Response(JSON.stringify({ ok: false, error: "unauthorized" }), {
        status: 401,
        headers: { "content-type": "application/json" },
      });
    }

    let body;
    try {
      body = await request.json();
    } catch {
      return new Response(JSON.stringify({ ok: false, error: "invalid_json" }), {
        status: 400,
        headers: { "content-type": "application/json" },
#
      });
    }

    const allowed = new Set(["claude-local", "gemini-local"]);
    const agentId = allowed.has(body.agentId) ? body.agentId : env.DEFAULT_AGENT_ID;

    const upstreamHeaders = {
      "content-type": "application/json",
      "authorization": `Bearer ${env.OPENCLAW_HOOK_TOKEN}`,
    };

    if (env.CF_ACCESS_CLIENT_ID && env.CF_ACCESS_CLIENT_SECRET) {
      upstreamHeaders["CF-Access-Client-Id"] = env.CF_ACCESS_CLIENT_ID;
      upstreamHeaders["CF-Access-Client-Secret"] = env.CF_ACCESS_CLIENT_SECRET;
    }
#

    const upstream = await fetch(`${env.OPENCLAW_ORIGIN_URL}/hooks/agent`, {
      method: "POST",
      headers: upstreamHeaders,
      body: JSON.stringify({
        message: body.message,
        agentId,
        timeoutSeconds: body.timeoutSeconds ?? 900,
      }),
    });

    return new Response(await upstream.text(), {
      status: upstream.status,
      headers: {
        "content-type": upstream.headers.get("content-type") || "application/json",
#
      },
    });
  },
};

Deploy:

wrangler deploy

  1. Sample Benest request body
#

Benest calls the Worker, not OpenClaw directly.

Example request to:

  • POST https://relay.example.com/benest

Headers:

Authorization: Bearer REPLACE_WITH_BENEST_RELAY_TOKEN
Content-Type: application/json

Body:

#
{
  "agentId": "claude-local",
  "message": "Open /home/youruser/workspaces/project-a, inspect the repo, and summarize the next 3 concrete steps.",
  "timeoutSeconds": 900
}

Gemini version:

{
  "agentId": "gemini-local",
  "message": "Review /home/youruser/workspaces/project-b and propose a refactor plan.",
  "timeoutSeconds": 900
}
#

One caution: this minimal setup is synchronous. It’s fine for short to medium jobs. If Claude Code or Gemini CLI runs get long, I’d switch the Worker to an async job pattern next.

south swan
#

@barren spoke
Cloudflare Worker returns 401 Unauthorized even with hardcoded token check.
Worker code:
const auth = request.headers.get("authorization") || "";
if (auth !== "Bearer TEST_TOKEN_12345") {
return unauthorized response
}

curl sends: Authorization: Bearer TEST_TOKEN_12345
Response is still 401 with content-type: text/plain (not application/json)

Is Cloudflare itself blocking the request before it reaches Worker code?

barren spokeBOT
#

Probably not Cloudflare stripping Authorization.

A Worker can read the inbound Authorization header normally. The likely causes are:

  1. You’re not actually hitting that Worker
    wrong route, wrong hostname, or request is going to the origin instead.

  2. The 401 is your own Worker response
    if you returned plain text, content-type: text/plain is expected.

  3. Cloudflare Access / WAF is in front of the Worker route
    this can block before your Worker runs, but then I’d expect the response shape to look different.

Fastest test

Replace the Worker with this:

#
export default {
  async fetch(request) {
    return new Response(JSON.stringify({
      ok: true,
      url: request.url,
      auth: request.headers.get("authorization"),
    }), {
      status: 200,
      headers: {
        "content-type": "application/json",
        "x-worker-hit": "yes",
      },
    });
  },
};
#

Then run:

curl -i https://relay.example.com/benest \
  -H 'Authorization: Bearer TEST_TOKEN_12345'

What to look for

  • If you see x-worker-hit: yes, the request did hit the Worker
  • If auth is Bearer TEST_TOKEN_12345, Cloudflare is not stripping it
  • If you don’t see x-worker-hit: yes, you are not hitting the Worker

Best isolation step

Try the *.workers.dev URL first.

#
  • If workers.dev works, but relay.example.com fails:
    • check route binding
    • check Cloudflare Access
    • check WAF / bot / firewall rules
  • If workers.dev also fails, it’s your Worker code or deployment

My guess

Because you mentioned text/plain, I’m leaning toward:

  • either the Worker is returning the 401 itself
  • or the custom domain route is not attached and you’re hitting something else

If you paste the exact curl -i output and your wrangler.toml route block, I can usually spot it pretty fast.

south swan
#

@barren spoke Cloudflare Worker token authentication issue.

Worker code checks:
const auth = request.headers.get("authorization") || "";
const expected = Bearer ${env.BENEST_RELAY_TOKEN};
if (auth !== expected) { return 401 }

When I send wrong_token: returns JSON 401 (new version working)
When I send correct token: returns plain text "Unauthorized" (old version)

Seems like Cloudflare edge nodes are running different versions.

How do I force all edge nodes to use the latest Worker version?
Is there a way to purge the Worker cache?

barren spokeBOT
#

I don’t think this is stale edge code.

The big clue is:

  • wrong token -> your new JSON 401
  • correct token -> different plain text Unauthorized

That usually means:

  1. the latest Worker is running
  2. the correct token passes your Worker auth check
  3. then the Worker hits the upstream
  4. the upstream returns the plain text 401

So this looks much more like an origin / tunnel / Access / OpenClaw hook auth problem than a Cloudflare rollout problem.

Direct answers

#

Force all edge nodes to latest Worker version?

Normally wrangler deploy is enough. There is no usual “purge Worker code cache” step.

If you are using Workers Versions / gradual deployments, make sure the latest version is promoted to 100%. Otherwise Cloudflare is not supposed to serve mixed versions randomly for one deployment.

Purge Worker cache?

For script code: not really, that’s not how Workers rollouts work.

For HTTP responses: yes, but only if you actually cached responses via:

  • Cache API
  • Cache Rules
  • a cacheable origin response

That would affect response bodies, not the Worker script itself.

Why I think it’s upstream

#

Your auth branch is deterministic:

  • bad token never reaches origin
  • good token does

So if only the good token path shows the weird/plain-text 401, that 401 is probably coming from:

  • Cloudflare Access on openclaw-origin.example.com
  • wrong/missing CF-Access-Client-Id / CF-Access-Client-Secret
  • wrong OpenClaw hook token
  • wrong tunnel hostname / route
  • origin not actually pointing at /hooks/agent

Best next step

Add explicit debug headers in the Worker:

#
if (auth !== expected) {
  return new Response(JSON.stringify({ ok: false, where: "worker-auth" }), {
    status: 401,
    headers: {
      "content-type": "application/json",
      "x-relay-branch": "worker-auth",
      "x-worker-version": "2026-04-23-1",
    },
  });
}

const upstream = await fetch(...);

return new Response(await upstream.text(), {
  status: upstream.status,
#
  headers: {
    "content-type": upstream.headers.get("content-type") || "text/plain",
    "x-relay-branch": "upstream",
    "x-worker-version": "2026-04-23-1",
    "x-upstream-status": String(upstream.status),
  },
});

Then run:

curl -i https://relay.example.com/benest \
  -H 'Authorization: Bearer CORRECT_TOKEN' \
  -H 'Content-Type: application/json' \
#
  --data '{"message":"test","agentId":"claude-local"}'

What the result will tell you

  • x-relay-branch: worker-auth
    your Worker is rejecting it

  • x-relay-branch: upstream
    your Worker is fine, the origin is returning the 401

Also useful

  • wrangler tail while you send the request
  • check response headers for CF-Cache-Status
  • test the origin separately from the Worker if possible
#

My honest bet: the Worker is current, and the plain text 401 is coming from the upstream fetch path, not from mixed edge versions.

south swan
#

@barren spoke Cloudflare Worker token match is confirmed (match: true in debug mode).
But when using real relay code, still getting plain text "Unauthorized".

Debug mode returns correct match:
{"received":"Bearer xxx","relay_token":"46e7c2d0","match":true}

But real relay code returns plain text "Unauthorized" not JSON.

Could the issue be that the real relay code is an older cached version?
How do I force Cloudflare to serve only the latest Worker version?

barren spokeBOT
#

If your debug Worker returns match: true, then the latest Worker code is already running and seeing the right bearer token.

So no, this really doesn’t look like stale cached Worker code.

What it looks like instead:

  1. Benest -> Worker auth succeeds
  2. Worker continues to the upstream fetch
  3. Upstream returns plain text Unauthorized

Most likely sources of that upstream 401:

  • wrong OPENCLAW_HOOK_TOKEN
  • missing / wrong CF-Access-Client-Id and CF-Access-Client-Secret
  • wrong OPENCLAW_ORIGIN_URL
  • wrong path, not actually hitting /hooks/agent
#

About forcing latest Worker version

For normal Workers, there is no “purge Worker code cache” step.

  • wrangler deploy publishes the latest version
  • if you are using Versions / gradual deployments, make sure latest is at 100%
  • otherwise Cloudflare is not supposed to serve some random old code branch

Best next step

Change the real relay temporarily so it wraps the upstream response in JSON instead of passing it through raw:

const upstream = await fetch(`${env.OPENCLAW_ORIGIN_URL}/hooks/agent`, {
  method: "POST",
  headers: {
#
    "content-type": "application/json",
    "authorization": `Bearer ${env.OPENCLAW_HOOK_TOKEN}`,
    ...(env.CF_ACCESS_CLIENT_ID && env.CF_ACCESS_CLIENT_SECRET
      ? {
          "CF-Access-Client-Id": env.CF_ACCESS_CLIENT_ID,
          "CF-Access-Client-Secret": env.CF_ACCESS_CLIENT_SECRET,
        }
      : {}),
  },
  body: JSON.stringify({
    message: body.message,
    agentId,
    timeoutSeconds: body.timeoutSeconds ?? 900,
  }),
});
#

const upstreamText = await upstream.text();

return new Response(JSON.stringify({
  where: "upstream",
  status: upstream.status,
  contentType: upstream.headers.get("content-type"),
  body: upstreamText,
}), {
  status: 200,
  headers: {
    "content-type": "application/json",
    "x-worker-version": "relay-debug-1",
  },
});
#

That will tell you immediately whether the 401 is from:

  • OpenClaw hooks auth
  • Cloudflare Access in front of the tunnel
  • some wrong origin URL

My bet

Your Worker is fine now.
The thing still failing is Worker -> origin, not Benest -> Worker.

south swan
#

@barren spoke
How do I configure a MyClaw agent (Benest) to call an external webhook URL?
I want Benest to be able to send POST requests to my Cloudflare Worker relay.
Is there a tool/webhook configuration in MyClaw dashboard for this?

barren spokeBOT
#

I’m going to verify whether there’s a documented outbound webhook/tool config path before I tell you to click some dashboard toggle that may not exist.

#

Short answer: I don’t see a documented “arbitrary outbound webhook” toggle in the MyClaw dashboard.

What I do see documented is:

  • MyClaw dashboard supports Skills installation/workspace files
  • OpenClaw hooks/webhooks are mainly inbound surfaces, meaning external systems call into OpenClaw, not the other way around

Docs:

The key distinction

If you want:

  • Cloudflare Worker -> OpenClaw: use hooks / webhooks
#
  • Benest -> Cloudflare Worker: that is an outbound tool call, not a hook

So for Benest to POST to your relay, the clean path is usually:

  1. Create/install a custom Skill for Benest
  2. That skill exposes a tool or instructions to call your relay
  3. Configure auth/token in the workspace or env available to that skill

So, is there a dashboard setting for this?

From the docs/public MyClaw material I could find: not an obvious built-in “Webhook URL” field for arbitrary outbound POSTs.

What MyClaw seems to push instead is:

  • upload files/credentials in the dashboard
  • install skills through the dashboard
  • let the agent gain capabilities through those skills
#

That matches the MyClaw tutorial language I found about “Skills” being how assistants connect to external services.

What I’d recommend

For your use case, I’d try one of these:

Option A, recommended

Build a small custom skill like benest_relay that tells the agent how to call your Worker.

Option B

If MyClaw cloud gives Benest access to a general HTTP/fetch/web tool already, use that and just point it at your Worker URL.

Option C

If neither is exposed in MyClaw cloud, this is probably a MyClaw product limitation / support question, not an OpenClaw config issue.

Important gotcha

#

Do not use OpenClaw hooks thinking that makes Benest able to call out.
hooks are for receiving inbound webhook requests.

If you want, I can sketch a minimal custom Skill layout for benest_relay next, either:

  1. HTTP/fetch-based, if MyClaw exposes an HTTP-capable tool
  2. exec/curl-based, if your hosted instance allows exec
south swan
#

@barren spoke
Can you sketch the minimal custom Skill layout for benest_relay?
HTTP/fetch-based, calling an external Cloudflare Worker URL.
The Worker expects:

  • POST request
  • Authorization: Bearer <token>
  • Content-Type: application/json
  • Body: {"agentId": "claude-local", "message": "<user message>", "timeoutSeconds": 900}

What files do I need to create, and where do I put them in the workspace?

barren spokeBOT
#
<workspace>/
  skills/
    benest_relay/
      SKILL.md
      relay.py

If you want it shared across all agents on that machine instead, use:

~/.openclaw/skills/benest_relay/

But for Benest only, prefer:

#
<workspace>/skills/benest_relay/

1) SKILL.md

---
name: benest_relay
description: Relay a user message to the Benest Cloudflare Worker, which forwards it to claude-local.
metadata: { "openclaw": { "os": ["linux", "darwin"], "requires": { "bins": ["python3"] } } }
---

# Benest Relay

Use this skill when the user wants to send a prompt to the Benest relay.
#

## Behavior

- Run the helper script at `{baseDir}/relay.py`
- Pass the user's message to that script
- The script will send:
  - `POST` to the configured Cloudflare Worker URL
  - `Authorization: Bearer <token>`
  - `Content-Type: application/json`
  - JSON body:
    `{"agentId":"claude-local","message":"<user message>","timeoutSeconds":900}`

## Rules

- Do not inline the bearer token in the prompt or command text
#
- Read `BENEST_RELAY_URL` and `BENEST_RELAY_TOKEN` from environment variables
- Return the worker response to the user
- If the worker returns an error, show the status code and response body

2) relay.py

#!/usr/bin/env python3
import json
import os
import sys
import urllib.request
import urllib.error
#
url = os.environ.get("BENEST_RELAY_URL")
token = os.environ.get("BENEST_RELAY_TOKEN")

if not url:
    print("Missing BENEST_RELAY_URL", file=sys.stderr)
    sys.exit(1)

if not token:
    print("Missing BENEST_RELAY_TOKEN", file=sys.stderr)
    sys.exit(1)

message = " ".join(sys.argv[1:]).strip()
if not message:
    message = sys.stdin.read().strip()
#
if not message:
    print("Missing message", file=sys.stderr)
    sys.exit(1)

payload = {
    "agentId": "claude-local",
    "message": message,
    "timeoutSeconds": 900,
}

req = urllib.request.Request(
    url,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {token}",
#
        "Content-Type": "application/json",
    },
    method="POST",
)

try:
    with urllib.request.urlopen(req, timeout=930) as resp:
        body = resp.read().decode("utf-8", errors="replace")
        print(body)
except urllib.error.HTTPError as e:
    err_body = e.read().decode("utf-8", errors="replace")
    print(f"HTTP {e.code}", file=sys.stderr)
    print(err_body, file=sys.stderr)
    sys.exit(1)
except Exception as e:
#
    print(str(e), file=sys.stderr)
    sys.exit(1)

3) Make the script executable

chmod +x <workspace>/skills/benest_relay/relay.py

4) Configure the env vars

Outside the workspace, in OpenClaw config:

#
{
  skills: {
    entries: {
      benest_relay: {
        enabled: true,
        env: {
          BENEST_RELAY_URL: "https://your-worker.your-subdomain.workers.dev",
          BENEST_RELAY_TOKEN: "YOUR_BEARER_TOKEN"
        }
      }
    }
  }
}

If you use skill allowlists, also expose it to the agent:

#
{
  agents: {
    defaults: {
      skills: ["benest_relay"]
    }
  }
}

Bottom line

For a minimal benest_relay, create:

  • <workspace>/skills/benest_relay/SKILL.md
  • <workspace>/skills/benest_relay/relay.py