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
10 changes: 6 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,11 @@ python3 ${CLAUDE_SKILL_DIR}/tools/version_manager.py --action rollback --slug {s
```

`/delete-ex {slug}`:
确认后执行
先展示将被删除的完整路径和 meta.json 中的 name,用户明确确认后执行
```bash
rm -rf exes/{slug}
python3 ${CLAUDE_SKILL_DIR}/tools/skill_writer.py --action delete --slug {slug} --base-dir ./exes
```
工具内部会校验 slug 格式、确认目标位于 base-dir 之内且确实是一个前任 Skill 目录。**不要直接拼接 `rm -rf` 命令。**

---
---
Expand Down Expand Up @@ -617,7 +618,8 @@ python3 ${CLAUDE_SKILL_DIR}/tools/version_manager.py --action rollback --slug {s
```

`/delete-ex {slug}`:
After confirmation:
First show the full path to be deleted and the name from meta.json, then after explicit user confirmation:
```bash
rm -rf exes/{slug}
python3 ${CLAUDE_SKILL_DIR}/tools/skill_writer.py --action delete --slug {slug} --base-dir ./exes
```
The tool validates the slug format, confirms the target resolves inside base-dir, and checks it is actually an ex Skill directory. **Never construct raw `rm -rf` commands.**
54 changes: 53 additions & 1 deletion tools/skill_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
--base-dir ./exes

python3 skill_writer.py --action list --base-dir ./exes

python3 skill_writer.py --action delete --slug xiaomei --base-dir ./exes
"""

from __future__ import annotations

import re
import json
import shutil
import argparse
Expand Down Expand Up @@ -301,9 +304,46 @@ def list_exes(base_dir: Path) -> list:
return exes


SLUG_RE = re.compile(r"[a-z0-9][a-z0-9_-]{0,63}")


def validate_slug(slug: str) -> str:
"""校验 slug 格式,防止路径穿越等非法输入"""
if not SLUG_RE.fullmatch(slug):
raise ValueError(
f"非法 slug:{slug!r}(只允许小写字母/数字/下划线/连字符,且以字母或数字开头)"
)
return slug


def delete_skill(base_dir: Path, slug: str) -> Path:
"""
安全删除一个前任 Skill 目录。
三重防御:校验 slug 格式;确认解析后的目标位于 base_dir 之内;
确认目标确实是一个前任 Skill 目录(含 meta.json)。
"""
validate_slug(slug)

base_resolved = base_dir.resolve()
skill_dir = (base_dir / slug).resolve()

# 双重保险:即使 slug 校验被绕过,也绝不删除 base_dir 之外的路径
if base_resolved not in skill_dir.parents:
raise ValueError(f"拒绝删除 base-dir 之外的路径:{skill_dir}")

if not skill_dir.exists():
raise ValueError(f"找不到 Skill 目录:{skill_dir}")

if not (skill_dir / "meta.json").exists():
raise ValueError(f"{skill_dir} 不是一个前任 Skill 目录(缺少 meta.json),拒绝删除")

shutil.rmtree(skill_dir)
return skill_dir


def main() -> None:
parser = argparse.ArgumentParser(description="Skill 文件写入器")
parser.add_argument("--action", required=True, choices=["create", "update", "list"])
parser.add_argument("--action", required=True, choices=["create", "update", "list", "delete"])
parser.add_argument("--slug", help="前任 slug(用于目录名)")
parser.add_argument("--name", help="前任昵称")
parser.add_argument("--meta", help="meta.json 文件路径")
Expand Down Expand Up @@ -373,6 +413,18 @@ def main() -> None:
new_version = update_skill(skill_dir, memories_patch, persona_patch)
print(f"✅ Skill 已更新到 {new_version}:{skill_dir}")

elif args.action == "delete":
if not args.slug:
print("错误:delete 操作需要 --slug", file=sys.stderr)
sys.exit(1)

try:
deleted = delete_skill(base_dir, args.slug)
except ValueError as e:
print(f"错误:{e}", file=sys.stderr)
sys.exit(1)
print(f"✅ 已删除 {deleted}")


if __name__ == "__main__":
main()