If you run OpenClaw on your own box or in ClawCloud, third-party skills are both the best feature and the biggest risk. Cisco Talos recently showed how a handful of Telegram bot skills quietly exfiltrated chat logs through side-channel HTTP calls. After reading their write-up I audited the 47 skills running on my production agent. Below is the playbook I wish I had last week — the practical steps to identify and avoid malicious skills before they siphon data or burn your credits.
Why malicious skills exist in the OpenClaw ecosystem
OpenClaw is basically an open plugin system. Anyone can publish a skill by dropping a folder with a skill.json, a SKILL.md, and some JavaScript in $HOME/.openclaw/skills or by pushing to @openclaw-skills on npm. That flexibility is a magnet for creativity and abuse. A rogue skill can:
- Fire off HTTP requests to a C2 server every time the agent responds
- Leak tokens via browser automation hooks
- Escalate to shell access because OpenClaw provides
exec()helpers - Abuse "memory" writes to smuggle PII into a persistence layer you never audit
The rest of this guide focuses on practical detection rather than theoretical hand-waving.
Reading SKILL.md like a threat analyst
Before touching code, start with the maintainer’s own words. The SKILL.md file is not just README fluff; it’s contract, changelog, and permission manifest rolled into one. Here are the red flags I look for:
- Vague purpose – “enhances productivity” without a concrete use-case usually means the author doesn’t want scrutiny.
- No maintainer contact – A throwaway GitHub alias or missing email suggests nobody will ship a security patch when things go south.
- Opaque permissions – You should see an explicit list like:
- requires: shell, browser, gmail.readonly
Anything less specific (“needs full access”) is unacceptable. - Minified code reference – If the markdown links to a
.min.jsbuild instead of source, assume the worst. - Unpinned dependencies – A
package.jsonwith"^"or"*"majors effectively gives strangers write access to your runtime in the nextnpm install.
Habit: I run awk '/^## Changelog/{flag=1;next}/^##/{flag=0}flag' on SKILL.md to make sure the change log exists and is not empty. Missing changelog = auto reject.
Grepping for hidden HTTP calls and dynamic eval
Reading the markdown is cheap, but you still need to eyeball the code. Every malicious skill Cisco flagged used a tiny loader that fetched a second-stage script from Pastebin. You can catch 90% of these with two shell one-liners:
$ grep -R --line-number -E "(fetch|axios|http.request|https.get)" skills/<skill-name>
$ grep -R --line-number "eval(.*fetch" skills/<skill-name>
Things I immediately distrust:
eval(Buffer.from(..., 'base64').toString())– classic obfuscation- Any
fetch()whose URL is not GitHub raw or a well-known CDN - Dynamic imports:
import('http://' + domain + '/payload.js')
If you find something fishy but not obviously malicious, run it through node --trace-warnings --experimental-policy with a strict lockfile to see which modules get executed.
Static analysis shortcut with semgrep
For larger audits I drop semgrep rules into a .semgrep/ folder:
rules:
- id: claw-suspicious-http
patterns:
- pattern: fetch($URL, ...)
- pattern-not: fetch("https://api.openai.com/$REST", ...)
message: "Suspicious remote fetch in skill"
severity: ERROR
Run:
$ semgrep --config .semgrep skills/<skill-name>
Semgrep isn’t perfect but it automates the boring parts.
Auditing declared permissions versus actual code usage
OpenClaw 0.41.0 introduced a granular permission model in skill.json:
{
"name": "github-pr-reviewer",
"permissions": ["github:write", "browser", "memory"]
}
I feed that list into a tiny script that searches for APIs the skill shouldn’t touch:
// audit-perms.js
import fs from 'node:fs/promises';
import path from 'node:path';
const allowed = new Set(process.argv.slice(3));
const dir = process.argv[2];
for await (const file of await fs.readdir(dir)) {
if (!file.endsWith('.js')) continue;
const text = await fs.readFile(path.join(dir, file), 'utf8');
if (text.includes('exec(') && !allowed.has('shell'))
console.log('Shell call without shell perm in', file);
if (text.match(/gmail\./) && !allowed.has('gmail:readonly'))
console.log('Gmail access without perm in', file);
}
Usage:
$ node audit-perms.js skills/github-pr-reviewer shell github:write browser memory
If the script yells, either patch the skill or delete it.
Leveraging the ClawCloud × VirusTotal scan pipeline
Since ClawCloud 2024-05-12 you get automatic VirusTotal coverage. Every time you upload or enable a skill through the web UI, the gateway zips the folder and posts it to VirusTotal’s private API. What you get back:
- SHA-256
- Malicious / Suspicious / Harmless verdict split across 72 engines
- Any network IOCs (domains, IPs)
- YARA matches
You can fetch the report from the CLI:
$ claw skill vt-report skills/github-pr-reviewer
┌─────────────────────┬────────┐
│ Engine │ Verdict│
├─────────────────────┼────────┤
│ CrowdStrike │ clean │
│ DrWeb │ clean │
│ Kaspersky │ suspicious │
└─────────────────────┴────────┘
One false positive is fine. Three suspicious engines? I yank the skill and file an issue.
Local hashing for on-prem users
If you run bare-metal OpenClaw, the same helper exists but is opt-in:
$ claw skill hash skills/telegram-photo-sync
SHA256: 4c2e...34a1
$ curl -s --request GET 'https://www.virustotal.com/api/v3/files/4c2e...34a1' \
--header 'x-apikey: $VT_TOKEN' | jq '.data.attributes.last_analysis_stats'
Yes, this leaks the hash to VirusTotal. If you’re dealing with TLP:RED data, skip and rely on local scanning instead.
Community reporting and quarantine workflow
The ecosystem only survives if we yell loudly when we find bad code. ClawCloud’s #security-alerts Discord, the GitHub SECURITY.md advisories, and the in-product “Report skill” button all end up in the same triage queue.
- Click “Report skill” in the UI or run
claw skill report <skillName> --reason 'phoning home'. - The gateway bundles: SHA-256, maintainer, last commit hash, your log snippet.
- Skill moves to quarantine state for 24 hours — new installs blocked, existing agents warned.
- Core team or a trusted responder (rotates weekly) reproduces.
- If confirmed, the skill is yanked from
@openclaw-skillsorg and marks asdeprecatedon ClawCloud.
The process is imperfect but better than silence. Last month voice-note-transcriber was flagged within 3 hours, well before widespread installs.
Zero-day bounty
If your finding is a true zero-day (e.g., RCE in the gateway) email security@clawcloud.com. Payouts are public on the @clawcloud-security repo.
Hardening your deployment with allow-lists and namespaces
You can’t audit every line when the backlog piles up. Two defensive configs help:
Skill allow-list
Add this to ~/.claw/config.yaml:
security:
allowedSkills:
- github-pr-reviewer
- notion-quick-note
- weather-cli
Any other skill will refuse to load, even if a rogue maintainer adds it as a dependency.
Namespace mapping
From OpenClaw 0.42, skills can be forced into a sandboxed Node vm.Module with a restricted global:
skills:
defaults:
namespace: "vm:readonly"
overrides:
github-pr-reviewer:
namespace: "vm:browser"
Inside vm:readonly the skill has no require('node:child_process') and no network. Fine for local math helpers, lethal for spyware.
Practical takeaway
Malicious OpenClaw skills are a supply-chain reality, not an edge case. Treat every new skill as untrusted code:
- Scan
SKILL.mdfor vague claims and missing changelogs - Grep the source for hidden HTTP calls and dynamic
eval - Cross-check declared permissions with actual API usage
- Pull the VirusTotal report before enabling in prod
- Use allow-lists and VM namespaces to contain fallout
Do that and you’ll sleep pretty well — until tomorrow’s npm advisory at least.