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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The agent skill ecosystem is exploding — thousands of skills for OpenClaw, Cla
- **SKILL.md Linting** — validates required fields (name, description, location, schema-version), detects placeholder text and broken links
- **Schema Validation** — validates frontmatter against OpenClaw, HCS-26, and generic skill schemas
- **Structure Checks** — verifies README, LICENSE, SKILL.md, examples/, and scans for leaked secrets
- **Registry Compatibility** — validates against ClaweHub / HCS-26 registry publish requirements
- **Registry Compatibility** — validates against ClawHub / HCS-26 registry publish requirements
- **Multi-Ecosystem** — auto-detects OpenClaw, Claude Code, Codex, or Gemini CLI from repo structure
- **Configurable Fail Policy** — `errors` / `warnings` / `none`
- **PR Comment + Job Summary** — detailed breakdown in GitHub Actions
Expand Down Expand Up @@ -123,6 +123,7 @@ Overall: PASS
| `name-placeholder` | Error | Name contains TODO/FIXME/TBD |
| `description-placeholder` | Error | Description contains placeholder text |
| `body-placeholder` | Warning | Body contains TODO/FIXME |
| `action-approval-boundary-missing` | Warning | Social or browser action skill lacks explicit approval or no-submit boundary |
| `tags-too-many` | Warning | More than 10 tags |

## Structure Checks
Expand All @@ -136,7 +137,7 @@ Overall: PASS
| `secret-detected` | Error | Potential secret in tracked files |
| `env-file-present` | Warning | .env file present |

## Registry Checks (ClaweHub / HCS-26)
## Registry Checks (ClawHub / HCS-26)

| Check | Level | Description |
|-------|-------|-------------|
Expand Down
4 changes: 2 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The agent skill ecosystem is exploding (antigravity-awesome-skills: 26k★, awes
3. **Dependency Check** — verify referenced tools, models, and external resources are reachable
4. **Dry-Run Test** — invoke skill in sandbox mode, verify it produces valid output structure
5. **Publish Readiness** — check all required files exist (SKILL.md, README, LICENSE, examples/)
6. **Registry Compatibility** — validate against ClaweHub / HCS-26 registry publish requirements
6. **Registry Compatibility** — validate against ClawHub / HCS-26 registry publish requirements
7. **CI Report** — GitHub Actions job summary + PR comment with lint/test breakdown
8. **Fail Modes** — configurable: fail on errors, warnings, or schema drift

Expand All @@ -39,7 +39,7 @@ The agent skill ecosystem is exploding (antigravity-awesome-skills: 26k★, awes
- examples/ directory with at least one example
- No secrets in tracked files (.env, API keys pattern scan)

### Registry Checks (ClaweHub / HCS-26)
### Registry Checks (ClawHub / HCS-26)
- skill-id unique format: org/skill-name
- category from allowed list
- tags: minimum 3, maximum 10
Expand Down
8 changes: 8 additions & 0 deletions dist/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const PLACEHOLDER_PATTERNS = [
];
const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/;
const NAME_VALID_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _\-.:()]+$/;
const SOCIAL_ACTION_RE = /\b(browser|dm|follow|like|message|post|publish|quote|reply|repost|schedule|send|social|tweet|unfollow|x\.com)\b/i;
const APPROVAL_BOUNDARY_RE = /\b(approval|approve|confirm|confirmation|draft-only|human review|manual review|never publish|never submit|preview-only|stop before|user review|without publishing|without submitting)\b/i;
const NEGATED_APPROVAL_BOUNDARY_RE = /\b(without|no|never|do not|don't|cannot|can't|skip|bypass)\s+(?:user\s+|human\s+|manual\s+|explicit\s+)?(approval|approve|confirmation|confirm|review|consent|permission)\b/i;
function lintSkillMd(content, filePath = 'SKILL.md') {
const issues = [];
const addIssue = (level, rule, message) => {
Expand Down Expand Up @@ -88,6 +91,11 @@ function lintSkillMd(content, filePath = 'SKILL.md') {
if (PLACEHOLDER_PATTERNS.some(p => p.test(body))) {
addIssue('warning', 'body-placeholder', 'SKILL.md body appears to contain placeholder text (TODO/FIXME/TBD)');
}
const actionText = [description, body].filter(Boolean).join('\n');
if (SOCIAL_ACTION_RE.test(actionText)
&& (!APPROVAL_BOUNDARY_RE.test(actionText) || NEGATED_APPROVAL_BOUNDARY_RE.test(actionText))) {
addIssue('warning', 'action-approval-boundary-missing', 'Skill appears to drive social or browser actions but does not mention an explicit approval or no-submit boundary');
}
// Check for broken-looking local links
const links = (0, detector_1.extractBodyLinks)(content);
for (const link of links) {
Expand Down
14 changes: 14 additions & 0 deletions src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const SEMVER_RE = /^\d+\.\d+(\.\d+)?$/;

const NAME_VALID_RE = /^[a-zA-Z0-9][a-zA-Z0-9 _\-.:()]+$/;

const SOCIAL_ACTION_RE = /\b(browser|dm|follow|like|message|post|publish|quote|reply|repost|schedule|send|social|tweet|unfollow|x\.com)\b/i;

const APPROVAL_BOUNDARY_RE = /\b(approval|approve|confirm|confirmation|draft-only|human review|manual review|never publish|never submit|preview-only|stop before|user review|without publishing|without submitting)\b/i;

const NEGATED_APPROVAL_BOUNDARY_RE = /\b(without|no|never|do not|don't|cannot|can't|skip|bypass)\s+(?:user\s+|human\s+|manual\s+|explicit\s+)?(approval|approve|confirmation|confirm|review|consent|permission)\b/i;

export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): LintResult {
const issues: LintIssue[] = [];
const addIssue = (level: LintIssue['level'], rule: string, message: string) => {
Expand Down Expand Up @@ -108,6 +114,14 @@ export function lintSkillMd(content: string, filePath: string = 'SKILL.md'): Lin
addIssue('warning', 'body-placeholder', 'SKILL.md body appears to contain placeholder text (TODO/FIXME/TBD)');
}

const actionText = [description, body].filter(Boolean).join('\n');
if (
SOCIAL_ACTION_RE.test(actionText)
&& (!APPROVAL_BOUNDARY_RE.test(actionText) || NEGATED_APPROVAL_BOUNDARY_RE.test(actionText))
) {
addIssue('warning', 'action-approval-boundary-missing', 'Skill appears to drive social or browser actions but does not mention an explicit approval or no-submit boundary');
}

// Check for broken-looking local links
const links = extractBodyLinks(content);
for (const link of links) {
Expand Down
12 changes: 12 additions & 0 deletions tests/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ describe('lintSkillMd', () => {
expect(result.infos.some(i => i.rule === 'local-link')).toBe(true);
});

it('warns when social action skills do not include an approval boundary', () => {
const content = `---\nname: X Reply Writer\ndescription: Draft and publish X replies from browser context for a social account.\nlocation: ./SKILL.md\nschema-version: 1.0\n---\n# X Reply Writer\n\nUse browser context to write replies on x.com.\n`;
const result = lintSkillMd(content);
expect(result.warnings.some(w => w.rule === 'action-approval-boundary-missing')).toBe(true);
});

it('warns when social action approval wording is negated', () => {
const content = `---\nname: X Publisher\ndescription: Publish X posts without approval from the user.\nlocation: ./SKILL.md\nschema-version: 1.0\n---\n# X Publisher\n\nSend social posts without human approval.\n`;
const result = lintSkillMd(content);
expect(result.warnings.some(w => w.rule === 'action-approval-boundary-missing')).toBe(true);
});

it('returns location in issue', () => {
const result = lintSkillMd('# no frontmatter', '/path/SKILL.md');
expect(result.issues[0].location).toBe('/path/SKILL.md');
Expand Down