CommunityCoding & Developmentgithub.com

jasveerai88/skill-vet

Vet an agent skill before you install it — stealing, spying, hidden sending, proven with file-and-line evidence. Static, read-only, pure Python.

What is skill-vet?

skill-vet is a Claude Code agent skill that vet an agent skill before you install it — stealing, spying, hidden sending, proven with file-and-line evidence. Static, read-only, pure Python.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jasveerai88/skill-vet/tree/HEAD

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

skill-vet — agent skill

Vet an agent skill pack before installing it, with one command. Use it whenever a task involves a skill from outside this machine — a marketplace download, a repo someone linked, a pack a client sent — before the pack is installed anywhere an agent will load it.

The one command

skill-vet vet <skill-pack-folder> [--json] [--strict]

(Installed via pipx install skill-vet or pip install skill-vet. From a source checkout, the same command is python3 src/skill_vet.py vet <skill-pack-folder> ….)

  • The argument is the pack's root folder (the one holding SKILL.md). One pack per run.
  • Exit codes: 0 verdict CLEAN or REVIEW, 1 verdict BLOCKED (or REVIEW with --strict), 2 input error (missing folder, not a folder, bad arguments). In --json mode every error — including argument errors — is one JSON object with an error key. Never a traceback.
  • Read-only, and it executes nothing. Vet reads the pack as static text: it never imports, runs, shells out to, or evaluates anything in the pack. Its own source imports no network or subprocess module. Safe to point at a pack you actively distrust.

The danger model (the whole product)

A pack is DANGEROUS only if it does one of three things — the owner's standing rule (2026-10-30), applied exactly:

  1. STEALS — reads credential stores, browser profiles, SSH keys, or environment secrets AND sends or copies the results out.
  2. SPIES — captures screen, keyboard, microphone, camera, or browser history AND persists or sends the capture, without the docs disclosing the capture.
  3. SENDS IN SECRET — network calls whose payload or destination is concealed: an encoded payload to an undisclosed endpoint, local file contents sent to an undisclosed endpoint, a send combined with writes outside the pack, or any send at all from a pack whose docs claim it works offline.

A pattern hit alone is never a danger verdict. Each test needs its combination of evidence in the same file, and the finding cites both halves with file and line. Anything short of a combination is a warning with the evidence stated. The combinations:

Evidence in one fileVerdict
secret read + send, or secret read + copy/move to an outside pathDANGER_STEALING (BLOCKED)
capture + (send or persistence), capture kind undisclosed in SKILL.md / README.mdDANGER_SPYING (BLOCKED)
send + encoded payload, endpoint undisclosed or non-literalDANGER_HIDDEN_SENDING (BLOCKED)
send + read of files outside the pack, endpoint undisclosedDANGER_HIDDEN_SENDING (BLOCKED)
send + write outside the pack, endpoint undisclosedDANGER_HIDDEN_SENDING (BLOCKED)
send + docs claim offline / local-onlyDANGER_HIDDEN_SENDING (BLOCKED)
secret read, no egress in the fileSECRET_ACCESS (WARN)
send to an endpoint the docs never mention, no concealment comboUNDISCLOSED_NETWORK (WARN)
send with no literal URL to checkDYNAMIC_ENDPOINT (WARN)
encoded payload to a DISCLOSED endpointENCODED_PAYLOAD (WARN)
capture + persistence/send, capture disclosed in the docsDISCLOSED_CAPTURE (WARN)
capture, no persistence or sendCAPTURE_CAPABILITY (WARN)

Judgement calls, stated openly:

  • Stealing needs no concealment: sending a credential to a documented endpoint is still stealing — the docs admitting it does not make it safe. Disclosure only separates spying and hidden-sending from their honest counterparts.
  • A send already judged as the egress half of a STEALING finding in the same file is not counted a second time as hidden sending.
  • Capture "disclosure" is checked per kind against plain words in SKILL.md / README.md (screenshot, keystroke, microphone, webcam, browser history, …). A pack that captures and says so is a capability to accept knowingly, not a spy.
  • Egress ingredients are collected per file across all its lines, including comments (conservative: capability evidence is not dismissed because it sits near a comment). Hygiene checks (shell=True, eval, download-exec) skip full-line comments — a comment mentioning a pattern is documentation, not code.

The rule table

RuleSeverityBasis
MISSING_SKILL_MDERRORNo SKILL.md at the pack root (a differently-cased name earns SKILL_MD_CASE, WARN, and is used).
FRONTMATTER_MISSINGERRORSKILL.md has no --- block.
FRONTMATTER_PARSEERRORA frontmatter line outside the supported subset (see below); the line is cited.
REQUIRED_FIELDERRORname or description missing or empty.
NAME_DIR_MISMATCHWARNNormalised frontmatter name ≠ folder name.
NAME_FORMATWARNName is not a lowercase kebab-case slug.
BROAD_TOOL_GRANTWARNAn allowed-tools entry that is *, a Tool(*) wildcard, or a bare Bash. Scoped forms like Bash(npm run *) are not flagged.
BROKEN_REFERENCEERRORA local file referenced from SKILL.md (markdown link or backticked path) or from a script (path literal) does not exist. Resolution tries the referencing file's folder, then the pack root.
REFERENCE_OUTSIDE_PACKWARNA reference resolving outside the pack folder.
LARGE_FILEWARNA file over 1 MiB (1,048,576 bytes).
BUNDLED_BINARYWARNA binary file (known binary extension, or a NUL byte in the first 8 KiB). Binaries cannot be statically vetted.
SYMLINK_OUTINFOA symlink whose target leaves the pack; the target is not scanned.
DANGER_STEALINGERROR, dangerThe combination table above.
DANGER_SPYINGERROR, dangerThe combination table above. Capture kinds: screen, keyboard, audio, camera, history (a history store read plus a database read in one file).
DANGER_HIDDEN_SENDINGERROR, dangerThe combination table above.
SECRET_ACCESSWARNA credential-store / env-secret read with no egress in the same file. Secret sources: .ssh / key files, .aws, .gnupg, .netrc, stored-credential files, browser stores and profile folders, .env files, env access naming a secret (TOKEN / SECRET / PASSWORD / API_KEY / …), whole-environment reads.
UNDISCLOSED_NETWORKWARNA network call whose endpoint host appears in neither SKILL.md nor README.md. A host counts as disclosed when it — or a parent domain of it — appears as a URL in the docs.
DYNAMIC_ENDPOINTWARNA network call with no literal URL anywhere in the file.
ENCODED_PAYLOADWARNAn encoder's output reaches a send to a disclosed endpoint. "Reaches" is judged by: the encoder call inside the send's call segment, or a variable assigned from an encoder used in the segment.
DISCLOSED_CAPTUREWARNCapture + persistence/send, disclosed per kind.
CAPTURE_CAPABILITYWARNA capture API with no persistence or send in the file.
HYGIENE_SHELL_TRUEWARNshell=True / shell: true on a code line.
HYGIENE_DYNAMIC_EXECWARNeval( / exec( / os.system( / os.popen( / __import__( on a code line.
HYGIENE_DOWNLOAD_EXECWARNA download piped to a shell (`curl …
HYGIENE_OBFUSCATIONWARNA base64 blob (120+ chars, mixed case with a digit or +/), a hex blob (120+ chars), or a run of 8+ \xNN escapes, in a text file. One finding per kind per file.
CONCEALED_UNICODEWARNInvisible characters (zero-width, word joiner, soft hyphen, bidi controls, Unicode tag characters) in a text file; count, first line, and character names cited. A leading BOM is a signature, not concealment, and is ignored.
CONCEALED_BLANK_RUNWARN50+ consecutive blank lines in a markdown file with content after them.
HIDDEN_COMMENTWARNAn HTML comment of 120+ characters in a markdown file.
WRITES_OUTSIDE_PACKWARNA script writes to an absolute, home, or system path (/tmp, /etc, ~/, $HOME, …).
INSTALL_HOOKWARNA file that runs at install time: setup.py, install.* / setup.sh, a .pth file, or a package.json with a preinstall/install/postinstall hook.

Verdicts: BLOCKED = at least one danger finding → exit 1. REVIEW = no danger, but at least one ERROR or WARN → exit 0 (1 with --strict). CLEAN = no ERROR or WARN (INFO may remain) → exit 0. REVIEW exits 0 on purpose: REVIEW means "a human should read this", and scripts that gate installs should fail on danger, not on warnings — --strict exists for those who disagree.

The frontmatter subset

The parser reads the frontmatter shape skill packs actually ship: key: value scalars (quotes stripped), key: [a, b] inline lists, key: followed by - item block lists, and indented continuation lines folded into the previous scalar. It is deliberately not a full YAML parser. A line outside the subset is a FRONTMATTER_PARSE error with the line cited — vet reports what it cannot read rather than guessing.

Scope of the scan

  • Every file in the pack is inventoried with its size. .git and __pycache__ directories are skipped.
  • Text scanning covers scripts (by extension or shebang), markdown, and other text files, up to 2 MiB per file. Binary files and symlink targets outside the pack are inventoried, never scanned.
  • References are collected from SKILL.md and from script files. References inside other markdown files (a references/*.md linking onward) are not followed in v1.0.0.
  • Disclosure is judged against the pack root's SKILL.md and README.md only — the two documents a person installing the pack is expected to have read.

JSON schema (--json)

One object:

  • tool, version, command
  • pack: dir (as passed), dirname, name (frontmatter name or null), skill_file
  • files[]: path, bytes, kind (script|markdown|text|binary|other), binary, scanned, symlink_out
  • findings[]: rule, severity (ERROR|WARN|INFO), danger (bool), file (or null), line (or null), message, fix
  • summary: findings, errors, warnings, info, danger — counts that recount exactly against findings
  • verdict (CLEAN|REVIEW|BLOCKED), verdict_rule, limits

Findings are sorted deterministically: danger first, then severity, file, line, rule — the same pack always produces the same report.

Deliberate non-goals (honesty)

  • It never executes anything. No script is run, imported, compiled, or shell-parsed; no payload is decoded (an encoded blob is reported, not opened).
  • Static patterns are evidence, not proof. A pack that assembles its behaviour at runtime from innocuous pieces, splits a combination across two files (vet judges combinations per file), or writes its social engineering in plain, honest sentences can pass. A CLEAN verdict means no pattern fired — the README says so, and so does every report.
  • Binaries are out of reach. A bundled binary is flagged precisely because vet can say nothing about it.
  • No telemetry, no network, no writes. Stdlib only; the source contains no network or subprocess code at all (a test parses its imports and proves it).
  • Pattern tables are a snapshot. New capture libraries, new credential stores, and new client APIs appear constantly; the tables in the source are dated by the release, not treated as complete.

Related Skills