
Create Claw Skill
- 32 installs
- 269 repo stars
- Updated June 11, 2026
- gupsammy/claudest
Create-claw-skill is an agent skill that scaffolds OpenClaw-native SKILL.md patterns and live doc lookup for the pi-coding-agent ecosystem.
About
Create-claw-skill helps solo and indie builders package reusable capabilities for the OpenClaw / pi-coding-agent stack rather than default Claude Code layouts. It blends a concrete fetch-page skill template—URL argument, web_fetch, concise summary, error handling—with an OpenClaw ecosystem pattern guide so new skills follow platform conventions. Authors learn to research official behavior through the clawdocs CLI (get, search, fetch) and to design bodies that delegate work via OpenClaw session/sub-agent patterns instead of copying Anthropic-only assumptions. Use it when you are authoring or refactoring SKILL.md files for OpenClaw gateways, groups, exec tools, or when documentation drift would break generated skills. The skill matters because agent marketplaces reward portable, well-scoped skills; getting frontmatter, invocation, and doc lookup wrong wastes agent turns and breaks user-invocable flows.
- Documents OpenClaw-only patterns (clawdocs CLI, sub-agent sessions) absent from Claude Code skill docs
- Includes fetch-and-summarize page workflow via web_fetch with 3–5 sentence summaries and link callouts
- Steers authors toward live clawdocs slugs (tools/skills, system-prompt, exec) instead of hardcoded docs
- Supports user-invocable skills with argument hints for URL-driven fetch flows
Create Claw Skill by the numbers
- 32 all-time installs (skills.sh)
- Ranked #401 of 782 Skill Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gupsammy/claudest --skill create-claw-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 32 |
|---|---|
| repo stars | ★ 269 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 11, 2026 |
| Repository | gupsammy/claudest ↗ |
What it does
Generate OpenClaw- and pi-coding-agent–compatible agent skills using ecosystem conventions instead of Claude Code patterns.
Who is it for?
Best when you're shipping custom OpenClaw skills and need fetch-page examples plus ecosystem-specific authoring rules in one place.
Skip if: Skip if you're only on Claude Code with no OpenClaw gateway, or anyone and only needs a one-off URL summary without authoring a reusable skill package.
When should I use this skill?
Authoring or converting agent skills for OpenClaw / pi-coding-agent when Claude Code defaults do not apply.
What you get
You get OpenClaw-aligned skill structure, documented research commands, and fetch/summarize patterns ready to ship in the OpenClaw catalog.
- OpenClaw-conformant SKILL.md draft with process steps
- clawdocs lookup commands embedded for maintainable doc references
By the numbers
- Fetch workflow uses a 4-step process (fetch, 3–5 sentence summary, links, error handling)
- Documents 3 clawdocs command modes: get, search, fetch
Files
Fetch Page
Fetch the content of a URL and produce a concise summary.
Process
1. Fetch the URL passed as $1 using the web_fetch tool 2. Summarize the main content in 3–5 sentences, preserving key facts and figures 3. List any notable links or resources mentioned in the page 4. If the URL is inaccessible, report the error and suggest checking the URL format
OpenClaw Ecosystem Patterns
OpenClaw-specific patterns that skill authors need to know when generating skills for the pi-coding-agent / OpenClaw ecosystem. These patterns have no equivalent in Claude Code and represent conventions unique to OpenClaw's architecture.
---
1. Documentation Research
Use clawdocs CLI to look up OpenClaw's own documentation from within generated skill bodies:
# Fetch a specific documentation page (no header, quiet mode)
clawdocs get "tools/skills" --no-header -q
# Search for relevant pages by slug
clawdocs search "frontmatter" --slugs-only
# Fetch a topic page
clawdocs fetch "concepts/system-prompt" --no-header -qCommon useful slugs: tools/skills, concepts/system-prompt, tools/exec, tools/sessions, config/gateway, tools/groups.
Generated skills that need to reference live OpenClaw config or query capabilities should use clawdocs rather than hardcoding documentation that may drift.
---
2. Sub-Agent Delegation
OpenClaw uses sessions_spawn for background delegation. This is the equivalent of Claude Code's Task(subagent_type=...), with key differences:
sessions_spawnis non-blocking — it launches the sub-agent and the result is
announced back to chat when complete (the main agent continues without waiting)
- No `subagent_type` — all sub-agents are general purpose; there are no specialized
agent types to route to
- Sub-agents receive only
AGENTS.md+TOOLS.mdin their system prompt — they do not
inherit the current agent's skills, persona context, or conversation history
- Sub-agents can read skill files via the
readtool if you reference the skill path
When to use sessions_spawn vs inline execution:
- Use inline for steps that must complete before the next step starts
- Use
sessions_spawnfor parallel work, long-running operations, or steps whose results
are independently useful without blocking the main workflow
---
3. Cross-Skill References
There is no Skill tool in OpenClaw — the model cannot programmatically invoke another skill. Skills are triggered by the routing model when the user's message matches the description. To reference another skill from within a generated skill body:
Option A — Tell the model to read the skill:
Read `{baseDir}/../<other-skill-name>/SKILL.md` via the `read` tool to access its instructions.Option B — Tell the user to invoke it:
Instruct the user to type `/<other-skill-name>` to trigger the [other skill] workflow.Option C — Copy the relevant instructions inline — for small, stable sub-procedures that shouldn't require the user to switch workflows.
Fully qualified skill references are not needed in OpenClaw (there is no plugin namespace to qualify against in frontmatter); just reference by skill name.
---
4. Tool Groups
OpenClaw organizes tools into groups. When writing skill bodies that reference tools, use the correct OpenClaw tool names (not Claude Code tool names):
| Group | Tools | Purpose |
|---|---|---|
group:fs | read, write, edit, apply_patch | File system operations |
group:runtime | exec (primary), bash, process | Shell / subprocess execution |
group:web | web_search, web_fetch | Web access |
group:sessions | sessions_spawn, sessions_list, sessions_get | Sub-agent delegation |
Key translation table (Claude Code → OpenClaw):
| Claude Code | OpenClaw |
|---|---|
Bash | exec |
Read | read |
Write | write |
Edit | edit |
Glob | use exec + find/ls |
Grep | use exec + grep/rg |
WebSearch | web_search |
WebFetch | web_fetch |
Task(subagent_type=...) | sessions_spawn |
AskUserQuestion | agent asks conversationally (no dedicated tool) |
Skill | agent reads SKILL.md via read tool |
EnterPlanMode / ExitPlanMode | not available |
NotebookEdit | not available |
---
5. Installation Guidance
When a generated skill is ready, the user can install it in one of two places:
Workspace skill (affects only this workspace, next session):
cp -r <skill-directory> <workspace>/skills/<skill-name>/The workspace path is whatever directory is configured in agents.defaults.workspace in the user's openclaw.json.
Managed skill (shared across all agents and workspaces on this machine):
cp -r <skill-directory> ~/.openclaw/skills/<skill-name>/Distribution via ClawHub:
clawhub publish <skill-directory> --slug <slug> --version 1.0.0 --tags latestSkills take effect on the next new session after installation — the current session was snapshotted at startup and won't pick up new skills until a fresh session begins.
---
6. Path Conventions
| Purpose | Pattern | Notes |
|---|---|---|
| Skill-relative files | {baseDir}/references/file.md | Substituted at load time |
| Scripts in skill | {baseDir}/scripts/script.py | Called via exec tool |
| Managed skills | ~/.openclaw/skills/<name>/ | Shared across all agents |
| Workspace skills | <workspace>/skills/<name>/ | Per-workspace, configured via openclaw.json |
| Other skill in same collection | {baseDir}/../<other-skill>/SKILL.md | Relative path via read |
{baseDir} is substituted before the model sees the skill body — it becomes the absolute path to the skill's directory. This substitution is performed by the pi-coding-agent skill loader, not by the shell. Do not use shell variable syntax ($baseDir).
OpenClaw Frontmatter Options & Patterns Reference
Load before writing frontmatter in Phase 2, Step 2. Contains the full OpenClaw field catalog, description patterns, the metadata JSON constraint, and path conventions. OpenClaw uses the AgentSkills spec (pi-coding-agent) — its frontmatter fields are substantially different from Claude Code's.
---
Valid Frontmatter Fields
---
name: identifier # Required. Hyphen-case, lowercase, digits, hyphens only.
description: > # Required. See description patterns below.
[See patterns below]
# User-facing behavior
user-invocable: true # Show in / command menu (default true). Set false for
# background-knowledge skills that auto-trigger only.
disable-model-invocation: true # Prevent auto-triggering; user must type slash command.
argument-hint: "[arg1] [arg2]" # Document expected arguments. Quote if value contains [...].
# Command dispatch (bypass model entirely)
command-dispatch: tool # Route slash command directly to a tool without model.
command-tool: web_search # Tool name for dispatch (used with command-dispatch).
command-arg-mode: raw # Argument passing mode. "raw" is default and most common.
# Gating and ecosystem metadata (MUST be single-line JSON on one line)
metadata: '{"openclaw": {"emoji": "🔧", "requires": {"bins": ["jq"]}}}'
------
Fields NOT Valid in OpenClaw
These are Claude Code-only fields. Do not include them in OpenClaw skills — they will be ignored or cause parse errors:
| Field | Claude Code purpose | OpenClaw equivalent |
|---|---|---|
model | Select haiku/sonnet/opus per skill | Set at agent level in openclaw.json |
context: fork | Run in isolated sub-agent | Use sessions_spawn in body |
agent | Route to specialized agent type | All sub-agents are general purpose |
allowed-tools | Restrict tools per skill | Tool policy is gateway config, not frontmatter |
hooks | Pre/PostToolUse scripts | System-level hooks, not skill-level |
license | SPDX license identifier | Not a skill field |
If any of these appear in a skill being validated, validate_claw_skill.py will report them as invalid with the explanation above.
---
The metadata Field: Single-Line JSON Constraint
Critical: metadata must be a single-line JSON string on one line. Multi-line YAML mappings under metadata are not parsed correctly by the pi-coding-agent skill loader.
# Correct — single-line JSON
metadata: '{"openclaw": {"emoji": "🔍", "requires": {"bins": ["rg"]}}}'
# Wrong — multi-line YAML mapping
metadata:
openclaw:
emoji: "🔍"
requires:
bins:
- rgmetadata.openclaw Subfield Catalog
All subfields are nested inside {"openclaw": { ... }}:
| Field | Type | Purpose |
|---|---|---|
always | bool | Load skill body into every session (use sparingly — costs tokens always) |
emoji | string | Emoji shown in macOS OpenClaw UI |
homepage | string | URL for skill documentation or repo |
os | string | Platform filter: "macos", "linux", "windows", or omit for all |
requires.bins | array | Binary gating — skill hidden if any listed binary not in PATH |
requires.anyBins | array | Binary gating — skill hidden if NONE of the listed binaries are in PATH |
requires.env | array | Env var gating — skill hidden if any listed var not set |
requires.config | array | Config key gating — skill hidden if any listed key absent |
primaryEnv | string | Primary env var driving the skill (informational) |
skillKey | string | Stable identifier for programmatic cross-referencing |
install | object | Installer spec: {"brew": "pkg"}, {"node": "pkg"}, {"go": "pkg"}, {"uv": "pkg"} |
Examples:
# Skill requires jq and only runs on macOS
metadata: '{"openclaw": {"emoji": "📋", "os": "macos", "requires": {"bins": ["jq"]}}}'
# Skill needs either rg or grep (anyBins — at least one must exist)
metadata: '{"openclaw": {"requires": {"anyBins": ["rg", "grep"]}}}'
# Skill needs OPENAI_API_KEY env var to be set
metadata: '{"openclaw": {"requires": {"env": ["OPENAI_API_KEY"]}}}'
# Skill with installer and homepage
metadata: '{"openclaw": {"emoji": "🌐", "homepage": "https://example.com", "install": {"brew": "my-tool"}}}'---
Description Patterns
For Skills (auto-triggered)
- Third-person framing is a routing signal, not a stylistic choice. The routing model evaluates the description as a triggering condition. Third-person ("This skill should be used when...") reads as a condition to test. First-person reads as an instruction to execute.
- Quoted phrases must be verbatim user speech. Routing matches on literal token patterns. Write the exact words a user would type, not paraphrases.
- The description is always in context, even when the skill isn't active. Every session pays the token cost of every skill's name, description, and location. Density matters. Avoid restating the skill name or explaining what skills are.
- Cover the naive phrasing. A user who doesn't know this skill exists won't search for it by name — they'll describe their problem in plain language.
- 3–5 trigger phrases minimum. Single-phrase descriptions have high miss rates. Include at least one naive phrasing from a user who has never heard of the skill.
- Derive trigger phrases from user language. Pull phrases from how the user actually described their need during requirements gathering, not from formalized or paraphrased versions. If the user said "fix my skill," use "fix my skill" — not "skill remediation." When no user phrasing is available, imagine the most natural way someone would describe this need without knowing the skill exists.
- Negative triggers in crowded domains. When multiple skills have overlapping concerns, include "Not for X" or "Don't use for Y" to sharpen the routing decision boundary. Example: a CSS-focused skill might add "Not for Tailwind configuration" if a separate Tailwind skill exists.
- Token budget: ~400 chars (~100 tokens), 600 chars (~150 tokens) absolute max. Per the OpenClaw cost formula, each skill costs
195 + 97 + field lengthscharacters in the system prompt (~4 chars/token). A 10-skill install with 600-char descriptions burns ~2,400 tokens per session on routing metadata alone. Prioritize trigger phrases over explanatory prose. - Use `>` scalar, not `|`. Folded scalar (
>) collapses newlines to spaces, producing a single continuous string. Literal scalar (|) preserves newlines, which creates unexpected whitespace.
# Correct — third-person, verbatim phrases, folded scalar
description: >
This skill should be used when the user asks to "create an OpenClaw skill",
"build a claw skill", "write a SKILL.md for openclaw", or wants to author
a skill for the pi-coding-agent ecosystem.
# Wrong — vague, no trigger phrases, not third-person
description: Helps with skill creation.For Commands (user-invoked)
- Verb-first, under 60 chars. The description appears as a single scannable line in the
/menu — treat it as a menu label. - Describe the action, not the tool. "Search web for topic" orients by outcome.
description: Search web for topic
description: Fetch and summarize a URL
description: Publish skill to ClawHub---
Token Cost Formula
Each skill costs: 195 base + 97/skill + field lengths tokens in the system prompt (name + description + location only — body is lazy-loaded on demand via read).
Keep descriptions dense and informative but not verbose. The body is free until triggered.
---
{baseDir} Path Substitution
{baseDir} is substituted before the model sees the skill body — it becomes the absolute path to the skill directory at load time. Use it for all skill-relative file references:
Read `{baseDir}/references/my-reference.md` for the full option catalog.python3 {baseDir}/scripts/my-script.py --output jsonDo not use $CLAUDE_PLUGIN_ROOT (Claude Code) or hardcoded absolute paths in OpenClaw skills.
Script & CLI Patterns Reference (OpenClaw)
Intelligence for recognizing when a workflow step should be a script, how to design it as a proper CLI, and how to wire it into an OpenClaw skill. Load before auditing script opportunities during generation (Phase 2, Step 4).
OpenClaw-specific differences from Claude Code:
- Scripts are invoked via the
exectool (notBash) - Skill-relative paths use
{baseDir}(not$CLAUDE_PLUGIN_ROOT) GlobandGreptools do not exist — useexec+find/rg/lsfor filesystem ops- Tool scoping (e.g.,
Bash(git:*)) is a Claude Code feature — tool policy is gateway config in OpenClaw
---
Signal Patterns: When a Step Should Be a Script
A workflow step is a CLI candidate when any of the following are true. The more signals present, the stronger the case for scripting.
Signal 1 — Repeated Generation
The step produces the same structure with different parameters across invocations. Examples: scaffolding a directory tree, generating a frontmatter block, creating a boilerplate file from a template. If the model is re-generating the same code on every invocation, a parameterized script produces it once and runs reliably thereafter.
Test: Would two different users invoking this skill with different inputs cause the model to write nearly identical code blocks with just the variable parts swapped? → Script it.
Signal 2 — Unclear Tool Choice
The step needs to do something but no standard OpenClaw tool (read, exec, write, edit, etc.) covers it cleanly without combining multiple tools in a fragile sequence. Example: "validate frontmatter YAML and report structured errors" requires reading a file, parsing YAML, and applying rules — awkward as a tool sequence, natural as a script.
Test: Does the skill body describe a multi-step procedure that would be done the same way every time, using tools as primitives? → The procedure is a script waiting to be named.
Signal 3 — Rigid Input/Output Contract
The step takes a specific input shape and produces a specific output shape. Rigid contracts are the shape of good CLIs — the interface is clear enough to parameterize immediately.
Test: Can you write the --help text for this step right now, without ambiguity? If yes, it's a CLI. If the args feel unclear, it's still agentic reasoning.
Signal 4 — Dual-Use Potential
The step would be useful to run independently, outside the skill workflow. Example: a validation script is useful during skill creation, during repair, and as a standalone pre-commit check. A scaffolding script is useful both when the skill generates a new artifact and when a user wants to scaffold manually.
Test: Would a user want to run this from the terminal directly, without triggering the full skill? → Design it as a proper CLI from the start, not an internal helper.
Signal 5 — Consistency Critical
The step must produce identical output for identical inputs — not "similar" output, but bit-for-bit reproducible results. LLM generation has variance; scripts don't. File naming conventions, path construction, structural templates — anything where variance causes downstream breakage should be scripted.
Test: Would a subtle difference in output (different field order, different whitespace, slightly different file name) break something? → Deterministic script, not LLM generation.
---
CLI Design for Skill Context
A script in a skill directory is also a CLI. Design it to be invoked both by the model during a workflow and by users from the terminal.
Interface Design
Positional arguments — use for required, ordered inputs where meaning is unambiguous from context. Best for 1–2 inputs: init_claw_skill.py <name> <target-dir>.
Named flags — use for optional inputs, boolean toggles, and anything where the label clarifies meaning: --dry-run, --output json.
Flag for output format — always add --output [text|json] when the script produces structured data. The model parses JSON efficiently; humans prefer text.
Stdin input — use when the script is meant to be piped to. Use sys.stdin.read() with a flag fallback for file paths.
Explicit help text — every script needs -h/--help. This is documentation the model reads when deciding how to invoke the script, and that users see when running it manually.
Output Conventions
Stdout for result data — primary output goes to stdout. The model captures stdout.
Stderr for diagnostic messages — progress notes, warnings, verbose logging go to stderr.
Exit codes — 0 for success, 1 for usage/validation errors, 2 for runtime errors (file not found, parse failure).
Structured output for multi-field results — if the script returns more than one piece of data, output JSON on stdout. {"valid": true, "errors": []} is easier for the model to parse than "Validation passed with 0 errors."
Script Anatomy (Python template)
#!/usr/bin/env python3
"""
One-line description of what this script does.
Usage:
script.py <required-arg> [--flag value]
Examples:
script.py input.yaml --output json
"""
import argparse
import json
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("input", help="Description of required input")
parser.add_argument("--output", choices=["text", "json"], default="text",
help="Output format (default: text)")
parser.add_argument("--dry-run", action="store_true",
help="Show what would happen without making changes")
args = parser.parse_args()
# Core logic here
result = process(args.input, dry_run=args.dry_run)
if args.output == "json":
print(json.dumps(result))
else:
print(format_text(result))
sys.exit(0 if result["success"] else 1)
if __name__ == "__main__":
main()---
Common Script Archetypes
Init — Scaffold a structure
Creates a directory tree or file set from a template. Takes a name and target path; produces the scaffolded output. Should fail fast on collision by default.
Canonical args: init.py <name> [target-dir] [--output json]
Validate — Check preconditions
Reads an artifact (file, directory, config), applies a rule set, and reports violations. Output should be structured (list of {field, message, severity} objects). Exit 0 on clean, exit 1 on violations. Never modifies anything.
Canonical args: validate.py <path> [--strict] [--output json]
Transform — Convert input to output
Takes structured input, applies a deterministic transformation, produces structured output. One input, one output, no side effects unless --write is passed.
Canonical args: transform.py <input-path> [--output-path path] [--dry-run]
Package — Assemble an artifact
Collects files or content from multiple sources and assembles a distributable artifact. Should validate inputs before assembling and report what was included.
Canonical args: package.py <source-dir> [output-dir] [--dry-run] [--output json]
Query — Read state, return structured result
Reads from a data source (DB, file, API) and returns structured data. Never writes.
Canonical args: query.py [--filter key=value] [--limit N] [--output json]
---
Wiring Scripts into an OpenClaw Skill
A script that isn't referenced in SKILL.md is invisible to the model.
In SKILL.md body, reference each script with: 1. When to invoke it (the trigger condition — which phase, what signals) 2. The exact invocation via exec tool, with relevant flags and {baseDir} path 3. How to interpret the output (exit codes, which output fields matter)
Example reference pattern:
**Validate before proceeding:**
python3 {baseDir}/scripts/validate_claw_skill.py "$SKILL_DIR" --output json
Exit 1 = parse the `errors` array; resolve all `critical` and `major` items before
continuing. Exit 0 = proceed to Phase 3.Avoid vague references like "run the validation script if needed" — the model won't know which script or when "if needed" applies. State the trigger condition explicitly.
Note: Use exec tool to run these scripts, not Bash (which is not available in OpenClaw unless the gateway explicitly enables it). Reference paths with {baseDir}, not $CLAUDE_PLUGIN_ROOT or hardcoded absolute paths.
#!/usr/bin/env python3
"""
OpenClaw Skill Initializer - Creates a new skill from template
Usage:
init_claw_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples]
Examples:
init_claw_skill.py my-new-skill --path ~/.openclaw/skills
init_claw_skill.py my-new-skill --path ~/.openclaw/skills --resources scripts,references
init_claw_skill.py my-api-helper --path ./skills --resources scripts --examples
"""
import argparse
import re
import sys
from pathlib import Path
MAX_SKILL_NAME_LENGTH = 64
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
SKILL_TEMPLATE = """---
name: {skill_name}
description: >
[TODO: Write trigger-rich description. Include 3-5 varied trigger phrases.
Example: "This skill should be used when the user asks to 'do X', 'handle Y', or mentions Z."]
---
# {skill_title}
[TODO: 1-2 sentences explaining what this skill enables]
## Process
[TODO: Choose structure that fits this skill's purpose:
**Workflow-Based** (sequential processes)
- Step-by-step procedures with clear ordering
- Example: ## Overview -> ## Step 1 -> ## Step 2...
**Task-Based** (tool collections)
- Different operations/capabilities grouped by function
- Example: ## Overview -> ## Task Category 1 -> ## Task Category 2...
**Reference/Guidelines** (standards or specifications)
- Brand guidelines, coding standards, requirements
- Example: ## Overview -> ## Guidelines -> ## Specifications...
Delete this section when done.]
## [First Section]
[TODO: Add content. Use imperative voice ("Analyze", "Generate").
No first-person ("I will"). Use exec tool for shell operations.
Reference skill-relative files with {{baseDir}}/path/to/file.]
## Resources
[TODO: Delete this section if no resources needed. Otherwise, document what's in each directory:]
### scripts/
Executable code invoked via the exec tool to perform deterministic operations.
### references/
Documentation loaded into context as needed via the read tool. Keep SKILL.md lean; put
detailed info here.
### assets/
Files used in output (templates, images, fonts) — not loaded into context.
"""
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
"""
Example helper script for {skill_name}
Replace with actual implementation or delete if not needed.
Invoke via the exec tool: python3 {{baseDir}}/scripts/example.py
"""
import sys
def main():
print("Example script for {skill_name}")
# TODO: Add actual script logic
sys.exit(0)
if __name__ == "__main__":
main()
'''
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
Replace with actual reference content or delete if not needed.
Load this file via the read tool when needed: read `{{baseDir}}/references/reference.md`
## When Reference Docs Are Useful
- Comprehensive option catalogs
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for SKILL.md
- Content only needed for specific use cases
"""
EXAMPLE_ASSET = """# Example Asset
This placeholder represents where asset files would be stored.
Replace with actual files (templates, images, fonts) or delete if not needed.
Asset files are NOT loaded into context — they are used in output.
"""
def normalize_skill_name(skill_name):
"""Normalize a skill name to lowercase hyphen-case."""
normalized = skill_name.strip().lower()
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
normalized = normalized.strip("-")
normalized = re.sub(r"-{2,}", "-", normalized)
return normalized
def title_case_skill_name(skill_name):
"""Convert hyphenated skill name to Title Case."""
return " ".join(word.capitalize() for word in skill_name.split("-"))
def parse_resources(raw_resources):
if not raw_resources:
return []
resources = [item.strip() for item in raw_resources.split(",") if item.strip()]
invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})
if invalid:
allowed = ", ".join(sorted(ALLOWED_RESOURCES))
print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}")
print(f" Allowed: {allowed}")
sys.exit(1)
return list(dict.fromkeys(resources)) # dedupe preserving order
def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):
for resource in resources:
resource_dir = skill_dir / resource
resource_dir.mkdir(exist_ok=True)
if resource == "scripts":
if include_examples:
example_script = resource_dir / "example.py"
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
example_script.chmod(0o755)
print("[OK] Created scripts/example.py")
else:
print("[OK] Created scripts/")
elif resource == "references":
if include_examples:
example_ref = resource_dir / "reference.md"
example_ref.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
print("[OK] Created references/reference.md")
else:
print("[OK] Created references/")
elif resource == "assets":
if include_examples:
example_asset = resource_dir / "example_asset.txt"
example_asset.write_text(EXAMPLE_ASSET)
print("[OK] Created assets/example_asset.txt")
else:
print("[OK] Created assets/")
def init_skill(skill_name, path, resources, include_examples):
skill_dir = Path(path).expanduser().resolve() / skill_name
if skill_dir.exists():
print(f"[ERROR] Skill directory already exists: {skill_dir}")
return None
try:
skill_dir.mkdir(parents=True, exist_ok=False)
print(f"[OK] Created skill directory: {skill_dir}")
except Exception as e:
print(f"[ERROR] Error creating directory: {e}")
return None
skill_title = title_case_skill_name(skill_name)
skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)
skill_md_path = skill_dir / "SKILL.md"
try:
skill_md_path.write_text(skill_content)
print("[OK] Created SKILL.md")
except Exception as e:
print(f"[ERROR] Error creating SKILL.md: {e}")
return None
if resources:
try:
create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)
except Exception as e:
print(f"[ERROR] Error creating resource directories: {e}")
return None
print(f"\n[OK] Skill '{skill_name}' initialized at {skill_dir}")
print("\nNext steps:")
print("1. Edit SKILL.md — complete TODOs and update description")
if resources:
print("2. Add/customize resources in scripts/, references/, assets/")
print("3. Install: copy to ~/.openclaw/skills/ or <workspace>/skills/")
print("4. Distribute: clawhub publish <path> --slug <slug> --version 1.0.0 --tags latest")
return skill_dir
def main():
parser = argparse.ArgumentParser(
description="Create a new OpenClaw skill directory with template SKILL.md",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)")
parser.add_argument("--path", required=True, help="Output directory (e.g., ~/.openclaw/skills)")
parser.add_argument("--resources", default="", help="Comma-separated: scripts,references,assets")
parser.add_argument("--examples", action="store_true", help="Create example files in resource dirs")
args = parser.parse_args()
skill_name = normalize_skill_name(args.skill_name)
if not skill_name:
print("[ERROR] Skill name must include at least one letter or digit.")
sys.exit(1)
if len(skill_name) > MAX_SKILL_NAME_LENGTH:
print(f"[ERROR] Skill name too long ({len(skill_name)} chars). Max: {MAX_SKILL_NAME_LENGTH}")
sys.exit(1)
if skill_name != args.skill_name:
print(f"Note: Normalized '{args.skill_name}' to '{skill_name}'")
resources = parse_resources(args.resources)
if args.examples and not resources:
print("[ERROR] --examples requires --resources")
sys.exit(1)
print(f"Initializing OpenClaw skill: {skill_name}")
print(f" Location: {args.path}")
if resources:
print(f" Resources: {', '.join(resources)}")
print()
result = init_skill(skill_name, args.path, resources, args.examples)
sys.exit(0 if result else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
OpenClaw Skill Validator - Validates skill structure and frontmatter
Usage:
validate_claw_skill.py <skill_directory> [--output json] [--strict]
Examples:
validate_claw_skill.py ~/.openclaw/skills/my-skill
validate_claw_skill.py ./skills/my-skill --output json
"""
import argparse
import json
import re
import sys
from pathlib import Path
MAX_SKILL_NAME_LENGTH = 64
# Valid OpenClaw frontmatter fields
ALLOWED_FRONTMATTER = {
"name",
"description",
"user-invocable",
"disable-model-invocation",
"command-dispatch",
"command-tool",
"command-arg-mode",
"metadata",
"argument-hint",
"homepage", # valid alias for metadata.openclaw.homepage
}
# Claude Code-only fields that are invalid in OpenClaw
CLAUDE_CODE_FIELDS = {
"model": "set at agent level in openclaw.json, not per-skill",
"context": "context: fork is not supported; use sessions_spawn in skill body",
"agent": "specialized agent routing is not available in OpenClaw",
"allowed-tools": "tool policy is gateway config, not frontmatter",
"hooks": "hooks are system-level, not per-skill in OpenClaw",
"license": "not a valid OpenClaw skill field",
}
# Claude Code tool names that should not appear in OpenClaw skill bodies
CLAUDE_CODE_TOOLS = [
"Bash", "WebSearch", "WebFetch", "Glob", "Grep",
"Task", "Skill", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode",
"NotebookEdit",
]
def parse_frontmatter(content):
"""Extract and parse YAML frontmatter from SKILL.md content without PyYAML."""
if not content.startswith("---"):
return None, "No YAML frontmatter found (must start with ---)", None
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not match:
return None, "Invalid frontmatter format (missing closing ---)", None
frontmatter_text = match.group(1)
body = content[match.end():].strip()
# Minimal YAML parser for flat key: value pairs (sufficient for skill frontmatter)
frontmatter = {}
lines = frontmatter_text.split("\n")
i = 0
while i < len(lines):
line = lines[i]
if not line.strip() or line.strip().startswith("#"):
i += 1
continue
# Match key: value
key_match = re.match(r"^([a-zA-Z][a-zA-Z0-9_-]*)\s*:\s*(.*)", line)
if not key_match:
i += 1
continue
key = key_match.group(1)
value_raw = key_match.group(2).strip()
# Folded/literal scalar (> or |)
if value_raw in (">", "|", ">-", "|-"):
scalar_lines = []
i += 1
while i < len(lines) and (lines[i].startswith(" ") or lines[i].strip() == ""):
scalar_lines.append(lines[i].strip())
i += 1
if value_raw.startswith(">"):
frontmatter[key] = " ".join(scalar_lines).strip()
else:
frontmatter[key] = "\n".join(scalar_lines).strip()
continue
# Quoted string
if value_raw.startswith("'") and value_raw.endswith("'"):
frontmatter[key] = value_raw[1:-1]
elif value_raw.startswith('"') and value_raw.endswith('"'):
frontmatter[key] = value_raw[1:-1]
elif value_raw.lower() == "true":
frontmatter[key] = True
elif value_raw.lower() == "false":
frontmatter[key] = False
elif value_raw == "":
frontmatter[key] = None
else:
frontmatter[key] = value_raw
i += 1
return frontmatter, None, body
def validate_skill(skill_path, strict=False):
"""
Validate an OpenClaw skill directory.
Returns a dict: {"valid": bool, "errors": [{"field": str, "message": str, "severity": str}]}
Severity levels: "critical", "major", "minor"
"""
skill_path = Path(skill_path).expanduser().resolve()
errors = []
def add(field, message, severity="major"):
errors.append({"field": field, "message": message, "severity": severity})
# Check SKILL.md exists
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
add("SKILL.md", "SKILL.md not found", "critical")
return {"valid": False, "errors": errors}
content = skill_md.read_text()
# Parse frontmatter
frontmatter, parse_error, body = parse_frontmatter(content)
if parse_error:
add("frontmatter", parse_error, "critical")
return {"valid": False, "errors": errors}
if not isinstance(frontmatter, dict):
add("frontmatter", "Frontmatter must be a YAML dictionary", "critical")
return {"valid": False, "errors": errors}
# Check for Claude Code-only fields (invalid in OpenClaw)
for field, reason in CLAUDE_CODE_FIELDS.items():
if field in frontmatter:
add(
field,
f"'{field}' is a Claude Code-only field and is not valid in OpenClaw ({reason})",
"critical",
)
# Check for unexpected fields
unexpected = set(frontmatter.keys()) - ALLOWED_FRONTMATTER - set(CLAUDE_CODE_FIELDS.keys())
if unexpected:
add(
"frontmatter",
f"Unrecognized frontmatter field(s): {', '.join(sorted(unexpected))}",
"major",
)
# Required fields
if "name" not in frontmatter:
add("name", "Missing required 'name' field", "critical")
if "description" not in frontmatter:
add("description", "Missing required 'description' field", "critical")
# Validate name
name = frontmatter.get("name", "")
if isinstance(name, str):
name = name.strip()
if name:
if not re.match(r"^[a-z0-9-]+$", name):
add("name", f"Name '{name}' must be hyphen-case (lowercase, digits, hyphens only)", "critical")
elif name.startswith("-") or name.endswith("-") or "--" in name:
add("name", f"Name '{name}' cannot start/end with hyphen or have consecutive hyphens", "major")
elif len(name) > MAX_SKILL_NAME_LENGTH:
add("name", f"Name too long ({len(name)} chars). Max: {MAX_SKILL_NAME_LENGTH}", "major")
elif name is not None:
add("name", f"'name' must be a string, got {type(name).__name__}", "critical")
# Validate description
description = frontmatter.get("description", "")
if isinstance(description, str):
description = description.strip()
if description:
if len(description) > 1024:
add("description", f"Description too long ({len(description)} chars). Max: 1024", "major")
if "[TODO" in description:
add("description", "Description contains TODO placeholder — please complete it", "major")
elif description is not None:
add("description", f"'description' must be a string, got {type(description).__name__}", "major")
# Validate metadata: must be parseable as JSON (not multi-line YAML mapping)
metadata_raw = frontmatter.get("metadata")
if metadata_raw is not None:
if isinstance(metadata_raw, str):
try:
json.loads(metadata_raw)
except json.JSONDecodeError as e:
add(
"metadata",
f"'metadata' must be a single-line JSON string parseable as JSON. Error: {e}. "
"Multi-line YAML mappings under metadata are not valid in OpenClaw.",
"critical",
)
elif isinstance(metadata_raw, dict):
# Parsed as YAML dict — this means it was written as multi-line YAML, which is invalid
add(
"metadata",
"'metadata' was parsed as a YAML mapping (multi-line). "
"OpenClaw requires metadata to be a single-line JSON string: "
"metadata: '{\"openclaw\": {\"emoji\": \"🔧\"}}'",
"critical",
)
else:
add("metadata", f"'metadata' must be a JSON string, got {type(metadata_raw).__name__}", "major")
# Validate command-dispatch and command-tool consistency
has_dispatch = "command-dispatch" in frontmatter
has_tool = "command-tool" in frontmatter
if has_dispatch and not has_tool:
add("command-tool", "'command-dispatch' is set but 'command-tool' is missing", "major")
if has_tool and not has_dispatch:
add("command-dispatch", "'command-tool' is set but 'command-dispatch' is missing", "minor")
# Check body
if not body:
add("body", "SKILL.md body is empty", "major")
elif "[TODO" in body:
add("body", "SKILL.md body contains TODO placeholders — please complete them", "major")
else:
# Strip inline code spans (content between backtick pairs) before scanning —
# a checklist item saying "do not use `Bash`" should not trigger a Bash violation.
body_no_code = re.sub(r"`[^`\n]+`", "", body)
# Warn if Claude Code tool names appear in body outside of code spans
for tool_name in CLAUDE_CODE_TOOLS:
pattern = rf'\b{re.escape(tool_name)}\b'
if re.search(pattern, body_no_code):
add(
"body",
f"Body references Claude Code tool '{tool_name}'. "
f"Use the OpenClaw equivalent instead (see references/claw-patterns.md).",
"minor",
)
# Check for $CLAUDE_PLUGIN_ROOT usage outside of code spans
if "$CLAUDE_PLUGIN_ROOT" in body_no_code:
add(
"body",
"Body uses '$CLAUDE_PLUGIN_ROOT' (Claude Code path convention). "
"Use '{baseDir}' instead for OpenClaw skill-relative paths.",
"major",
)
valid = not any(e["severity"] in ("critical", "major") for e in errors)
return {"valid": valid, "errors": errors}
def format_text_output(result, skill_path):
lines = [f"Validating: {skill_path}"]
if result["valid"]:
lines.append("[OK] Skill is valid")
else:
lines.append("[FAIL] Skill has validation errors")
for e in result["errors"]:
prefix = "[ERROR]" if e["severity"] in ("critical", "major") else "[WARN]"
lines.append(f" {prefix} [{e['severity'].upper()}] {e['field']}: {e['message']}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Validate an OpenClaw skill directory",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("skill_directory", help="Path to skill directory containing SKILL.md")
parser.add_argument(
"--output",
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
parser.add_argument(
"--strict",
action="store_true",
help="Treat minor issues as errors (affects exit code)",
)
args = parser.parse_args()
result = validate_skill(args.skill_directory, strict=args.strict)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(format_text_output(result, args.skill_directory))
if args.strict:
success = len(result["errors"]) == 0
else:
success = result["valid"]
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
How it compares
Use as an OpenClaw skill authoring guide—not a generic skills.sh meta skill or an MCP server integration.
FAQ
Who is create-claw-skill for?
Developers packaging agent skills for OpenClaw, pi-coding-agent, or clawdocs-driven workflows who already use Claude Code–style SKILL.md but need platform-specific patterns.
When should I use create-claw-skill?
During Build when drafting agent-tooling skills; during Ship when hardening user-invocable fetch skills; during Operate when refreshing skills against drifting OpenClaw docs via clawdocs.
Is create-claw-skill safe to install?
Review the Security Audits panel on this Prism page and treat generated skills that call web_fetch or shell as network- and filesystem-capable before enabling in production agents.