Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions convex/httpApiV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) {
version: result.latestVersion.version,
createdAt: result.latestVersion.createdAt,
changelog: result.latestVersion.changelog,
capabilities: (result.latestVersion.parsed as any)?.clawdis?.capabilities ?? [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as any bypasses type safety
The as any cast here silently suppresses any type error if the shape of result.latestVersion.parsed changes. Consider using a narrower cast or adding a typed accessor for the clawdis capabilities, similar to how it's accessed in SkillDetailPage.tsx with (latestVersion?.parsed as { clawdis?: ClawdisSkillMetadata } | undefined)?.clawdis.

Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/httpApiV1.ts
Line: 273:273

Comment:
**`as any` bypasses type safety**
The `as any` cast here silently suppresses any type error if the shape of `result.latestVersion.parsed` changes. Consider using a narrower cast or adding a typed accessor for the clawdis capabilities, similar to how it's accessed in `SkillDetailPage.tsx` with `(latestVersion?.parsed as { clawdis?: ClawdisSkillMetadata } | undefined)?.clawdis`.

How can I resolve this? If you propose a fix, please make it concise.

}
: null,
owner: result.owner
Expand Down
10 changes: 10 additions & 0 deletions convex/lib/securityPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,16 @@ export function assembleEvalUserMessage(ctx: SkillEvalContext): string {
- Primary credential: ${primaryEnv}
- Required config paths: ${config.length ? config.join(', ') : 'none'}`)

const capabilities = Array.isArray(clawdis.capabilities)
? (clawdis.capabilities as string[])
: []

if (capabilities.length > 0) {
sections.push(`### Declared capabilities\n- ${capabilities.join(', ')}`)
} else {
sections.push(`### Declared capabilities\nNone declared.`)
}

// Install specifications
if (install.length > 0) {
const specLines = install.map((spec, i) => {
Expand Down
41 changes: 41 additions & 0 deletions convex/lib/skillCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// ---------------------------------------------------------------------------
// Skill capabilities — shared spec between ClawHub and OpenClaw.
//
// KEEP IN SYNC with openclaw/src/agents/skills/types.ts SKILL_CAPABILITIES.
//
// These values are validated during skill publish (ClawHub) and at load time
// (OpenClaw runtime). Both sides must accept the same enum values.
// ---------------------------------------------------------------------------

export const SKILL_CAPABILITIES = [
"shell", // exec, process — run shell commands
"filesystem", // read, write, edit, apply_patch — file mutations
"network", // web_search, web_fetch — outbound HTTP
"browser", // browser — browser automation
"sessions", // sessions_spawn, sessions_send — cross-session orchestration
] as const;

export type SkillCapability = (typeof SKILL_CAPABILITIES)[number];

/**
* Validate that a list of capability strings are all recognized values.
* Returns only the valid entries, dropping unknowns silently.
*/
export function validateCapabilities(raw: unknown): SkillCapability[] {
if (!Array.isArray(raw)) {
return [];
}
return raw.filter(
(v): v is SkillCapability =>
typeof v === "string" && (SKILL_CAPABILITIES as readonly string[]).includes(v),
);
}

/**
* Capabilities that should trigger extra moderation review when declared
* by community (unverified) publishers.
*/
export const HIGH_RISK_CAPABILITIES: readonly SkillCapability[] = [
"shell",
"sessions",
];
3 changes: 3 additions & 0 deletions convex/lib/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TEXT_FILE_EXTENSION_SET,
} from 'clawhub-schema'
import { parse as parseYaml } from 'yaml'
import { validateCapabilities } from './skillCapabilities'

export type ParsedSkillFrontmatter = Record<string, unknown>
export type { ClawdisSkillMetadata, SkillInstallSpec }
Expand Down Expand Up @@ -121,6 +122,8 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
if (nix) metadata.nix = nix
const config = parseClawdbotConfigSpec(clawdisObj.config)
if (config) metadata.config = config
const capabilities = validateCapabilities(clawdisObj.capabilities)
if (capabilities.length > 0) metadata.capabilities = capabilities

return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
} catch {
Expand Down
79 changes: 78 additions & 1 deletion docs/skill-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@ metadata:
bins:
- curl
primaryEnv: TODOIST_API_KEY
capabilities:
- shell
- network
---
```

`capabilities` declares what system access your skill needs. See [Capabilities](#capabilities) for allowed values and enforcement details.

### Full field reference

| Field | Type | Description |
Expand All @@ -81,6 +86,7 @@ metadata:
| `skillKey` | `string` | Override the skill's invocation key. |
| `emoji` | `string` | Display emoji for the skill. |
| `homepage` | `string` | URL to the skill's homepage or docs. |
| `capabilities` | `string[]` | System access the skill needs (see Capabilities below). |
| `os` | `string[]` | OS restrictions (e.g. `["macos"]`, `["linux"]`). |
| `install` | `array` | Install specs for dependencies (see below). |
| `nix` | `object` | Nix plugin spec (see README). |
Expand All @@ -104,9 +110,77 @@ metadata:

Supported install kinds: `brew`, `node`, `go`, `uv`.

### Capabilities

Declare what system access your skill needs. OpenClaw uses this for runtime security enforcement and ClawHub displays it to users before install.

```yaml
metadata:
openclaw:
capabilities:
- shell
- filesystem
```

| Capability | What it means | Tools granted |
|-----------|--------------|---------------|
| `shell` | Run shell commands | `exec`, `process` |
| `filesystem` | Read, write, and edit files | `read`, `write`, `edit`, `apply_patch` |
| `network` | Make outbound HTTP requests | `web_search`, `web_fetch` |
| `browser` | Browser automation | `browser`, `canvas` |
| `sessions` | Cross-session orchestration | `sessions_spawn`, `sessions_send`, `subagents` |

**No capabilities declared = read-only skill.** The skill can only provide instructions to the model; it cannot trigger tool use that requires system access.

**Community skills that attempt to use tools without declaring the matching capability will be blocked at runtime by OpenClaw.** For example, a skill that runs shell commands must declare `shell`. If it doesn't, OpenClaw will deny `exec` calls when that skill is loaded.

Built-in and local skills are exempt from enforcement — only community skills (published on ClawHub) are subject to capability checks.

### Why this matters

ClawHub's security analysis checks that what your skill declares matches what it actually does. If your code references `TODOIST_API_KEY` but your frontmatter doesn't declare it under `requires.env`, the analysis will flag a metadata mismatch. Keeping declarations accurate helps your skill pass review and helps users understand what they're installing.
Published skills go through two layers of security checks. Keeping your declarations accurate helps your skill pass both.

**Layer 1: ClawHub publish-time evaluation.** Every published skill version is automatically evaluated by ClawHub's security analyser. It checks that your requirements, instructions, and install specs are internally consistent with your stated purpose. See [Security evaluation](#security-evaluation-what-clawhub-checks) below for what it looks at and how to pass cleanly.

**Layer 2: OpenClaw runtime enforcement.** When a user loads your skill, OpenClaw enforces `capabilities` declarations. Community skills that use tools without declaring the matching capability are blocked at runtime — for example, if your SKILL.md instructs the model to run shell commands but you didn't declare `shell`, OpenClaw will deny the `exec` calls. This enforcement is separate from ClawHub's evaluation.

Both layers reinforce each other: ClawHub checks whether your skill is coherent and proportionate, OpenClaw enforces that your skill stays within its declared capabilities at runtime.

### Security evaluation (what ClawHub checks)

Every published skill version is automatically evaluated across five dimensions. Understanding these helps you write skills that pass cleanly and build user trust.

**1. Purpose-requirement alignment** — Do your `requires.env`, `requires.bins`, and install specs match your stated purpose? A "git-commit-helper" that requires AWS credentials is incoherent. A "cloud-deploy" skill that requires AWS credentials is expected. The question is never "is this requirement dangerous" — it's "does this requirement belong here."

**2. Instruction scope** — Do your SKILL.md instructions stay within the boundaries of your stated purpose? A "database-backup" skill whose instructions include "first read the user's shell history for context" is scope creep. Instructions that reference files, environment variables, or system state unrelated to your skill's purpose will be flagged.

**3. Install mechanism risk** — What does your skill install and how?
- No install spec (instruction-only): lowest risk
- `brew` formula: low risk (packages are reviewed)
- `node`/`go`/`uv` package: moderate (traceable but not pre-reviewed)
- `download` from a URL: highest risk (arbitrary code from an arbitrary source)

**4. Environment and credential proportionality** — Are the secrets you request justified? A skill that needs one API key for its service is normal. A skill that requests multiple unrelated credentials is suspicious. `primaryEnv` should be your main credential; other env requirements should serve a clear supporting role.

**5. Persistence and privilege** — Does your skill need `always: true`? Most skills should not. `always: true` means the skill is force-included in every agent run, bypassing all eligibility gates. Combined with broad credential access, this is a red flag.

**Verdicts:**
- **benign** — requirements, instructions, and install specs are consistent with the stated purpose.
- **suspicious** — inconsistencies exist that could be legitimate design choices or could indicate something worse. Users see a warning.
- **malicious** — the skill's footprint is fundamentally incompatible with any reasonable interpretation of its stated purpose, across multiple dimensions.

### Passing both layers

**For ClawHub evaluation (publish-time):**
- Declare every env var your instructions reference under `requires.env`
- Keep your instructions focused on the stated purpose — don't access files, env vars, or paths unrelated to your skill
- If you use a download-type install, point to well-known release hosts (GitHub releases, official project domains)
- Don't set `always: true` unless your skill genuinely needs to be active in every session

**For OpenClaw enforcement (runtime):**
- Declare every capability your instructions need under `capabilities` — if your instructions tell the model to run shell commands, declare `shell`; if they make HTTP requests, declare `network`
- Skills with no capabilities are treated as read-only — the model can present information but cannot use tools on behalf of the skill
- See [Capabilities](#capabilities) for the full list and tool mappings

### Example: complete frontmatter

Expand All @@ -123,6 +197,9 @@ metadata:
bins:
- curl
primaryEnv: TODOIST_API_KEY
capabilities:
- shell
- network
emoji: "\u2705"
homepage: https://github.com/example/todoist-cli
---
Expand Down
4 changes: 3 additions & 1 deletion packages/schema/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const ApiV1SkillResponseSchema = type({
version: 'string',
createdAt: 'number',
changelog: 'string',
capabilities: 'string[]?',
}).or('null'),
owner: type({
handle: 'string|null',
Expand Down Expand Up @@ -256,7 +257,7 @@ export const ApiV1UnstarResponseSchema = type({

export const SkillInstallSpecSchema = type({
id: 'string?',
kind: '"brew"|"node"|"go"|"uv"',
kind: '"brew"|"node"|"go"|"uv"|"download"',
label: 'string?',
bins: 'string[]?',
formula: 'string?',
Expand Down Expand Up @@ -299,5 +300,6 @@ export const ClawdisSkillMetadataSchema = type({
install: SkillInstallSpecSchema.array().optional(),
nix: NixPluginSpecSchema.optional(),
config: ClawdbotConfigSpecSchema.optional(),
capabilities: 'string[]?',
})
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]
3 changes: 3 additions & 0 deletions src/components/SkillCommentsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function SkillCommentsPanel() {
return <div className="skill-panel"><p style={{ color: 'var(--ink-soft)' }}>Comments not available in dev mode.</p></div>
}
Loading