请与 skill 一起提交进 Git,或用 --lock-out 保存到 skill 目录之外。

August 30, 2026 · View on GitHub

Read a Skill before it reads your machine. 在让一个 Agent Skill 碰你的电脑之前,先读懂它。

License: MIT Python Tests Dependencies Topics

skill-sentry is the trust layer for AI agents: a local, static, offline tool that covers the whole life-cycle of a third-party Agent Skill.

skill-sentry面向 AI Agent 的信任层:一个本地、静态、离线的工具,覆盖第三方 Agent Skill 的整个生命周期。

Stage / 阶段CommandWhat you get / 你得到什么
Audit before install / 安装前审计auditA risk report with file:line evidence
Lock before run / 运行前锁定lockA SHA-256 evidence baseline (skill-sentry.lock.json)
Verify on hand-off / 交接时可验证verifyA per-file diff against the baseline, every finding carries a path
Trace after change / 改动后可追溯verifyBefore/after hashes for every drifted entry

It never executes the scanned Skill, never opens a network connection, never reads environment variables, and never uploads anything.

不会执行被扫描的 Skill、不会联网不读环境变量不上传任何内容


3-minute quick start / 3 分钟快速开始

git clone https://github.com/whaojie797-design/skill-sentry
cd skill-sentry
python --version          # 3.9 or newer / 需要 3.9 及以上

There is nothing to install — zero third-party dependencies, pure Python standard library.

无需安装任何依赖——零第三方依赖,纯 Python 标准库。

# 1) Audit a Skill you are about to install / 审计一个你正准备安装的 Skill
python scripts/skill_sentry.py audit ./some-skill

# 2) If it looks acceptable, lock it / 觉得没问题就锁定基线
python scripts/skill_sentry.py lock ./some-skill

# 3) Re-check it after hand-off, an update, or a week / 交接后、更新后、一周后再校验
python scripts/skill_sentry.py verify ./some-skill

audit output goes to SKILL_AUDIT.md / skill-sbom.json / policy-result.json. lock writes skill-sentry.lock.json. verify writes VERIFY_REPORT.md and verify-report.json, and prints the same report to stdout.

The legacy v0.1.0 entry point still works exactly as before:

python scripts/audit_skill.py ./some-skill            # unchanged behaviour / 行为不变

Real example / 真实示例

The commands below were run on a 4-file demo Skill (SKILL.md, scripts/run.py, scripts/helper.sh, assets/logo.png) with an in-skill policy.yml. The output below is a verbatim capture, not a mock-up.

以下命令在一份 4 文件的 demo Skill 上真实执行(SKILL.mdscripts/run.pyscripts/helper.shassets/logo.png + skill 内的 policy.yml)。输出为逐字捕获, 不是编造的示例。

Capture note / 捕获说明:输出来自 Windows(/tmp 对应本机的 C:\temp)。 Linux / macOS 上只有路径分隔符与路径不同,其余内容一致。

lock

$ python scripts/skill_sentry.py lock demo-skill --policy demo-skill/policy.yml
skill-sentry 0.2.0 - lock
========================================================================
Lock file    : C:\temp\demo-skill\skill-sentry.lock.json
Skill        : demo-skill
Files        : 4 (text=3 binary=1 unknown=0 symlink=0)
Rule-scanned : 3
Empty dirs   : 0
Warnings     : 0
Policy       : source=explicit present=True external=False
Audit        : policy_passed=True max_level=INFO findings=0
Integrity    : sha256 files_hash=cceb3b2961f160df65453a277dcbbc7902280daddd4e93a3eb82df9fb65e60a6
Limits       : max_file_size=26214400 follow_symlinks=False

This lock file is a baseline, NOT a safety guarantee.
该锁文件是基线证据,不是安全保证。
Commit it with the skill, or store it outside the skill directory.
请与 skill 一起提交进 Git,或用 --lock-out 保存到 skill 目录之外。
========================================================================
echo $?
0

lock writes the file even when the baseline is incomplete, so verify always has something to compare against. Running lock twice on an unchanged Skill produces byte-identical output apart from generated_at.

即使基线不完整,lock 也会落盘(这样 verify 永远有可比对象)。对未改动的 Skill 连续执行两次 lock,除 generated_at字节完全一致

verify — unchanged / 未改动

$ python scripts/skill_sentry.py verify demo-skill
skill-sentry 0.2.0 - verify
========================================================================
Result       : OK (exit 0) / 与基线一致(退出码 0)
Lock file    : C:\temp\demo-skill\skill-sentry.lock.json
Skill dir    : demo-skill
Findings     : error=0  review=0  info=0  unverifiable=0  fatal=0

What "verify passed" means / "verify 通过"到底意味着什么
  verify PASS = "this skill still matches the recorded baseline".
  verify PASS != "this skill is safe".
  If any entry is UNVERIFIABLE, it is NOT counted as unchanged.
  verify 通过 = 「与锁定时的记录一致」。
  verify 通过 ≠ 「这个 Skill 是安全的」。
  若有条目无法判定(UNVERIFIABLE),本报告不会把它算作「未漂移」。

Findings / 发现(0 条)
  No findings: this skill matches the recorded baseline.
  未发现差异:当前内容与锁定基线一致。
========================================================================
wrote: C:\temp\demo-skill\VERIFY_REPORT.md
wrote: C:\temp\demo-skill\verify-report.json
echo $?
0

verify — after someone edits one file / 有人改了一个文件之后

Changing total = sum(range(10)) to sum(range(11)) in scripts/run.py:

$ python scripts/skill_sentry.py verify demo-skill --no-write
skill-sentry 0.2.0 - verify
========================================================================
Result       : DRIFT (exit 1) / 检测到漂移(退出码 1)
Lock file    : C:\temp\demo-skill\skill-sentry.lock.json
Skill dir    : C:/temp/demo-skill
Findings     : error=1  review=0  info=0  unverifiable=0  fatal=0

What "verify passed" means / "verify 通过"到底意味着什么
  verify PASS = "this skill still matches the recorded baseline".
  verify PASS != "this skill is safe".
  If any entry is UNVERIFIABLE, it is NOT counted as unchanged.
  verify 通过 = 「与锁定时的记录一致」。
  verify 通过 ≠ 「这个 Skill 是安全的」。
  若有条目无法判定(UNVERIFIABLE),本报告不会把它算作「未漂移」。

Findings / 发现(1 条)
  [ERROR] FILE_MODIFIED  scripts/run.py
          content differs from the locked baseline
          expected: 24b1ff2afa92d9bbcd0b6c43399aa5463d49600b59895e40dc7180b94e15ca72
          actual:   1ebcbc3173f3a41516833b37ccd41a7079abea772de71a909452ed1dfa34ea5f
------------------------------------------------------------------------
Every finding above carries a path so it can be located in the diff.
上面每一条发现都带有 path,可在 diff 中直接定位。
========================================================================
echo $?
1

Exit codes / 退出码

Every command uses the same four codes. Treat any non-zero code as "do not trust this Skill".

CodeMeaning / 含义Trigger
0OKno drift, everything judged / 无漂移且全部可判定
1DRIFTconfirmed drift, or policy failure with --fail-on-policy
2ERRORcould not complete: missing dir, unreadable/corrupt/incompatible lock file, IO failure
3UNVERIFIABLEno drift, but some entries could not be judged

Priority is always 2 > 1 > 3 > 0. Code 3 exists because "we could not tell" must never be reported as "nothing changed".

优先级恒为 2 > 1 > 3 > 0。退出码 3 存在的意义是:「判不出来」绝不能被报成「没变」。


What audit checks / audit 检查什么

DomainRulesCatches
DestructiveDESTrm -rf /, wipe home, fork bomb, disk overwrite
NetworkNEToutbound requests, download-and-execute pipes (curl … | sh)
SecretsSECRETSSH dir, private keys, AWS creds, token patterns, env collection
ObfuscationOBFBase64 decode, dynamic evaluation, reflection
InjectionINJ"ignore previous instructions", "bypass safety", "don't tell the user"
PersistencePERSISTshell startup files, cron, launch agents, systemd
FilesystemFSwrites into /etc, /System, personal dirs
DomainsDOMAINevery external host referenced (SBOM)

Every hit reports file:line, a rule_id, a reason, a confidence and a level (INFO / REVIEW / HIGH). A heuristic hit is not a confirmed vulnerability — HIGH hits always need human review.


Threat model summary / 威胁模型摘要

The full model is in references/threat-model.md.

What it protects against / 能防什么

IDThreatHow
T1Silent code added or replaced after installlock records a hash baseline; verify reports FILE_ADDED / FILE_MODIFIED with paths
T2Malicious code smuggled in an "update"Same, plus --reaudit to compare the audit summary
T3TOCTOU: the audited version ≠ the version that runsThe lock file binds the audit conclusion to the file hashes in one artifact
T4Policy quietly loosened, invalidating an old PASSpolicy.sha256 is recorded; verify reports POLICY_CHANGED
T5No way to prove "this is what I reviewed"The lock file is diffable, committable evidence
T6New undeclared network destinationsaudit.domains is locked; --reaudit compares the list
T7Symlinks dragging host files into scopeSymlinks are never followed by default; link_target is recorded for review
T8Huge or binary files used to exhaust the toolStreaming hashes, a 25 MiB rule-scan threshold, binaries are hashed but not parsed

What it does NOT protect against / 不防什么 (abridged — read the full file)

IDNot protectedWhyMitigation outside this tool
N1A compromised hostAn attacker with equal file rights can edit the Skill, the lock file and the tool itselfOS integrity protection, read-only mounts, least privilege
N2The lock file being edited together with the SkillThe lock file sits next to the Skill by default and is not signed; it is not a root of trustlock --lock-out outside the Skill; protect the Git branch; external signing (under evaluation for v3)
N3Runtime behaviourPurely static: no sandboxing, no syscall filtering, no network monitoringContainer / sandbox / seccomp
N4IntentIt can say "differs from the baseline", never "this change is malicious"Human review of the diff
N5Author identity and provenanceNo signatures, no certificatesSigstore / minisign under evaluation for v3
N6Contents of archives and binariesA payload inside a .zip is invisible; only the .zip hash changesUnpack and review manually
N7Cryptographic absolutesSHA-256 collision resistance is an engineering assumption, and there is no signature
N8Live reputationIt cannot tell whether a domain is malicious nowExternal threat intelligence
N9Secrets injected at runtimeIt deliberately never reads the environmentAudit the runtime environment separately
N10Rule-engine false positives/negativesA hit is not a vulnerability; a miss is not a proof of safetyHuman review; see the false-positive playbook

The one sentence to remember / 务必记住的一句话

verify 通过 = 「与锁定时的记录一致」
verify 通过 ≠ 「这个 Skill 是安全的」
若有条目无法判定(UNVERIFIABLE),本报告不会把它算作「未漂移」。

Every verify report prints this, in both languages, every time.


Honest limitations / 诚实的局限

  • It does not guarantee that any Skill is safe. / 它不保证任何 Skill 的安全性。
  • It does not execute the scanned Skill. / 它不会执行被扫描的 Skill。
  • It does not upload anything, ever. / 它从不上传任何内容。
  • A heuristic hit is not a confirmed vulnerability. / 启发式命中不等于已确认的漏洞。
  • A clean verify means "matches the baseline", not "is safe". / verify 通过只意味着「与基线一致」,不意味着「安全」。
  • The lock file is not signed in v2; anyone who can edit the Skill can edit it. See N2 above. / v2 的锁文件未签名,能改 Skill 的人就能改它(见上文 N2)。
  • Symlink targets are recorded but not compared, because they are environment specific (see the threat model). / 符号链接的目标会被记录但不参与比对,因为它与环境相关。

Command reference / 命令参考

python scripts/skill_sentry.py audit  <skill_dir> [--policy P] [--out DIR]

python scripts/skill_sentry.py lock   <skill_dir>
        [--policy P]            policy file to record (its SHA-256 is stored)
        [--lock-out PATH]       lock file path (default: <skill>/skill-sentry.lock.json)
        [--max-file-size N]     rule-scan threshold in bytes (default 26214400 = 25 MiB)
        [--follow-symlinks]     follow symlinks that stay inside the Skill (default: off)
        [--no-audit]            skip the audit section (audit = null)
        [--fail-on-policy]      exit 1 when the audit fails the policy
        [--indent N]            JSON indent (default 2; 0 = compact)
        [--quiet]               errors only

python scripts/skill_sentry.py verify <skill_dir>
        [--lock PATH]           lock file path (default: <skill>/skill-sentry.lock.json)
        [--policy PATH]         policy that overrides the one recorded in the lock
        [--out DIR]             report directory (default: the lock file's directory)
        [--md-out PATH] [--json-out PATH]
        [--no-write]            print only; write nothing
        [--check-audit]         treat audit-summary changes as drift (exit 1)
        [--reaudit]             re-run the rule engine and compare the summary
        [--ignore-empty-dirs]   ignore added/removed empty directories
        [--allow-unverifiable]  record unverifiable entries but keep exit code 0
        [--quiet]

--follow-symlinks is documented as "only use it when you fully trust this Skill": following a symlink means hashing content that lives outside the Skill directory. The containment check (the resolved target must stay under the Skill root) still applies.

--follow-symlinks 的文档说明是**「仅在你能完全信任该 Skill 时使用」**:跟随符号链接 意味着哈希 Skill 目录之外的内容。即使开启,仍会强制「解析后目标必须仍在 Skill 根目录内」。


Install as an Agent Skill / 作为 Agent Skill 安装

Each host installs Skills on its own / 各宿主自行安装:

# Claude Code
git clone https://github.com/whaojie797-design/skill-sentry ~/.claude/skills/skill-sentry
# Cursor
git clone https://github.com/whaojie797-design/skill-sentry ~/.cursor/skills/skill-sentry
# Codex / OpenAI
git clone https://github.com/whaojie797-design/skill-sentry ~/.codex/skills/skill-sentry
# Gemini CLI
git clone https://github.com/whaojie797-design/skill-sentry ~/.gemini/skills/skill-sentry

CI / 持续集成

- name: Verify Skills have not drifted
  run: |
    python scripts/skill_sentry.py lock  ./skills/my-skill --lock-out build/my-skill.lock.json
    python scripts/skill_sentry.py verify ./skills/my-skill --lock build/my-skill.lock.json

Storing the lock file outside the Skill directory (--lock-out) is the stronger configuration: a change to the Skill cannot silently rewrite its own baseline. See SECURITY.md.

把锁文件存到 Skill 目录之外--lock-out)是更强的配置:对 Skill 的改动无法顺手 改写它自己的基线。详见 SECURITY.md

Zero dependencies / 零依赖

Runtime dependencies: none. Only the Python standard library is used (argparse, dataclasses, hashlib, json, os, sys, stat, datetime, platform, typing), and the code targets Python 3.9+. pytest is a test-time-only dependency.

运行时依赖:。只用标准库,目标 Python 3.9+。pytest 仅为测试期依赖。

The source is guarded by tests/test_guards.py, which statically asserts that no module imports socket / urllib.request / http.client / subprocess, never calls eval( / exec(, and never reads os.environ / os.getenv.

Resources / 资源

License

MIT — Copyright (c) 2026 whaojie797-design.