feat: embed AI agent skills in extension packages#1126
Conversation
Allow extension authors to ship skills (SKILL.md + references, scripts, assets) alongside their extensions. Skills follow the Agent Skills open standard and are installed to the correct AI tool directory on pull. - Add `skills` field to manifest schema (directory names, not files) - Push resolves skills from project-local then global skill directories - Pull extracts skills to tool-specific dir (.claude/skills/, .cursor/skills/, etc.) - Prominent security warning on pull for prompt injection and executable scripts - Track skill directories (not individual files) in upstream_extensions.json - Extension rm deletes skill directories via recursive removal - Send skill metadata to registry during push confirm Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CRITICAL: Use safePathString for skills manifest field to prevent path traversal (e.g., skills: ["../../.ssh"]) - HIGH: Check isDirectory before recursive removal in extension rm to prevent accidental tree deletion of non-directory tracked paths - HIGH: Use swampPath fallback for skillsDir in auto_resolver_adapters instead of empty string that resolves to repo root - MEDIUM: Track hasScripts per-skill instead of globally so one skill with scripts doesn't mark all skills as having them - MEDIUM: Extract resolveSkillsDir() helper to eliminate duplicated skillsDir resolution across 5 CLI commands - MEDIUM: Consolidate GLOBAL_SKILL_DIRS into SKILL_DIRS (were identical) - MEDIUM: Add unit tests for extension_skill_validator.ts (7 tests covering frontmatter validation, scripts detection, file counting) - Bump DDD ratchet from 23→24 for skill_dirs.ts infrastructure import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Missing unit test for
resolveSkillsDir(src/domain/repo/skill_dirs.ts): This function has branching logic (tool-aware resolution with fallback) but no dedicated test file. The validator tests covervalidateExtensionSkillswell, butresolveSkillsDiris used across 5+ CLI commands and deserves its ownskill_dirs_test.tsto verify known tools resolve to tool-specific paths and unknown/"none"falls back to the swamp pulled-skills directory. -
ExtractedSkill.descriptionis always empty string (src/libswamp/extensions/push.ts:518): The validator already parses SKILL.md frontmatter and has access to thedescriptionfield, butValidatedSkilldoesn't carry it through, so push hardcodesdescription: ""in the registry metadata. Since the PR description notes registry metadata is "sent but not consumed yet," this is fine for now, but worth a follow-up to thread the description throughValidatedSkillbefore the registry starts consuming it. -
Duplicate recursive file collection:
collectFiles()inextension_skill_validator.tsand the inlinecollectSkillFiles()inresolve_extension_files.ts(lines 302-311) do the same recursive directory walk. Minor duplication — could share the helper, but not worth blocking on.
Overall: Clean, well-structured feature. Good security posture with size limits, path validation, and prominent skill warnings on pull. DDD layering is correct (domain → infrastructure import tracked by ratchet). Test coverage is solid for the validator.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Pull renderer log mode — warning placement: Skill security warnings appear after
Extracted N files:(which may be a long list), meaning they scroll off screen. The existing pattern puts warnings before the success/extracted output for other signals (e.g. safety warnings come beforePulled X@Y). Consider hoisting the skill warning above the file list so it's less likely to be missed on a large pull. Not blocking — the warnings are present and uselogger.warncorrectly. -
Pull renderer log mode — escalated scripts warning ordering: The escalated EXECUTABLE SCRIPTS warning is placed after the file list rather than immediately after the first warning sentence. A user scanning the output might miss the escalation. Minor wording suggestion: fold the scripts escalation into the first warning line itself (
includes AI agent skills (N files), including EXECUTABLE SCRIPTS) so it's impossible to miss. -
Push dry-run —
fileCountper skill vs.skillCountin summary: Dry-run showsSkills (1): aws-ec2 (3 files)while the success summary showsSkills: 1. This mixed representation (directory count vs. file count) is consistent with how models work (dry-run lists names, summary shows count), so it's fine as-is.
Verified
copyDirreturnsrelative(repoDir, destPath)paths — skill file paths in warnings are repo-relative (e.g..claude/skills/aws-ec2/SKILL.md), not absolute. Good.- JSON mode:
skillWarningfollows the existing multi-object pattern (same assafetyWarnings,platforms,missingSourceFiles). Consistent. - Push JSON:
ExtensionPushResolvedData.skillsandskillCountinExtensionPushSuccessDataare both serialized automatically byJsonExtensionPushRenderer. Complete. - Error messages are actionable: missing skill dir includes looked-in paths; unsupported tool error includes the fix command.
extension rmrecursive removal is handled correctly viastat.isDirectorycheck.
Verdict
PASS — no blocking issues. New skill UX is consistent with existing extension commands.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
src/libswamp/extensions/push.ts:889-892—startsWithpath matching double-counts skill files when one skill name is a prefix of anotherbuildResolvedDatacomputes per-skillfileCountusing:input.allSkillFiles.filter((f) => f.startsWith(s.absolutePath)).length
If two skills exist at
/repo/.claude/skills/fooand/repo/.claude/skills/foobar, the string/repo/.claude/skills/foobar/SKILL.mdmatchesstartsWith("/repo/.claude/skills/foo")— so files fromfoobarare also counted underfoo.Breaking example:
manifest.yamlwithskills: ["aws", "aws-ec2"]. The dry-run preview showsaws (5 files)when it really has 2, becauseaws-ec2's 3 files also match thestartsWithcheck.Fix:
f.startsWith(s.absolutePath + "/")(or usesepfrom@std/pathfor Windows compat). -
src/libswamp/extensions/push.ts:498-510—ExtractedSkill.descriptionis always""in registry metadataThe validator reads and validates SKILL.md frontmatter (ensuring
nameanddescriptionexist), butValidatedSkilldoesn't carry the parseddescriptionback. SocontentMetadata.skillsalways getsdescription: "". The PR body acknowledges the registry metadata "is sent but not consumed yet," so this may be intentional incompleteness — but theExtractedSkilltype promises adescriptionfield that's always empty, which could mislead downstream consumers when they do start reading it.Fix: Add
description: stringtoValidatedSkillin the validator, populate it from parsed frontmatter, then usevalidated?.description ?? ""in push.ts.
Low
-
src/libswamp/extensions/rm.ts:337-339—removeFilenow unconditionally stats before removalThe change from
Deno.remove(path)tostat(path) + remove(path, { recursive: stat.isDirectory })applies to ALL tracked entries, not just skills. If a previously-tracked file path has been replaced on disk with a directory (e.g., user accidentally creates a dir at that path),extension rmnow recursively deletes it instead of failing. The blast radius broadened slightly. This is by-design for the skill directory use case but worth noting — the old behavior was a safe failure; the new behavior silently succeeds with recursive delete. In practice, the tracked file list is tightly controlled by the pull process so this is unlikely to cause harm. -
src/domain/extensions/extension_skill_validator.ts:93-99—parseFrontmatteraccepts any YAML, not just mappingsIf the frontmatter YAML is a valid scalar like
---\ntrue\n---,parseYamlreturns a boolean. Thetypeof parsed === "object" && parsed !== nullcheck correctly rejects this, returningnull, which triggers a "missing frontmatter" error. This works, but the error message ("must start with --- delimiters") is misleading when the delimiters are present but the content isn't a mapping. Minor UX nit, not blocking.
Verdict
PASS — The code is solid overall. The safePathString validation on manifest skills, the NotFound guards on readDir, and the recursive-copy approach are all correct. The startsWith path-prefix bug (Medium #1) is a real logic error but only affects dry-run display counts, not the actual archive. The empty description (Medium #2) is incomplete metadata, not corruption. Ship it.
Summary
Extension authors can now ship AI agent skills alongside their extensions. Skills follow the Agent Skills open standard and can contain SKILL.md, references, scripts, assets, and evals.
How it works
Push side: Authors add
skills: ["my-skill"]to theirmanifest.yaml. Push resolves the skill from the project-local skill directory (e.g.,.claude/skills/my-skill/) first, then falls back to the global~/.claude/skills/my-skill/. All files in the directory are packaged recursively — no file type restrictions.Pull side: Skills are extracted to the correct AI tool directory based on
.swamp.yaml:.claude/skills/.cursor/skills/.kiro/skills/.agents/skills/none→.swamp/pulled-extensions/skills/(fallback)Skills keep their original directory name — no namespacing. Collisions are handled by the existing conflict detection.
Cleanup: Skill directories (not individual files) are tracked in
upstream_extensions.json.extension rmdeletes them via recursive removal.Security
Skills are prompt content loaded into AI agent context and can include executable scripts. The pull renderer displays a prominent warning:
If
scripts/is detected, an escalated warning is shown.Design tradeoffs
Skills-only extensions are invalid — the existing
.refine()that requires at least one model/workflow/vault/driver/datastore/report is unchanged. Skills must accompany real functionality.No bundling — skills are plain files (markdown, scripts, JSON). No deno bundle step, no cached bundles.
Directory-level tracking —
upstream_extensions.jsontracks the skill directory root, not individual files. This simplifies cleanup (one recursive delete) at the cost of not tracking exactly which files were installed.Tool switching is out of scope — if a user switches from Claude to Cursor, skills stay in the old location.
extension pull --forcere-installs to the new location. A migration step could be added torepo upgradelater.Registry metadata is sent but not consumed yet — push sends
ExtractedSkill[]in content metadata so swamp-club can display skill badges. Server-side tracked in systeminit/swamp-club#354.Push-time validation — SKILL.md must exist with valid frontmatter (
name+description). Size limits enforced (500KB per file, 2MB total). No file type restrictions since the standard allows scripts, assets, evals, etc.Files changed
extension_manifest.tsskillsfieldextension_skill_validator.ts(new)skill_dirs.ts(new),paths.tspush.ts,extension_push.ts,resolve_extension_files.tspull.ts,extension_pull.ts,extension_install.ts,extension_update.ts,extension_search.tsrm.tsextension_pull.ts,extension_push.ts(renderers)extension_content.ts,extension_content_extractor.tsskillsfields to type constructionsTest plan
deno checkpassesdeno lintpassesdeno fmtpassesswamp repo init+ create skill +swamp extension push --dry-runshows "Skills (1): test-skill (3 files)"extension/skills/test-skill/with SKILL.md, references/, scripts/.claude/skills/(requires published extension)extension rmremoves skill directory🤖 Generated with Claude Code