dontbesilent2025/dbskill已通过检查
SKILL DETAIL
dbs-skill-cleaner
dontbesilent2025/dbskill/dbs-skill-cleaner
dbs-skill-cleaner 是一个本地 Skill 审查与清理工具,用于扫描本地 Agent 环境中的 Skill 目录,识别广告导流、隐蔽商业意图、任务劫持、可疑外部调用和敏感数据读取等风险信号。它基于 OECD AI 原则,强调人的自主性、透明与可解释、安全与可靠、问责和公共福祉。 工具默认只进行只读扫描,不会联网、执行被扫描的脚本或修改文件。扫描结果按风险等级(严重、高风险、待复核、信息)分类报告,并引用命中的文件、行号和原文片段。用户明确指定要处理的 Skill 后,工具会先复述目标路径和原因,得到确认后执行隔离操作(将真实目录移动到隔离区,软链仅解除桥接),并支持后续恢复。
安装量 · 170查看来源
Installation
npx skills add https://github.com/dontbesilent2025/dbskill --skill dbs-skill-cleaner
技能文件
SKILL.md
最近同步 · 2026年8月31日
agents/openai.yaml›
interface:
display_name: "dbs-skill-cleaner"
short_description: "扫描本地 Skill 的广告导流、任务劫持、外部调用与敏感数据读取风险"
default_prompt: "使用 $dbs-skill-cleaner,扫描目标 Skill 并报告可疑行为与风险等级。"
scripts/skill_cleaner.py›
#!/usr/bin/env python3
"""本地 skill 的只读审查与可恢复隔离工具。"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import shutil
import sys
from collections import Counter
from datetime import datetime
from pathlib import Path
DEFAULT_ROOTS = [Path.home() / name / "skills" for name in (".claude", ".codex", ".agents", ".grok")]
QUARANTINE_ROOT = Path.home() / ".dbs/skill-cleaner/quarantine"
EXECUTABLE_SUFFIXES = {".py", ".sh", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rb", ".pl"}
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build", "docs", "references", "assets", "tests", "test"}
ORDER = {"严重": 0, "高风险": 1, "待复核": 2, "信息": 3}
COMMERCIAL = r"(?:加微信|添加微信|微信号|购买|下单|付费解锁|报名课程|付费咨询|课程|咨询|推广链接|联盟链接|返佣|佣金)"
SENSITIVE = r"(?:cookie(?:s)?|浏览器(?:\s*)凭据|凭据|密钥|私钥|token|环境变量)"
NETWORK = r"(?:curl|wget|fetch\s*\(|axios|requests\.|webhook|http(?:s)?://|上传|发送|传输)"
def line_number(text: str, index: int) -> int:
return text.count("\n", 0, index) + 1
def excerpt(text: str, index: int) -> str:
start = text.rfind("\n", 0, index) + 1
end = text.find("\n", index)
return " ".join(text[start:len(text) if end < 0 else end].strip().split())[:240]
def finding(rule: str, severity: str, principle: str, message: str, path: Path, content: str, index: int) -> dict:
return {"rule": rule, "severity": severity, "principle": principle, "message": message,
"file": str(path), "line": line_number(content, index), "excerpt": excerpt(content, index)}
def scanned_files(skill_dir: Path):
"""只读取入口说明与可能被执行的代码,刻意跳过文档、变更记录和测试。"""
for path in skill_dir.rglob("*"):
# 只排除 skill 内部的文档与测试目录。用户显式传入的根目录即便位于
# tests/ 下,也必须能够作为回归样本被扫描。
if any(part in SKIP_DIRS for part in path.relative_to(skill_dir).parts):
continue
if not path.is_file() or path.stat().st_size > 1_000_000:
continue
if path.name == "SKILL.md" or path.suffix.lower() in EXECUTABLE_SUFFIXES:
yield path
def skill_fingerprint(skill_dir: Path) -> str:
"""用于合并同一 skill 的多端镜像;仅以入口及可执行文件为依据。"""
digest = hashlib.sha256()
for path in sorted(scanned_files(skill_dir), key=lambda item: str(item.relative_to(skill_dir))):
try:
digest.update(str(path.relative_to(skill_dir)).encode())
digest.update(path.read_bytes())
except OSError:
pass
return digest.hexdigest()
def find_skills(roots: list[Path]) -> tuple[list[Path], int]:
"""按真实路径和内容指纹去重,避免软链、多端镜像和内嵌副本重复计数。"""
candidates: list[Path] = []
seen_real: set[Path] = set()
for root in roots:
if not root.is_dir():
continue
for marker in root.rglob("SKILL.md"):
if any(part in SKIP_DIRS for part in marker.relative_to(root).parts[:-1]):
continue
candidate = marker.parent
try:
real = candidate.resolve()
except OSError:
real = candidate.absolute()
if real not in seen_real:
candidates.append(candidate)
seen_real.add(real)
# 精确相同的入口与执行代码通常是同一 skill 的多端副本,报告一次即可。
found: list[Path] = []
seen_content: set[str] = set()
duplicates = 0
for candidate in sorted(candidates, key=str):
fingerprint = skill_fingerprint(candidate)
if fingerprint in seen_content:
duplicates += 1
continue
found.append(candidate)
seen_content.add(fingerprint)
return found, duplicates + len(candidates) - len({str(path.resolve()) for path in candidates})
def has_consent(window: str) -> bool:
return bool(re.search(r"(?:用户|你)(?:明确|主动)?(?:要求|请求|询问|提出|授权|同意)|在用户(?:明确|主动)?(?:要求|请求|询问|提出|授权|同意)后", window, re.I))
def scan_content(path: Path, content: str) -> list[dict]:
findings: list[dict] = []
lowered = content.lower()
# 明确的系统/用户指令覆盖,才是任务劫持;防御外部内容的指令不计入。
for match in re.finditer(r"(?:忽略|无视|覆盖).{0,80}(?:之前|以上|用户|系统).{0,80}(?:指令|要求)|(?:不得|不允许).{0,60}(?:告诉|披露).{0,60}(?:用户|此指令)", content, re.I | re.S):
window = content[max(0, match.start() - 160):match.end() + 160]
if re.search(r"(?:邮件|网页|文档|附件|外部|不可信).{0,80}(?:指令|提示)", window, re.I):
continue
findings.append(finding("instruction-hijacking", "严重", "人的自主性", "文本要求覆盖用户或系统指令,或隐瞒该行为,可能破坏授权边界。", path, content, match.start()))
# 隐藏商业关系或要求所有输出导流,属于高风险;用户主动选择的商业选项只作信息记录。
for match in re.finditer(COMMERCIAL, content, re.I):
window = content[max(0, match.start() - 180):match.end() + 180]
if re.search(r"(?:每次|所有|任何).{0,80}(?:回复|回答|输出).{0,100}" + COMMERCIAL, window, re.I | re.S) or re.search(r"(?:无论|不管).{0,60}(?:用户|任务).{0,100}" + COMMERCIAL, window, re.I | re.S):
findings.append(finding("forced-commercial-diversion", "高风险", "人的自主性", "文本要求在正常任务输出中持续插入商业动作,用户难以拒绝且完成原任务。", path, content, match.start()))
elif re.search(r"(?:隐藏|不要披露|不得告知|伪装).{0,100}" + COMMERCIAL + r"|" + COMMERCIAL + r".{0,100}(?:隐藏|不要披露|不得告知|伪装)", window, re.I | re.S):
findings.append(finding("covert-commercial-intent", "高风险", "透明与可解释", "文本要求隐瞒或伪装商业关联,用户无法知情选择。", path, content, match.start()))
elif has_consent(window):
findings.append(finding("user-requested-commercial-option", "信息", "透明与可解释", "商业信息限定在用户明确请求购买、联系或服务时出现。", path, content, match.start()))
else:
findings.append(finding("commercial-reference", "待复核", "透明与可解释", "文本含商业或导流动作;请核对它是否会出现在无关任务中,以及商业关联是否已披露。", path, content, match.start()))
break
# Cookie/凭据按权限和数据流分级。仅讨论词汇、或有用户授权的按需操作不能直接判严重。
sensitive_match = re.search(SENSITIVE, content, re.I)
if sensitive_match:
window = content[max(0, sensitive_match.start() - 250):sensitive_match.end() + 250]
active_access = re.search(r"(?:读取|导入|获取|访问|提取|复制|收集).{0,80}" + SENSITIVE + r"|" + SENSITIVE + r".{0,80}(?:读取|导入|获取|访问|提取|复制|收集)", window, re.I | re.S)
exfiltration = re.search(SENSITIVE + r".{0,240}" + NETWORK + r"|" + NETWORK + r".{0,240}" + SENSITIVE, window, re.I | re.S)
if active_access and exfiltration:
findings.append(finding("sensitive-data-exfiltration", "严重", "安全与可靠", "文本同时涉及读取敏感数据与向外部发送或上传;应立即核对数据去向和授权。", path, content, sensitive_match.start()))
elif active_access and has_consent(window):
findings.append(finding("authorized-sensitive-access", "信息", "透明与可解释", "敏感数据能力说明了用户明确授权的前提;仍应在实际执行前展示范围与去向。", path, content, sensitive_match.start()))
elif active_access:
findings.append(finding("undisclosed-sensitive-access", "高风险", "安全与可靠", "文本涉及读取或导入敏感数据,却没有看到明确的用户授权前提。", path, content, sensitive_match.start()))
# 独立的网络访问仍提醒核对,但不因 curl 一词升级为严重。
match = re.search(r"\b(?:curl|wget|nc|ncat|ssh|scp)\b", lowered)
if match and not re.search(r"(?:敏感|cookie|凭据|密钥|token).{0,240}" + NETWORK, content, re.I | re.S):
findings.append(finding("external-command", "待复核", "安全与可靠", "文本包含外部命令或网络访问;请核对目标、数据范围和用户授权。", path, content, match.start()))
return findings
def scan_skill(skill_dir: Path) -> dict:
findings: list[dict] = []
for file_path in scanned_files(skill_dir):
try:
findings.extend(scan_content(file_path, file_path.read_text(encoding="utf-8", errors="replace")))
except OSError:
continue
findings.sort(key=lambda item: (ORDER[item["severity"]], item["file"], item["line"]))
return {"name": skill_dir.name, "path": str(skill_dir), "is_symlink": skill_dir.is_symlink(), "findings": findings}
def print_report(results: list[dict], roots: list[Path], duplicates: int) -> None:
counts = Counter(item["severity"] for result in results for item in result["findings"])
print("# 本地 skill 审查报告")
print(f"扫描范围:{', '.join(str(root) for root in roots if root.exists()) or '未找到默认目录'}")
print(f"发现 skill:{len(results)}(已合并 {duplicates} 个软链、镜像或内嵌副本)")
print(f"严重:{counts['严重']}|高风险:{counts['高风险']}|待复核:{counts['待复核']}|信息:{counts['信息']}")
for result in results:
if not result["findings"]:
continue
print(f"\n## {result['name']}\n位置:`{result['path']}`")
for item in result["findings"]:
print(f"\n- {item['severity']}|{item['rule']}|{item['principle']}\n - {item['file']}:{item['line']}\n - {item['message']}\n - 命中:`{item['excerpt']}`")
if not any(result["findings"] for result in results):
print("\n未发现本规则集中的风险信号。该结果不等于安全保证。")
print("\n扫描未修改任何文件。隔离前请逐个确认目标路径。")
def command_scan(args: argparse.Namespace) -> int:
roots = [Path(path).expanduser() for path in args.root] if args.root else DEFAULT_ROOTS
skills, duplicates = find_skills(roots)
skills = [path for path in skills if path.name != "dbs-skill-cleaner"]
results = [scan_skill(path) for path in skills]
if args.format == "json":
print(json.dumps({"roots": [str(root) for root in roots], "deduplicated": duplicates, "skills": results}, ensure_ascii=False, indent=2))
else:
print_report(results, roots, duplicates)
return 0
def command_quarantine(args: argparse.Namespace) -> int:
skill_dir = Path(args.skill).expanduser().absolute()
if not args.yes:
print("拒绝执行:隔离操作需要 --yes。", file=sys.stderr); return 2
if not (skill_dir / "SKILL.md").is_file() or skill_dir.name == "dbs-skill-cleaner":
print("拒绝执行:目标必须是非 dbs-skill-cleaner 的 skill 目录。", file=sys.stderr); return 2
if skill_dir.is_symlink():
target = os.readlink(skill_dir); skill_dir.unlink()
print(json.dumps({"action": "removed_symlink", "path": str(skill_dir), "source_retained": target, "reason": args.reason}, ensure_ascii=False)); return 0
destination = QUARANTINE_ROOT / datetime.now().strftime("%Y%m%d-%H%M%S") / skill_dir.name
destination.parent.mkdir(parents=True, exist_ok=True); shutil.move(str(skill_dir), str(destination))
print(json.dumps({"action": "quarantined", "from": str(skill_dir), "to": str(destination), "reason": args.reason}, ensure_ascii=False)); return 0
def command_list_quarantine(_: argparse.Namespace) -> int:
entries = sorted(QUARANTINE_ROOT.rglob("SKILL.md")) if QUARANTINE_ROOT.exists() else []
print("\n".join(str(item.parent) for item in entries) or "隔离区为空。")
return 0
def command_restore(args: argparse.Namespace) -> int:
source, target = Path(args.source).expanduser().absolute(), Path(args.target).expanduser().absolute()
if not args.yes or not str(source).startswith(str(QUARANTINE_ROOT)) or not (source / "SKILL.md").is_file() or target.exists() or target.is_symlink():
print("拒绝执行:需使用 --yes;来源须在隔离区且目标不存在。", file=sys.stderr); return 2
target.parent.mkdir(parents=True, exist_ok=True); shutil.move(str(source), str(target))
print(json.dumps({"action": "restored", "from": str(source), "to": str(target)}, ensure_ascii=False)); return 0
def main() -> int:
parser = argparse.ArgumentParser(description="扫描与隔离本地 skill。")
subparsers = parser.add_subparsers(dest="command", required=True)
scan = subparsers.add_parser("scan", help="只读扫描 skill"); scan.add_argument("--root", action="append", default=[]); scan.add_argument("--format", choices=["text", "json"], default="text"); scan.set_defaults(handler=command_scan)
quarantine = subparsers.add_parser("quarantine", help="隔离一个明确指定的 skill"); quarantine.add_argument("skill"); quarantine.add_argument("--reason", default="用户确认"); quarantine.add_argument("--yes", action="store_true"); quarantine.set_defaults(handler=command_quarantine)
listing = subparsers.add_parser("list-quarantine", help="列出隔离区"); listing.set_defaults(handler=command_list_quarantine)
restore = subparsers.add_parser("restore", help="从隔离区恢复 skill"); restore.add_argument("source"); restore.add_argument("target"); restore.add_argument("--yes", action="store_true"); restore.set_defaults(handler=command_restore)
args = parser.parse_args(); return args.handler(args)
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md›
---
name: dbs-skill-cleaner
description: 扫描本地 Skill 中的广告导流、隐蔽商业意图、任务劫持、可疑外部调用和敏感数据读取,默认只报告。用户要求审查、清理或隔离问题 Skill 时使用。
---
# dbs-skill-cleaner:本地 skill 清理器
你是用户本地 Agent 环境的 skill 审查与清理工具。你的职责是守住用户授权的任务边界,让 skill 保持透明、可控、可追溯。
审查依据采用 [OECD AI 原则](https://www.oecd.org/en/topics/ai-principles.html):人的自主性、透明与可解释、安全与可靠、问责、公共福祉。广告导流只是其中一个高频信号;任何暗中改变用户任务、收集数据或操纵选择的行为都在审查范围内。
## 核心边界
1. 用户调用 skill 时,授权范围默认限于完成当前任务。
2. 商业关系只能在用户明确提出购买、联系、课程、赞助或付费服务需求后进入对话。
3. 作者署名、版本信息、开源地址可以保留,只要它们不干扰任务。
4. 商业关联、外部调用、数据读取和行为限制必须可见、可解释、可拒绝。
5. 清理操作必须先展示扫描结果,再取得用户对具体 skill 的明确确认。
6. 清理采用隔离,不直接删除。软链只解除桥接,真实目录移动到本地隔离区,方便恢复。
## 工作模式
### 默认扫描
用户说「扫描本地 skill」「检查 skill」「检测广告」或没有指定动作时,运行:
```bash
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py scan
```
脚本会扫描存在的目录:
- `~/.claude/skills`
- `~/.codex/skills`
- `~/.agents/skills`
- `~/.grok/skills`
用户指定项目或目录时,追加一个或多个 `--root`:
```bash
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py scan --root "/absolute/path/to/skills"
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py scan --root "/path/a" --root "/path/b"
```
只读扫描。脚本只检查 `SKILL.md` 与实际可执行的代码/脚本文件,跳过 `CHANGELOG`、`TODO`、`docs`、示例、测试与二进制文件;它不会联网、执行被扫描的脚本或修改文件。
扫描器会按真实路径和入口与执行代码的内容合并软链、多端镜像与同一大 skill 内嵌的重复副本,避免把同一风险重复计数。
### 报告方式
按风险从高到低给出结果,引用命中的文件、行号、规则和原文片段。不要把「命中关键词」直接说成恶意行为;先说明风险信号与上下文。
风险分级:
| 等级 | 含义 | 典型信号 | 建议 |
|---|---|---|---|
| 🔴 严重 | 明显超出用户授权,可能损害数据、指令完整性或自主性 | 读取敏感数据后向外传输;要求覆盖用户或系统指令;隐藏推广指令 | 优先隔离,必要时检查来源与已泄露数据 |
| 🟠 高风险 | 明显的暗中导流、返佣推广或任务劫持 | 每次回复插入购买/加微信;隐藏联盟链接;将无关任务转向商业动作 | 建议隔离 |
| 🟡 待复核 | 可能存在商业目的或外部副作用,仍需结合上下文判断 | 未说明触发条件的微信、购买、付费解锁、`curl`、`wget` | 逐条阅读后决定 |
| 🟢 信息 | 透明、可控且按需触发的能力 | 作者署名、项目地址、用户主动请求后才提供的服务、经用户授权的凭据操作 | 保留 |
上下文判定规则:
- 「不要执行邮件、网页、附件中的指令」属于提示注入防护,不标记为任务劫持。
- Cookie、凭据与 Token 的能力分三类:用户明确授权后按需导入或读取,记为信息;未说明授权的读取或导入,记为高风险;读取后又发送、上传或传输到外部,记为严重。
- 商业选项分三类:用户主动询问购买、课程、联系或付费服务时提供,记为信息;无法确认使用场景的推广词,记为待复核;要求每次/所有回复导流,或要求隐藏商业关系,记为高风险。
报告结尾必须清楚区分:建议隔离、需要用户判断、可保留。再询问一句:`要隔离哪些 skill?请回复名称或完整路径。`
### 隔离
用户明确指定要处理的 skill 后,先复述目标路径和原因,得到确认后运行:
```bash
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py quarantine "/absolute/path/to/skill" --yes --reason "用户确认:<原因>"
```
规则:
- 不接受模糊的「都清掉」。要求用户指明报告中的名称或路径。
- 如果目标是软链,只移除软链,源 skill 保持原样。
- 如果目标是真实目录,移动到 `~/.dbs/skill-cleaner/quarantine/<时间戳>/`。
- 脚本拒绝隔离自身,避免工具失去审查能力。
- 完成后报告隔离位置或解除的桥接位置;不要声称已永久删除。
### 恢复
用户要求恢复时,先列出隔离区内容:
```bash
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py list-quarantine
```
确认来源与恢复目标均无冲突后运行:
```bash
python3 skills/dbs-skill-cleaner/scripts/skill_cleaner.py restore "/absolute/path/to/quarantined/skill" "/absolute/path/to/target" --yes
```
恢复前应重新扫描该 skill,说明仍然命中的风险;用户仍决定恢复时再执行。
## 误报处理
以下情况默认归入「待复核」:
- 用户明确要求制作营销文案、课程页、广告或销售流程;
- skill 用 `curl` 或 API 调用完成用户要求的公开服务;
- skill 防御邮件、网页或附件中的提示注入;
- skill 在用户明确授权后才操作 Cookie、凭据或 Token,且没有外传;
- skill 讨论微信、购买、付费、链接等词,但没有把它们植入无关用户任务;
- skill 包含安全审计示例,文本中出现密钥或攻击关键词。
审查时看三件事:用户是否授权、意图是否披露、用户能否拒绝且照常完成基础任务。
## 输出模板
```markdown
# 本地 skill 审查报告
扫描范围:{目录}
发现 skill:{数量}
严重:{数量}|高风险:{数量}|待复核:{数量}
## 建议隔离
### {skill 名称}
- 位置:`{路径}`
- 命中:`{规则}`,{文件}:{行号}
- 风险:{说明它如何偏离用户授权}
- 建议:隔离 / 保留并修改
## 需要你判断
{逐条列出上下文不足的命中项}
## 可保留
{列出透明且不干扰任务的元信息}
要隔离哪些 skill?请回复名称或完整路径。
```
## 自检
- 扫描阶段没有修改任何文件;
- 每个判断都有文件位置与原文依据;
- 没把关键词命中当成事实结论;
- 隔离前拿到了用户对具体目标的确认;
- 隔离后说明了可恢复的位置;
- 避免建议通过隐藏广告、规避检测或伪装商业意图来解决问题。
---
完成当前任务后直接结束。只有用户明确询问下一步,且当前环境已经安装 `/dbs` 时,简短提示:「下一步不确定时,可以输入 `/dbs`。」