#My custom skill dont load

1 messages · Page 1 of 1 (latest)

peak latch
left deltaBOT
wary ironBOT
#

First, paste openclaw --version.

I checked the docs/source, and the main gotcha is this:

  1. triggers: is not a supported OpenClaw skill-matching feature
    OpenClaw does not regex-match frontmatter triggers to decide skill use. The model only gets a compact list of:

    • name
    • description
    • location

    Then it decides whether to read the SKILL.md.

  2. openclaw skills showing ready only means the skill loaded successfully
    It does not guarantee the current chat session actually included that skill in the prompt.

So in your case, the likely problems are:

#
  • the big triggers: block is being ignored
  • the skill description is doing too much
  • if you use agents.defaults.skills / agents.list[].skills, the skill may not be visible to that agent
  • if the skills prompt gets compacted, descriptions can be omitted, and then max-banking no longer obviously matches Qual meu saldo

What I’d change first:

---
name: max_banking
description: Consultar saldo do Max Bank, ver conta conectada, fazer PIX e pagar boleto.
---

Then keep all the workflow details in the body.

Also, don’t hardcode this path:

#
$HOME/.openclaw/workspace/skills/max-banking/...

Use {baseDir} in the skill instead, per docs.

For a deterministic test, try a direct skill invocation instead of relying on natural-language matching:

  • /skill max-banking qual meu saldo
  • or the generated slash command, probably /max_banking

Please send these outputs:

openclaw --version
openclaw skills list --eligible --verbose
peak latch
#

openclaw --version
OpenClaw 2026.4.14 (323493f)

openclaw skills list --eligible --verbose
appears ready

openclaw skills info max-banking --json
{
"name": "max-banking",
"description": "Ações financeiras da conta Max Bank — saldo, PIX (chave e QR copia e cola), boleto e conexão de conta. Acione proativamente ao receber códigos PIX (00020...), linhas digitáveis de boleto ou imagens de QR/código de barras.",
"source": "openclaw-workspace",
"bundled": false,
"filePath": "/home/jonatan/.openclaw/workspace/skills/max-banking/SKILL.md",
"baseDir": "/home/jonatan/.openclaw/workspace/skills/max-banking",
"skillKey": "max-banking",
"always": true,
"disabled": false,
"blockedByAllowlist": false,
"eligible": true,
"requirements": {
"bins": [],
"anyBins": [],
"env": [],
"config": [],
"os": []
},
"missing": {
"bins": [],
"anyBins": [],
"env": [],
"config": [],
"os": []
},
"configChecks": [],
"install": []
}

wary ironBOT
#

Nice, this rules out the loader 👍

Your skill is definitely loading:

  • eligible: true
  • source: openclaw-workspace
  • always: true

So the issue is routing, not discovery.

What’s happening today:

  • always: true = “keep this skill eligible”, not “always read this SKILL.md”
  • triggers: is ignored for chat routing
  • free-form messages like Qual meu saldo still go through the model’s skill chooser
  • the chooser only gets the skill name + description + location, then decides whether to read the file
#

So yes, this part is expected today: a skill can be ready and still not be selected for a natural-language message.

What I’d do:

  1. Shorten the description a lot
    Your current one is too broad. Make it literal.

    Example:

    ---
    name: max_banking
    description: Consultar saldo do Max Bank, ver conta conectada, fazer PIX e pagar boleto.
    ---
    
  2. Use {baseDir} instead of a hardcoded workspace path

#

Safer than $HOME/.openclaw/workspace/skills/max-banking/...

  1. Test with direct invocation

    • /skill max-banking qual meu saldo
    • or /max_banking

    If that works, the skill itself is fine, and only the natural-language matching is flaky.

  2. Check /context in a fresh /new
    If max-banking appears in available_skills, then OpenClaw is exposing it correctly and this is just model selection behavior.

The key point is: there is no hard trigger engine for plain chat messages yet.
If you want Qual meu saldo to deterministically route to that skill every time, that’s a feature request, not something triggers: currently implements.

Docs:

#

If you want, paste your reduced SKILL.md frontmatter and I’ll help tighten it so matching works better.

peak latch
#

\U0001f9e0 Context breakdown
Workspace: /home/jonatan/.openclaw/workspace
Bootstrap max/file: 20,000 chars
Bootstrap max/total: 150,000 chars
Sandbox: mode=off sandboxed=false
System prompt (run): 23,187 chars (~5,797 tok) (Project Context 14,099 chars (~3,525 tok))

Injected workspace files:

AGENTS.md: OK | raw 7,727 chars (~1,932 tok) | injected 7,727 chars (~1,932 tok)
SOUL.md: OK | raw 1,738 chars (~435 tok) | injected 1,738 chars (~435 tok)
TOOLS.md: OK | raw 850 chars (~213 tok) | injected 850 chars (~213 tok)
IDENTITY.md: OK | raw 633 chars (~159 tok) | injected 633 chars (~159 tok)
USER.md: OK | raw 474 chars (~119 tok) | injected 474 chars (~119 tok)

Skills list (system prompt text): 4,826 chars (~1,207 tok) (9 skills)
Skills: clawhub, healthcheck, mcporter, node-connect, skill-creator, taskflow, taskflow-inbox-triage, weather, max-banking
Tool list (system prompt text): 0 chars (~0 tok)
Tool schemas (JSON): 9,907 chars (~2,477 tok) (counts toward context; not shown as text)
Tools: read, edit, write, apply_patch, exec, process, image_generate, video_generate, sessions_yield, web_fetch, image, memory_search, memory_get

Session tokens (cached): 13,289 total / ctx=200,000

Inline shortcut: a command token inside normal text (e.g. \u201chey /status\u201d) that runs immediately (allowlisted senders only) and is stripped before the model sees the remaining message.

wary ironBOT
#

Perfect, this /context output is the smoking gun.

It proves all of this:

  • max-banking is in the system prompt
  • the skills section is not truncated
  • there are only 9 skills, so this is not a compact/overflow issue
  • the problem is not loading

So what’s left is:

the model saw the skill and still chose not to read SKILL.md

That means this is a skill-routing / skill-adherence problem, not a loader problem.

A few important clarifications:

#
  • always: true only means “keep this skill eligible”
  • triggers: still does nothing for chat routing
  • plain text like Qual meu saldo is still best-effort model selection, not a hard trigger

Best next test, and the one I’d trust most:

1) Force only this skill for a test agent

In ~/.openclaw/openclaw.json:

{
  agents: {
    defaults: {
      skills: ["max-banking"]
    }
  }
#
}

Then /new again.

If it still ignores max-banking when it’s the only visible skill, that looks like a real bug.

2) Also test direct invocation

  • /skill max-banking qual meu saldo
  • or /max_banking

If direct invocation works but plain chat does not, that confirms the issue is only the natural-language router.

3) Tighten the skill metadata

I’d still shorten it to something like:

#
---
name: max_banking
description: Consultar saldo do Max Bank, ver conta conectada, fazer PIX e pagar boleto.
---

And use {baseDir} instead of hardcoding the skill path.


At this point, if:

  • /context shows the skill, and
  • it’s the only allowed skill, and
  • Qual meu saldo still does not make the agent read it
#

then I’d call that a reproducible bug and open an issue here:

https://github.com/openclaw/openclaw/issues

Include:

  • openclaw --version
  • openclaw skills info max-banking --json
  • the /context output
  • exact message: Qual meu saldo

That would be a very solid bug report.

peak latch
wary ironBOT
#

Yep, this version is better.

My read is:

  • if your openclaw.json change was a skill allowlist like agents.defaults.skills: ["max-banking"], that probably had the biggest impact on reliability
  • your new SKILL.md also helped, especially the shorter, more literal description

So, likely both helped, but the config change is usually the stronger lever.

Quick review

Good changes

  • The description is much better for routing now.
  • You stopped relying on triggers: for matching, which was the main trap.
  • The skill intent is much clearer.

I’d still change these

#
  1. Use snake_case for name

    name: max_banking
    

    Docs expect snake_case, and slash commands stay cleaner.

  2. Use single-line JSON for metadata
    Your current parser may accept it, but the documented format is still:

    metadata: { "openclaw": { ... } }
    
  3. Use {baseDir} instead of hardcoding the workspace path
    Right now you still have:

    $HOME/.openclaw/workspace/skills/max-banking/scripts/maxbank.sh
    
#

Better:

bash {baseDir}/scripts/maxbank.sh ...
  1. Homepage looks wrong
    I see:
    homepage: https://github.com/maxter/mcp-payment-skill
    
    If this skill lives in your repo, I’d point it to your repo instead.

Biggest remaining concern

This one matters a lot because it’s a payments skill:

You are still building exec shell commands from user-provided values like:

  • PIX key
#
  • QR payload
  • boleto line
  • URLs
  • image paths

That is the main risk now. Even with quoting guidance, shell interpolation is fragile. For something financial, I’d much rather see:

  • a small wrapper that accepts structured args safely, or
  • a real custom tool/plugin instead of raw shell composition

So overall: much better, but I would still fix:

  • name
  • metadata format
  • {baseDir}
  • homepage
  • shell-safety around user input

Docs:

peak latch
#

I tried to use {baseDir} but sometimes the skill try to execute {baseDir}/scripts/maxbank.sh and got error

wary ironBOT
#

Yep, that makes sense.

{baseDir} is not shell expansion. It’s just a convention in the skill text, and the model is supposed to substitute it. Sometimes it fails and sends it literally, which is exactly the error you saw.

So for reliability, I would not put {baseDir} directly inside the shell command.

Better pattern

Write it like this in the skill:

The script is at `scripts/maxbank.sh`, relative to this SKILL.md.

Before calling `exec`, resolve `scripts/maxbank.sh` against the skill directory and use the absolute path in the command.

Never pass `{baseDir}` literally to the shell.
#

Then your examples can say:

bash <resolved-absolute-path>/scripts/maxbank.sh saldo

That works better because OpenClaw already tells the model to resolve relative paths against the skill directory.

Practical advice

  • Portable/published skill: use the relative-path wording above
  • Single local deployment: hardcoding the absolute path is honestly more reliable than a flaky {baseDir} placeholder

So if your goal is “works every time on my machine”, the hardcoded path is fine.
If your goal is “publishable skill”, use relative path + explicit resolve instruction, not raw {baseDir} in the shell command.

Long-term, the cleanest fix is still a small wrapper/custom tool so the model never has to build shell paths at all.

peak latch
#

Could you give a simple example of skill with .sh execution?

example:
scripts/api.sh => simple get
SKILL.md

wary ironBOT
#

Yep, here’s a minimal safe example.

Folder

my-api-skill/
  SKILL.md
  scripts/
    api.sh

scripts/api.sh

#!/usr/bin/env bash
set -euo pipefail

curl -fsSL "https://httpbin.org/get"
#

You can run it with bash scripts/api.sh, so chmod +x is optional.


SKILL.md

---
name: api_demo
description: Call a simple HTTP GET script when the user asks to test the demo API.
metadata: { "openclaw": { "requires": { "bins": ["curl"] } } }
---

# API Demo Skill

Use this skill when the user asks to test the demo API or fetch the demo API response.
#
The script is at `scripts/api.sh`, relative to this SKILL.md.

Before calling `exec`, resolve `scripts/api.sh` against the skill directory and use the absolute path in the command.

Never pass `{baseDir}` literally to the shell.

## How to run

Execute:

```bash
bash <resolved-absolute-path>/scripts/api.sh

Response

#

- If the command succeeds, summarize the result briefly for the user.
- If it fails, tell the user the API request failed and include the error briefly.
- Do not invent response fields that were not returned.

Why this pattern is better

  • uses relative path
  • avoids raw {baseDir} in the shell command
  • keeps frontmatter simple
  • avoids passing user input into shell

If you want, I can also give you a version with scripts/api.sh <url> so the skill accepts a URL parameter.

peak latch
#

give me a version with a URL parameter

wary ironBOT
#

Sure, here’s a simple version with a URL parameter.

scripts/api.sh

#!/usr/bin/env bash
set -euo pipefail

URL="${1:-}"

if [[ -z "$URL" ]]; then
  echo "Usage: api.sh <url>" >&2
  exit 2
fi

case "$URL" in
  http://*|https://*) ;;
#
  *)
    echo "URL must start with http:// or https://" >&2
    exit 2
    ;;
esac

curl -fsSL --max-time 20 "$URL"

SKILL.md

---
name: api_demo_url
#
description: Fetch a URL with a shell script when the user asks to test or fetch an API endpoint.
metadata: { "openclaw": { "requires": { "bins": ["curl"] } } }
---

# API Demo URL Skill

Use this skill when the user asks to fetch or test a URL.

The script is at `scripts/api.sh`, relative to this SKILL.md.

Before calling `exec`, resolve `scripts/api.sh` against the skill directory and use the absolute path in the command.

Never pass `{baseDir}` literally to the shell.

## Rules
#

- If the user did not provide a URL, ask for it.
- Only pass one URL argument to the script.
- Always pass the URL as a single quoted shell argument.
- Do not invent or rewrite the URL.

## How to run

Example:

```bash
bash <resolved-absolute-path>/scripts/api.sh 'https://httpbin.org/get'

Response

#

- If it succeeds, summarize the response briefly.
- If it fails, tell the user the request failed and show the short error.

Small safety note

For a real production skill, I’d strongly recommend an allowlist of domains instead of arbitrary URLs, to avoid SSRF trouble.

If you want, I can also give you:

  1. a GET + headers version, or
  2. a GET + JSON parsing (jq) version.
peak latch
wary ironBOT
#

Yep, this is much better. I’d call it mostly solid now 👍

What improved

  • name: max_banking âś…
  • homepage now points to your repo âś…
  • metadata is now single-line JSON âś…
  • the execution section is much clearer than before âś…

I only see a few things left

  1. You still have one contradiction about transferĂŞncia
    In Workflow 2 and Workflow 7, transferir / transferĂŞncia = PIX.
    But in the “Ação inexistente” examples section, you still have:

    "quero ver meu extrato" / "fazer saque" / "transferĂŞncia"

    and that response says it is unavailable.

#

I’d remove transferência from that example, otherwise the model gets conflicting instructions.

  1. setup.sh looks like a typo
    In Workflow 4 you wrote:

    Produção: o setup.sh usa o MCP...

    But the skill uses maxbank.sh setup, not setup.sh.

    I’d change that wording to avoid confusion.

  2. <resolved> is better than {baseDir}, but it is still a placeholder
    Some models may still paste it literally.

    So I’d slightly reword that section to say:

    • resolve scripts/maxbank.sh to an absolute path first
#
  • then execute the final command with that absolute path

    • the examples below are shape examples, not literal text

    That reduces the chance of the model sending <resolved> literally.

Nice-to-have, not blockers

  • The description is still a bit long, but it’s fine now.
  • The biggest long-term risk is still shell safety with user-provided values, especially QR payload / boleto / URL. For a payments skill, a custom tool would still be safer than shell composition.

Verdict

I think this version is good enough to use, with those 2-3 cleanup tweaks.

If you want, I can rewrite the exact 3 small text fixes for you, ready to paste into SKILL.md.

peak latch
#

I want a simple setup do you understand?

That's why I have an setup.sh to install all depencies and config the openclaw.json could you review?

https://github.com/appmaxbrasil/openclaw-max-banking-skill/blob/fix/skill-md-robustness/scripts/setup.sh

I want for example:

1 - My client enters on my bank app, and install a agent operator
2 - Receive a code
3 - Install the skill on workspace/skills/
4 - Restart the gateway and /new session and just type
5 - "Quero conectar minha conta do max cĂłdigo ABCD-1234"
6 - execute the setup.sh and configure the bank account and mcporter.

wary ironBOT
#

Yeah, I get the UX you want:

  • install once
  • /new
  • user says “Quero conectar minha conta do max cĂłdigo ABCD-1234”
  • skill finishes the connection

That’s a good goal.

My honest review of setup.sh

Right now it is trying to do two different jobs at once:

  1. installer/bootstrapper
    • install mcporter
    • install zbar
    • copy the skill
#
  • rewrite openclaw.json
  1. runtime account connection
    • exchange pairing code
    • save secrets
    • configure mcporter with the agent key

I think that is the main design problem.

For the flow you described, the chat-triggered path should do only #2.


The biggest problems in the current script

1) openclaw.json editing is risky

You are doing:

#
JSON.parse(fs.readFileSync(p, 'utf8'))

But openclaw.json is typically JSON5, not strict JSON.

So if the file has comments, trailing commas, or unquoted keys, your parse fails and you silently fall back to {}. That can wreck the user’s config.

I would not edit openclaw.json this way.

2) openclaw skill add steipete/mcporter looks wrong

The documented CLI is openclaw skills ..., not openclaw skill add ....

So that line is probably dead code hidden by || true.

3) Don’t set global tools.allow from a skill installer

This is too invasive:

#
cfg.tools.allow = ...

Once you set tools.allow, you are creating a real allowlist baseline. That can accidentally hide other tools and surprise the user.

For a “simple setup”, I would avoid touching tools.allow unless absolutely necessary.

4) Don’t install OS packages in the chat-time connect flow

This part:

  • brew install zbar
  • sudo apt-get install ...
  • dnf, pacman, etc.

is fine for a manual installer, but not for the “user types one message” flow.

#

That flow will be much more reliable if all dependencies are already there.

5) SECRETS_DIR ignores OPENCLAW_HOME

You compute:

OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
SECRETS_DIR="$HOME/.openclaw/secrets/maxbank"

That should probably use OPENCLAW_HOME, otherwise custom state dirs break.


What I’d recommend instead

Keep two scripts

#

A) install.sh or onboarding step, run once

This can:

  • install mcporter
  • optionally install zbar
  • place the skill
  • maybe print next steps

But I would not have the skill call this during normal chat use.

B) maxbank.sh setup ... for the actual skill flow

When the user says:

“Quero conectar minha conta do max código ABCD-1234”

this should only:

  • parse code/environment
  • call connect-mcp.js
#
  • save secrets
  • configure mcporter with the received token

That is the right script for the skill to run.


Best-fit flow for your product

I’d make it:

  1. Client installs the skill once
  2. Client installs deps once
  3. Gateway restart + /new
  4. User says:
    “Quero conectar minha conta do max código ABCD-1234”
  5. Skill runs only the pairing/config path
#

That gives you the simple UX you want, without doing package installs and global config mutations from chat.


My recommendation in one sentence

Use setup.sh as a one-time installer, and use maxbank.sh setup as the chat-triggered connection flow.

That will be much simpler and much more reliable.

If you want, I can help you redesign it into:

  • install.sh
  • maxbank.sh setup
  • minimal SKILL.md wording for the connection flow

That split would clean this up a lot.

peak latch
#

Please giveme a prompt, so I will instruct the Claudecode to do the refactor on install

#

When I just have for example "skills": ["max_banking"] on my openclaw.json, the skills works fine!

But when I remove this config from openclaw.json

And type something like "Qual Ă© meu saldo?"

He tries to spawn a session and put a subagent
{
"status": "forbidden",
"error": "agentId is not allowed for sessions_spawn (allowed: none)"
}

Why this fuck happend?

wary ironBOT
#

Yeah, this is basically the model going off the rails, not your skill loader.

What’s happening:

  • with skills: ["max_banking"], you made routing tight
  • without that config, the agent sees all the other skills again
  • then the model sometimes picks a dumb path, likely a planning/task skill, and tries sessions_spawn
  • your session/tool policy does not allow sessions_spawn, so OpenClaw blocks it with:
{
  "status": "forbidden",
  "error": "agentId is not allowed for sessions_spawn (allowed: none)"
}

So the short version is:

#

your banking skill works, but without the allowlist the model is choosing the wrong thing before it ever commits to max_banking.

Given the skills you showed earlier, the most likely troublemakers are the bundled planning skills, especially things like:

  • taskflow
  • taskflow-inbox-triage

Those are much more likely to push the model toward subagent/planning behavior.

What I’d do

If you want banking to be reliable, keep one of these setups:

Option A, best and simplest

Keep:

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

Option B, cleaner long-term

Create a dedicated banking agent that only has max_banking, and keep your normal agent broad.


Why it works with the allowlist

Because you turned this:

#

“pick the right thing from a bunch of skills + generic behavior”

into this:

“there is basically one relevant skill, use it”

That is why it suddenly becomes reliable.

So no, this is not “your skill randomly breaking”.
It is routing quality plus other visible skills pulling the model into bad behavior.

Docs: https://docs.openclaw.ai/tools/skills-config

If you want, I can help you design the exact banking-agent config so you keep your normal assistant and also have a banking-only one that behaves predictably.

peak latch
#

But I want all my bank clients that have openclaw allow to easy use off my skill although they have anothers skills for example. I disabled taskflow
taskflow-inbox-triage on dashboard

wary ironBOT
#

I get it, and this is the annoying part:

you want one extra skill to work well in a normal OpenClaw install, without forcing users to give up their other skills.

That is a reasonable product goal.

But today, plain free-text routing like:

“Qual é meu saldo?”

is still heuristic when multiple skills are visible.

So disabling taskflow and taskflow-inbox-triage was a good move, but it does not fully solve the problem.

The blunt truth

If your clients have many skills enabled, OpenClaw does not currently guarantee:

#

“bank-like phrase” → “your skill”

every time.

That’s the core limitation.


Best ways to make it easy for clients

Option 1, best real-world UX

Keep natural language support, but also give them a deterministic entrypoint:

  • /max_banking
  • or /skill max_banking ...

That way:

#
  • plain chat can work
  • slash command always works

For a product, this is the safest path.


Option 2, better routing without removing all other skills

Make the description even more direct, like:

description: Banco Max: consultar saldo, conectar conta, fazer PIX e pagar boleto.

That helps the router a lot more than a long description.

Put QR/image details in the body, not the description.

#

Option 3, best reliability while keeping other skills

Use a dedicated banking agent.

So users can still have:

  • their normal assistant
  • a banking-focused assistant with only max_banking

That is the cleanest way to keep both reliability and flexibility.


My recommendation

For clients, I’d ship the UX like this:

  1. Install skill
#
  1. Restart gateway
  2. /new
  3. User can either:
    • type naturally, or
    • use /max_banking for guaranteed behavior

That gives you the “easy” path without depending 100% on fuzzy routing.


If you want true automatic matching across many skills

That sounds like an actual OpenClaw feature request, something like:

  • skill priority
  • trigger phrases
  • preferred skill routing
  • stronger “when this phrase appears, read this skill first”