#How to call external REST APIs (Sanity CMS) from an agent — best approach?

83 messages · Page 1 of 1 (latest)

lean orbit
#

DO i need to create a script and ask openclaw to run the script or is there any otherway ?

cosmic mistBOT
#

Short version: for arbitrary REST APIs, yes, the usual way is a small script or wrapper that OpenClaw runs via exec.

Built-in options:

  • web_fetch for simple public HTTP GET page fetching, not full REST clients, and it does not do JS. Docs: https://docs.openclaw.ai/tools/web-fetch
  • browser for JS-heavy sites, but not ideal as a generic API client

So for Sanity:

  • if you just need a simple public read, web_fetch might be enough
  • if you need auth headers, POST/PUT/PATCH, custom JSON handling, pagination, etc., use one of these:
    1. a small script + exec
    2. a custom skill/plugin
    3. an MCP/server wrapper if you want a cleaner long-term integration

exec docs: https://docs.openclaw.ai/tools/exec

If you want, I can sketch the smallest good pattern for Sanity, either as a quick script or as a reusable skill.

lean orbit
#

i want to make post call so webfetch wont work right ? and how to create a plugin like in plugin i can call the api or what i cant see docs saying it

cosmic mistBOT
#

Yep, correct, web_fetch is GET-only, so it is not the right tool for a POST call. Docs: https://docs.openclaw.ai/tools/web-fetch

For what you want, there are really 2 good paths:

  1. Small script + exec
    Fastest and simplest for one API integration.

  2. Plugin that registers a real tool
    Better if you want something reusable like sanity_post(...).

My honest recommendation: start with a script first. A plugin is nicer long-term, but heavier.

Important distinction

  • Skill = instructions for the agent
  • Plugin = actual code that adds a new tool/capability

So if you need to actually call a REST API, a plugin can do that, yes.

#

Minimal plugin shape

Docs: https://docs.openclaw.ai/plugins/building-plugins
Plugin install/config docs: https://docs.openclaw.ai/tools/plugin

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

export default definePluginEntry({
  id: "sanity-api",
  name: "Sanity API",
  description: "Adds a tool for Sanity POST requests",
  register(api) {
    api.registerTool({
      name: "sanity_post",
      description: "Send a POST request to Sanity",
#
      parameters: Type.Object({
        url: Type.String(),
        body: Type.Any(),
      }),
      async execute(_id, params) {
        const res = await fetch(params.url, {
          method: "POST",
          headers: {
            "content-type": "application/json",
            // auth header here
          },
          body: JSON.stringify(params.body),
        });

        const text = await res.text();
#
        return {
          content: [{ type: "text", text }],
        };
      },
    });
  },
});

Then install it

openclaw plugins install ./my-plugin
openclaw gateway restart

Practical advice

#
  • For auth tokens, do not pass them in chat
  • Put secrets in plugin config or env vars
  • If this is just one Sanity workflow, script + exec is way less painful

If you want, I can give you either:

  • a tiny script-based Sanity POST setup, or
  • a full minimal OpenClaw plugin folder you can copy-paste.
lean orbit
#

i want plugin thing so i cna reuse it explain step by step approach on how to do that

cosmic mistBOT
#

Yep, if you want the reusable version, make a tool plugin.

Here’s the clean step-by-step path I’d use.

1. Create a plugin folder

mkdir -p ~/my-sanity-plugin
cd ~/my-sanity-plugin

You’ll make these files:

my-sanity-plugin/
├── package.json
#
  "name": "@you/openclaw-sanity-plugin",
  "version": "0.0.1",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
      "minGatewayVersion": "2026.3.24-beta.2"
    }
  }
}

3. Add openclaw.plugin.json

#

This is the manifest. OpenClaw requires it.

{
  "id": "sanity-api",
  "name": "Sanity API",
  "description": "Custom Sanity REST tool",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "baseUrl": { "type": "string" },
      "token": { "type": "string" }
    },
    "required": ["baseUrl", "token"]
  },
#
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

export default definePluginEntry({
  id: "sanity-api",
  name: "Sanity API",
  description: "Custom Sanity REST tool",
  register(api) {
    api.registerTool(
      {
        name: "sanity_post",
        description: "Send a POST request to the configured Sanity API",
        parameters: Type.Object({
          path: Type.String({ description: "Path appended to baseUrl" }),
          body: Type.Any({ description: "JSON body to send" })
#
        }),
        async execute(_id, params) {
          const cfg = api.pluginConfig as {
            baseUrl?: string;
            token?: string;
          };

          if (!cfg.baseUrl || !cfg.token) {
            throw new Error(
              "Missing plugin config. Set plugins.entries.sanity-api.config.baseUrl and token"
            );
          }

          const res = await fetch(`${cfg.baseUrl}${params.path}`, {
            method: "POST",
#
            headers: {
              "content-type": "application/json",
              authorization: `Bearer ${cfg.token}`
            },
            body: JSON.stringify(params.body)
          });

          const text = await res.text();

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
#
                    ok: res.ok,
                    status: res.status,
                    body: text
                  },
                  null,
                  2
                )
              }
            ]
          };
        }
      },
      { optional: true }
    );
  }
#
});

Why optional: true? Because this tool does writes, and the docs recommend optional tools for side-effecting stuff.


5. Install the plugin locally for development

Use the local link mode while iterating:

openclaw plugins install -l ~/my-sanity-plugin

Then restart:

#
openclaw gateway restart

6. Enable it in config

Add this in your openclaw.json:

{
  plugins: {
    entries: {
      "sanity-api": {
        enabled: true,
#
        config: {
          baseUrl: "https://YOUR_PROJECT.api.sanity.io/v2025-02-19",
          token: "YOUR_SANITY_TOKEN"
        }
      }
    }
  },
  tools: {
    allow: ["sanity_post"]
  }
}

Because the tool is optional, you must allow it.


#

7. Restart the gateway again

openclaw gateway restart

If the current chat session does not see the tool immediately, start a new session/thread.


8. Test it

Now ask the agent something like:

Use sanity_post to call /data/mutate/production with this JSON body: …

That should make the agent call your plugin tool instead of trying web_fetch.

#

9. Improve it once the first version works

After the basic version works, add:

  • sanity_get
  • better error handling
  • typed params instead of Type.Any()
  • separate tools for mutate/query/assets
  • env/secret handling instead of plain token in config

10. Publish later if you want reuse across machines

When it works locally, you can publish it to npm or ClawHub so openclaw plugins install ... works anywhere.

#

If you want, I can give you a Sanity-specific version next, for example:

  • sanity_query
  • sanity_mutate
  • sanity_create_document

That would be a nicer plugin than one generic sanity_post tool.

lean orbit
#

then in skill do i need to emntion call this tool or something like tha

cosmic mistBOT
#

Yep, if you add a skill, the skill should mention when to use the tool.

But important bit:

  • Plugin = creates the tool
  • Skill = teaches the agent when/how to use it

So:

  • if your plugin registers sanity_post, the agent can already use it
  • a skill is optional, but it usually makes usage much more reliable

Minimal example skill

---
#
name: sanity_helper
description: Use the Sanity tool for content mutations and API requests.
---

When the user asks to create, update, publish, or mutate content in Sanity, use the `sanity_post` tool.

Guidelines:
- Use `/data/mutate/production` for mutations unless the user specifies another dataset/path.
- Build a valid JSON body before calling the tool.
- If required info is missing, ask for it before calling the tool.
- After the tool returns, summarize the result clearly.

So do you need a skill?

  • No, not strictly
  • Yes, if you want the agent to use your tool consistently and correctly
lean orbit
#

yes

cosmic mistBOT
#

Nice. Here’s the smallest useful plugin + skill combo I’d use for Sanity.

1. Plugin, adds a real tool

Create a folder like ~/my-sanity-plugin.

package.json

{
  "name": "@you/openclaw-sanity-plugin",
  "version": "0.0.1",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
#
      "minGatewayVersion": "2026.3.24-beta.2"
    }
  }
}

openclaw.plugin.json

{
  "id": "sanity-api",
  "name": "Sanity API",
  "description": "Sanity mutation tool",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
#
    "properties": {
      "baseUrl": { "type": "string" },
      "token": { "type": "string" },
      "dataset": { "type": "string" }
    },
    "required": ["baseUrl", "token"]
  },
  "uiHints": {
    "token": {
      "label": "Sanity token",
      "sensitive": true
    }
  }
}
#

index.ts

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

function tryParse(text: string) {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

export default definePluginEntry({
  id: "sanity-api",
  name: "Sanity API",
#
  description: "Sanity mutation tool",
  register(api) {
    api.registerTool(
      {
        name: "sanity_mutate",
        description: "Send mutations to Sanity CMS",
        parameters: Type.Object({
          dataset: Type.Optional(Type.String()),
          mutations: Type.Array(Type.Any(), { minItems: 1 })
        }),
        async execute(_id, params) {
          const cfg = api.pluginConfig as {
            baseUrl?: string;
            token?: string;
            dataset?: string;
#
          };

          if (!cfg.baseUrl || !cfg.token) {
            throw new Error(
              "Missing plugin config. Set plugins.entries.sanity-api.config.baseUrl and token"
            );
          }

          const dataset = params.dataset ?? cfg.dataset ?? "production";

          const res = await fetch(`${cfg.baseUrl}/data/mutate/${dataset}`, {
            method: "POST",
            headers: {
              "content-type": "application/json",
              authorization: `Bearer ${cfg.token}`
#
            },
            body: JSON.stringify({
              mutations: params.mutations
            })
          });

          const text = await res.text();

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    ok: res.ok,
#
                    status: res.status,
                    dataset,
                    body: tryParse(text)
                  },
                  null,
                  2
                )
              }
            ]
          };
        }
      },
      { optional: true }
    );
  }
#
});

2. Install the plugin locally

openclaw plugins install -l ~/my-sanity-plugin
openclaw gateway restart

3. Enable it in openclaw.json

#
{
  plugins: {
    entries: {
      "sanity-api": {
        enabled: true,
        config: {
          baseUrl: "https://YOUR_PROJECT.api.sanity.io/v2025-02-19",
          token: "YOUR_SANITY_TOKEN",
          dataset: "production"
        }
      }
    }
  },
  tools: {
    allow: ["sanity_mutate"]
#
  }
}

Because the tool is optional, you need tools.allow.


4. Skill, teaches the agent when to use it

In the workspace for that agent, create:

skills/sanity-helper/SKILL.md

#
---
name: sanity_helper
description: Use the sanity_mutate tool for Sanity CMS write operations.
---

When the user asks to create, update, patch, or delete content in Sanity CMS, use the `sanity_mutate` tool.

Rules:
- Prefer the configured dataset unless the user explicitly asks for another one.
- Build valid Sanity mutation objects before calling the tool.
- If required info is missing, ask for it first.
- After the tool returns, summarize whether it succeeded and include important ids or errors.

Common mutation shapes:
- Create:
#
  { "create": { "_type": "post", "title": "Hello" } }

- Create if not exists:
  { "createIfNotExists": { "_id": "post-123", "_type": "post", "title": "Hello" } }

- Patch:
  { "patch": { "id": "post-123", "set": { "title": "Updated title" } } }

- Delete:
  { "delete": { "id": "post-123" } }

Then restart or start a new session so the skill is picked up.


#

5. How it works together

  • plugin creates sanity_mutate
  • skill tells the model when to call sanity_mutate

So yes, the skill should explicitly mention the tool name.


6. Example prompt to test

Try asking the agent:

Create a Sanity post titled "Hello World" with _id post-1

The skill should push it toward calling sanity_mutate.

#

If you want, next I can give you a better v2 with:

  • sanity_query
  • sanity_mutate
  • typed params instead of Type.Any()
  • safer auth via env/secret refs
lean orbit
#

wha tis the other option we need to run the script and ask openclaw to run those script or what

cosmic mistBOT
#

Yep, there are 2 different approaches:

Option 1, script + exec

You write a script, for example sanity.js or sanity.py, and then the agent runs it with the exec tool.

Flow:

  • user asks for a Sanity action
  • skill says “use the script”
  • OpenClaw runs the script
  • script calls the API

So in this option, yes, OpenClaw is basically running your script.


Option 2, plugin tool

#

You do not run a separate script.

Instead:

  • you build a plugin
  • plugin registers a tool like sanity_mutate
  • agent calls that tool directly
  • the tool code does the API request inside OpenClaw

So here the flow is:

  • user asks for a Sanity action
  • skill says “use sanity_mutate
  • OpenClaw calls the plugin tool directly

No external script needed.

#

Short version

  • Script option = “make a script and let OpenClaw run it”
  • Plugin option = “make a tool and let OpenClaw call it directly”

Since you said you want something reusable, plugin is the cleaner option.

If you want, I can draw the exact flow for both in one tiny example so it clicks immediately.

lean orbit
#

ok ok tell me what all i nee to do with this plugin

cosmic mistBOT
#

Yep, if you go the plugin route, this is the full checklist:

What you need to do

  1. Create a plugin folder

    • package.json
    • openclaw.plugin.json
    • index.ts
  2. In index.ts, register a tool

    • example: sanity_mutate
    • that tool does the fetch(..., { method: "POST" })
  3. Install the plugin into OpenClaw

    openclaw plugins install -l /path/to/your/plugin
    
#
  1. Enable the plugin in openclaw.json

    • add plugins.entries.sanity-api.enabled: true
    • add your config like baseUrl, token, dataset
  2. Allow the tool

    • if you made it optional, add:
    tools: {
      allow: ["sanity_mutate"]
    }
    
  3. Add a skill

    • create skills/sanity-helper/SKILL.md
    • tell the agent:
      • when to use sanity_mutate
      • what kind of requests it should build
#
  1. Restart the gateway

    openclaw gateway restart
    
  2. Start a new session/thread

    • so the agent picks up the plugin + skill cleanly
  3. Test it

    • ask something like:
      • “Create a Sanity post with title X”
      • “Update document Y in Sanity”
  4. Later, improve/publish it

  • add more tools like sanity_query
  • publish to npm/ClawHub if you want reuse across machines
#

So the minimum pieces are

  • plugin code
  • plugin config
  • tool allowlist if optional
  • skill
  • restart
  • test

And no, with this route you do not need a separate script.

If you want, I can make it even simpler and give you a super short 5-file starter template you can copy exactly.

lean orbit
#

i have amin skill can i say in that only to use this plugin why to create other skill

cosmic mistBOT
#

Yes, absolutely, you can put it in your existing admin skill.

You do not need a separate skill if:

  • that skill is already relevant
  • you’re okay keeping Sanity instructions inside it

So the setup can be:

  • plugin registers sanity_mutate
  • your existing admin skill says when to use sanity_mutate

Why I mentioned a separate skill

Only for cleanliness:

  • separate skill = easier to reuse
  • separate skill = less clutter in your admin skill
#
  • separate skill = easier to debug later

But functionally, your current skill is fine.

What to add in your admin skill

Something like:

When the user asks to create, update, patch, or delete content in Sanity CMS, use the `sanity_mutate` tool.

Prefer the configured dataset unless the user explicitly asks for another one.
Ask for missing ids/fields before calling the tool.
Summarize the result after the tool returns.

So yeah, short answer:

#
  • plugin is still needed
  • new skill is optional
  • you can reuse your existing admin skill

After editing the skill, start a new session so OpenClaw reloads it.

lean orbit
#

When I run openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin I get this error:
Config validation failed: plugins.entries.sanity-api.config.baseUrl: invalid config: must have required property 'baseUrl'
I haven't added the plugin to openclaw.json yet — do I need to add the config there before running the install command? Or should the install work without config first?

cosmic mistBOT
#

Short answer: the install is failing because your plugin schema says baseUrl is required.

So right now OpenClaw is validating plugins.entries.sanity-api.config, sees no baseUrl, and stops.

What I’d do

Best fix

Make the plugin config optional in the manifest, and check it at runtime inside the tool instead.

So change openclaw.plugin.json from this:

"required": ["baseUrl", "token"]

to no required array at all, like this:

#
{
  "id": "sanity-api",
  "name": "Sanity API",
  "description": "Sanity mutation tool",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "baseUrl": { "type": "string" },
      "token": { "type": "string" },
      "dataset": { "type": "string" }
    }
  }
}
#

Then keep your runtime check in index.ts:

if (!cfg.baseUrl || !cfg.token) {
  throw new Error("Missing plugin config...");
}

That way:

  • plugin installs cleanly
  • config can be added after
  • tool fails only when actually used without config

Then do this order

#
  1. loosen the manifest schema
  2. run:
    openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
    
  3. add config to openclaw.json
  4. restart:
    openclaw gateway restart
    

Alternative

If you want to keep baseUrl and token required in schema, then yes, you need the config present before the plugin can validate cleanly.

But honestly, for a local dev plugin, I’d make them optional in schema.

#

One more thing

You may already have a half-created plugins.entries.sanity-api entry now. If it keeps failing even after changing the manifest, check that section too.

If you want, paste:

  • openclaw --version
  • openclaw config get plugins.entries.sanity-api

and I’ll tell you exactly what state it’s in.

lean orbit
#

"I'm getting Cannot find module 'openclaw/plugin-sdk/plugin-entry' — what is the correct import path for definePluginEntry and @sinclair/typebox on a Lightsail OpenClaw install?"

cosmic mistBOT
#

The correct import path is already:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

So the error is probably not the path, it is usually one of these:

Most likely fix

For a standalone plugin, put both in dependencies, not devDependencies:

{
  "dependencies": {
#
    "openclaw": "YOUR_OPENCLAW_VERSION",
    "@sinclair/typebox": "^0.34.0"
  }
}

Then install them in the plugin folder with:

cd ~/.openclaw/plugins/sanity-plugin
pnpm add openclaw @sinclair/typebox

Then reinstall/link the plugin again:

#
openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
openclaw gateway restart

Why this happens

OpenClaw docs say plugin installs use:

  • npm install --omit=dev --ignore-scripts

So if openclaw or @sinclair/typebox are only in devDependencies, runtime cannot see them.

One more important detail

Set the openclaw package version to match your installed OpenClaw as closely as possible.

lean orbit
#

OpenClaw 2026.3.23 (ccfeecb)

cosmic mistBOT
#

Ah, that helps, you’re on 2026.3.23, and the example I gave was from newer docs. So first thing, change your plugin package to match your installed OpenClaw version.

Use this in package.json

{
  "name": "@you/openclaw-sanity-plugin",
  "version": "0.0.1",
  "type": "module",
  "dependencies": {
    "openclaw": "2026.3.23",
    "@sinclair/typebox": "^0.34.0"
  },
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
#
      "pluginApi": ">=2026.3.23",
      "minGatewayVersion": "2026.3.23"
    }
  }
}

Import lines should stay

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";

Then run

#
cd ~/.openclaw/plugins/sanity-plugin
pnpm install
openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
openclaw gateway restart

Important

The earlier 2026.3.24-beta.2 compat block is wrong for your box, so remove that.

If it still says module not found

Then your installed 2026.3.23 build likely does not expose that SDK subpath cleanly, and the easiest fix is to update OpenClaw to a newer version that matches the docs examples.

If you want, paste your current package.json and I’ll sanity-check it before you retry.

lean orbit
#

do i need to run pnpm install here

cosmic mistBOT
#

Not strictly, no.

If you are using:

openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin

then OpenClaw should install the plugin’s runtime dependencies itself.

So the normal flow is just:

openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
openclaw gateway restart
#

When pnpm install helps

Only as a local debugging step if you want to check the plugin folder itself.

So:

  • normal usage: no, you usually do not need pnpm install
  • debugging/dev: you can run it in the plugin folder

If you’re still getting Cannot find module, the more likely issue is:

  • wrong package.json
  • wrong OpenClaw version match
  • missing dependency declaration in dependencies

If you want, paste your current package.json and I’ll check it quickly.

lean orbit
#

GNU nano 7.2 /home/ubuntu/.openclaw/plugins/sanity-plugin/package.json
{
"name": "sanity-api",
"version": "0.0.1",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
}
}
}
check if this works

cosmic mistBOT
#

Not yet, I’d change it.

Problems in that file:

  1. wrong compat version for your box
    You’re on 2026.3.23, not 2026.3.24-beta.2

  2. missing dependencies
    You import:

    • openclaw/plugin-sdk/plugin-entry
    • @sinclair/typebox

    so those need to be in dependencies

Use this instead:

#
{
  "name": "sanity-api",
  "version": "0.0.1",
  "type": "module",
  "dependencies": {
    "openclaw": "2026.3.23",
    "@sinclair/typebox": "^0.34.0"
  },
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.23",
      "minGatewayVersion": "2026.3.23"
    }
  }
#
}

Then run:

openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
openclaw gateway restart

So, short answer: the current package.json will probably not work.

If you want, paste index.ts next and I’ll check that too.

lean orbit
#

ubuntu@ip-172-26-13-239:~$ openclaw plugins install -l ~/.openclaw/plugins/sanity-plugin
13:14:11 [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: sanity-api (/home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts). Set plugins.allow to explicit trusted ids.
13:14:11 [plugins] sanity-api failed to load from /home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts: Error: Cannot find module 'openclaw/plugin-sdk/plugin-entry'
Require stack:

  • /home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts
    咽 OpenClaw 2026.3.23 (ccfeecb) — Shell yeah—I'm here to pinch the toil and leave you the glory.
    13:14:12 [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: sanity-api (/home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts). Set plugins.allow to explicit trusted ids.
    13:14:12 [plugins] sanity-api failed to load from /home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts: Error: Cannot find module 'openclaw/plugin-sdk/plugin-entry'
    Require stack:
  • /home/ubuntu/.openclaw/plugins/sanity-plugin/index.ts
    Confi-> f21, backup=/home/ubuntu/.openclaw/openclaw.json.bak)
    Linked plugin path: ~/.openclaw/plugins/sanity-plugin
    Restart the gateway to load plugins. why am i seeing sha tag again and again
cosmic mistBOT
#

The ccfeecb bit is just the OpenClaw commit hash in the CLI banner, not the problem.

The real problem is this:

You put the plugin in an auto-discovery folder

You’re linking this path:

~/.openclaw/plugins/sanity-plugin

But ~/.openclaw/plugins/... is already a place OpenClaw scans for plugins.

So what’s happening is:

  1. OpenClaw sees ~/.openclaw/plugins/sanity-plugin/index.ts
#
  1. It tries to auto-load it directly
  2. It fails on openclaw/plugin-sdk/plugin-entry
  3. Then your plugins install -l ... command continues and you see more logs

That’s why it looks repetitive.

What to do instead

Move the plugin source out of ~/.openclaw/plugins/.

For example:

mkdir -p ~/Projects
mv ~/.openclaw/plugins/sanity-plugin ~/Projects/sanity-plugin
#

Then link that:

openclaw plugins install -l ~/Projects/sanity-plugin
openclaw gateway restart

Also set an allowlist

That warning is separate, but worth fixing:

{
  plugins: {
    allow: ["sanity-api"]
  }
}