返回 Skills 目錄
dontbesilent2025/dbskill已通過檢查

SKILL DETAIL

dbs-skill-cleaner

dontbesilent2025/dbskill/dbs-skill-cleaner

dbs-skill-cleaner is a local Skill review and cleanup tool that scans Skill directories in the local Agent environment to identify risk signals such as ad redirection, hidden commercial intent, task hijacking, suspicious external calls, and sensitive data reads. It is based on OECD AI principles, emphasizing human autonomy, transparency and explainability, safety and reliability, accountability, and public welfare. The tool performs read-only scans by default, without network access, executing scanned scripts, or modifying files. Scan results are categorized by risk level (critical, high, review, info) and reported with references to files, line numbers, and original snippets. After the user explicitly specifies a Skill to handle, the tool restates the target path and reason, and upon confirmation, performs quarantine (moving real directories to a quarantine area, or removing symlinks only) and supports later restoration.

安裝量 · 224查看來源

Installation

npx skills add https://github.com/dontbesilent2025/dbskill --skill dbs-skill-cleaner

技能檔案

SKILL.md

最近同步 · 2026年8月29日

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`。」