Skill Scanning Open-Core
An agent skill is a directory a harness loads on its own initiative — .claude/skills/<name>/SKILL.md for Claude Code, .agents/skills/<name>/SKILL.md for Intutic's own bundled skills — containing a markdown file of instructions the agent treats as authoritative, the same way it treats the system prompt. Nothing in this codebase read that content before this feature. intutic skill audit, the sync daemon's periodic report, and the dashboard's agent posture ring now do.
What gets scanned, and by what
scanSkillContent (@intutic/shared-types, skillScan.ts) runs a table of regex patterns against a skill's raw markdown — front matter included, since front matter is just text at the top of the file, not stripped before the scan. Three threat categories, borrowed from Cisco's open-source skill-scanner taxonomy (the categorisation, not the implementation — this is a native TypeScript port, not a wrapped external tool: this codebase's own runtime stays Python-free, and already owned a detector for this exact threat genre before this phase shipped. That is a claim about OUR runtime, not a ban on ever talking to a Python tool — see Cisco skill-scanner integration below for the separate, opt-in, OPERATOR-INSTALLED integration this phase's own text used to describe only as future work. Nothing in this codebase embeds, vendors, or depends on that tool; it is shelled out to only when the operator has separately installed it, and every consumer degrades gracefully — never silently, never by pretending a skipped scan is a clean one — when it is absent):
- Prompt injection — hidden instruction blocks (
<system>,<important>tags), instructions to conceal an action from the user, redirects away from another tool or skill, HTML comments carrying a directive invisible in rendered markdown. - Data exfiltration — instructions to read a credential-shaped path (
~/.ssh/id_rsa,.env,.aws/) or route its contents into an unrelated output, and markdown links/images whose URL itself names a credential — the auto-rendered-image exfiltration vector, since an image fires an outbound request with no user action. - Malicious code — an instruction to decode an obfuscated (base64/atob) payload and execute or eval it.
Most of these patterns are a direct port of packages/proxy/src/tool_poison.rs, the proxy's existing detector for poisoned tool descriptions — the same threat genre (prose an agent treats as authoritative, published by a party the user never reviewed), adapted from tool-description context to markdown/skill-file context. A few are new, for markdown's own attack surface (hidden HTML comments, image-based exfiltration links) that a JSON tool description never has.
Every pattern ships with fixtures — strings that must trigger it, and at least three that must not — checked at import time, not from a test someone could skip. A pattern whose own fixtures disagree with it fails to import, for every consumer, immediately.
Where it runs
| Surface | What it does |
|---|---|
intutic skill audit | Reads the SKILL.md of each skill directory directly under .agents/skills/ and .claude/skills/ (one level deep; a symlinked skill directory is skipped), scans each, prints findings, and reports them to the control plane. It exits 1 when there is any finding, when a file other than a data file (.md, .json, .yaml, images and the like) could not be scanned (unreadable, over the size cap, or an unrecognised file type) or when a Cisco engine run failed, unless --exit-zero is passed. Auto-pruning a flagged line is opt-in via the enableLocalSkillAuditDelete workspace setting — the same gate that already covers the legacy rule-file audit (CLAUDE.md, .cursorrules, etc). Also enumerates and scans each skill's bundled scripts — see below. |
| Sync daemon | Scans .agents/skills/<name>/SKILL.md on every sync cycle and attaches the verdict to the agent's reported skills facet — no separate command needed. Also computes a scripts: {total, scanned, flagged} facet per skill from the same bounded bundled-file enumeration. |
| Agent posture score | The skills scoring dimension is content-aware: a skill scanned clean scores full marks, a skill with a confirmed finding scores zero, and a skill this report cannot vouch for either way (never scanned, or reported by an older daemon build) scores in between — see What this cannot catch for why "in between" and not "clean." A flagged bundled script pulls the whole skill to zero regardless of how SKILL.md itself scored; an unscanned (refused, over-cap, or unrecognized-language) bundled script pulls the skill down to the same "in between" band an unscanned SKILL.md gets. |
A file that cannot be read — permissions, vanished between discovery and the read, not a regular file — is reported scanned: false, never folded into issuesDetected: 0 or clean: true. A scan that never ran says nothing about the file's safety, and treating silence as a clean bill of health is exactly the failure mode this distinction exists to avoid.
Bundled scripts
A skill directory can ship executable files alongside its SKILL.md — a setup.sh the instructions tell the agent to run, a helper.py it imports, a downloaded binary. Until bundled-script scanning was added, nothing in this codebase even enumerated those files: intutic skill audit and the sync daemon's collectSkills both walked skill directories but only ever opened the one SKILL.md inside each, so a sibling script was invisible to every consumer.
That gap is closed by packages/shared-types/src/scriptScan.ts (bundled-file counterpart to skillScan.ts, reusing its pattern shape, 3-category taxonomy, and fixture discipline) plus two new functions in tools/cli/src/commands/skill.ts:
discoverSkillBundledFiles— a bounded, symlink-skipping walk of a skill directory's sibling files. Bounded by three caps (@intutic/shared-types):MAX_SKILL_DIR_DEPTH(3 — how deep the walk descends into subdirectories),MAX_FILES_PER_SKILL(40 — how many files it will ever return for one skill), andMAX_SCRIPT_SCAN_BYTES(256 KiB — the largest file it will read content from). Symlinks are never followed — a symlink inside a skill directory could point outside it, or outside the workspace entirely, which would turn a bounded, skill-scoped walk into an effectively unbounded one. The sync daemon'scollectSkillsapplies the same three caps and the same symlink-skip discipline through its own, independent implementation (collectSkillScripts), the same way it already re-implementsSKILL.mddiscovery independently of the CLI rather than importing fromtools/cli.auditScriptFile— reads one discovered file, infers its language viadetectScriptLanguage(extension first, shebang as a fallback), and always computes its sha256 hash — even for a file this scanner cannot itself interpret, such as a compiled binary. That hash is deliberate, not incidental: the separate, opt-in VirusTotal hash lookup depends on every bundled file being hashed regardless of whether this phase's own regex patterns can say anything about its content. (The Cisco integration below scans the skill directory itself and does not use the hashes.) Content scanning itself (scanScriptContent) only runs when the language is recognized and the file is within the byte cap.
Refusal-not-pass applies here exactly as it does for SKILL.md: a file over MAX_SCRIPT_SCAN_BYTES is reported scanned: false — a refusal, never a silent skip and never a false "clean" — and the same is true of a file whose language could not be determined. Both are still hashed.
SCRIPT_SCAN_PATTERNS seeds cover the shapes this kind of script most often carries: a remote download piped straight into a shell (curl … | sh), a base64-decoded payload piped into a shell, reading a credential-shaped path (.ssh, .aws/credentials, .env) from a script, an outbound POST (or piped stdin into curl) to an external URL, chmod +x of a just-downloaded file, and two Python-specific idioms for the same decode-then-execute shape — subprocess/os.system fed a base64-decoded payload, and eval(compile(...))/exec(base64.b64decode(...)). Every pattern carries the same fixture discipline as skillScan.ts's table: at least one string that must trigger it, at least three that must not, both checked at import time.
Findings from bundled scripts land in the same places as SKILL.md findings — the CLI's console output, the skills/report payload (each row now optionally carries kind: 'skill_md' | 'script' and, for scripts, language), the --sarif output (one intutic-skill-scan rule catalog, union of both pattern tables), and the sync daemon's per-skill scripts: {total, scanned, flagged} facet, which the posture scorer folds into the same skills dimension described above.
Cisco skill-scanner integration (opt-in)
There is an OPTIONAL integration with Cisco's open-source skill-scanner project — the one whose category taxonomy this page's native scanner already borrows (see above). It is a separate binary the operator installs themselves; nothing in this codebase's own runtime depends on it, before or after this phase.
What it adds beyond native scanning. Everything above this section — scanSkillContent, scanScriptContent — is regex-genre pattern matching: fast and dependency-free, with no AST parsing, dataflow analysis, or semantic understanding (see What this cannot catch). The Cisco integration runs static YARA/pattern detection, Python bytecode integrity checks, and command-pipeline taint analysis by default, plus — since this integration always passes --use-behavioral — full AST/dataflow analysis, all fully offline and requiring no API key. That is a genuinely different, deeper analysis path for the same bundled-script surface scriptScan.ts covers, run as a second engine alongside — never instead of — native scanning. (An earlier version of this page pointed at the wrong PyPI package.)
How to enable it.
pipx install cisco-ai-skill-scanner— installs a binary namedskill-scanneron PATH (the package name and the binary name differ).intutic doctorreports whether it is present (optional, never a failing check) and, when present, its version.intutic skill audit --engine cisco— explicitly runs the Cisco engine for this invocation, IN ADDITION to native scanning. If the binary is not on PATH, this fails loudly (non-zero exit) rather than silently falling back — an explicit request for a specific engine that cannot be honored is an error, not a degraded pass.- The
ciscoSkillScannerEnabledworkspace setting (defaultfalse) makesintutic skill auditAUTO-run the Cisco engine on every invocation, whenever the binary happens to be on PATH. Unlike--engine cisco, this path degrades gracefully: if the setting is on but the binary is absent, the CLI logs an info-level skip and continues with native scanning only — this is a best-effort auto-run, not an explicit per-invocation request.
Findings and provenance. Every finding — native or Cisco — now carries an engine: 'native' | 'cisco-skill-scanner' field, so a consumer can tell which engine produced it. In --sarif output, the Cisco runs for every skill directory are combined into one second entry in the document's runs[] array, keeping Cisco's tool name, rules and results — SARIF is explicitly designed to carry multiple tools' output in one document. Cisco's findings are also translated, for the human-readable CLI report, the control-plane skills/report payload and the native intutic-skill-scan run (so in SARIF each appears in both runs), into this codebase's own SkillScanFinding shape — patternId prefixed cisco. followed by their ruleId, category mapped onto this page's prompt_injection | data_exfiltration | malicious_code taxonomy, excerpt bounded — the same SkillScanFinding shape native findings already use, just with engine: 'cisco-skill-scanner' instead of 'native'.
Trust boundary — advisory, not enforcement. Exactly like every native finding on this page, a Cisco finding is surfaced, never acted on automatically: nothing in this codebase blocks, refuses, or auto-deletes a skill on the strength of a Cisco skill-scanner verdict alone.
VirusTotal hash lookup (opt-in, hash-only)
Hash-only, stated up front: this lookup never uploads file content. A separate, later phase added an opt-in integration that checks the sha256 hash auditScriptFile already computes for every bundled script (see Bundled scripts above) against VirusTotal's public GET /api/v3/files/{sha256} endpoint — never POST /api/v3/files (upload), and the module that calls VirusTotal (virusTotalService.ts) carries no code path capable of uploading content at all. See the dedicated VirusTotal Integration page for setup, the budget/pacing model, and what a flagged hash does to the posture score.
This is a narrower, different thing from this product's standing decline of a global MCP-server reputation database / VirusTotal integration — see MCP Governance for that separate decision, which this hash lookup does not reverse: that decline is about judging whether an MCP server is known-bad across tenants; this is a workspace-scoped, opt-in hash check on a skill-bundled script file, off by default, and never shared as a public reputation signal beyond the identical-hash verdict cache described on the integration page.
The scanner reports; two gate tiers enforce
The scanner itself never blocks, refuses, or auto-deletes a skill. Its findings show up in the CLI output, the control-plane report, and the posture score, and that is all, unless an operator has opted into the existing enableLocalSkillAuditDelete pruning gate. The sync daemon's skill_flagged event and the intutic skill scan-staged pre-commit check stay warn-only.
Enforcement happens in the tool-call gates, before a write lands, in two separate tiers described below: one checks where a write goes, the other checks what it writes.
Block-tier skill-directory protection
A second, separate control — not this scanner — now refuses a write or edit whose target path lands under .agents/skills/** or .claude/skills/**. It is worth being precise about the boundary: this mechanism looks only at where a call is writing, never at what the content says, so it is enforceable with none of the caveats above. It lives in services/sync-daemon/src/harness/protectedPaths.ts (SKILL_SURFACE_PATTERNS) and services/sync-daemon/src/lib/policySnapshot.ts (SKILL_SURFACE_TIER_SEVERITY), and follows the same "policy snapshot dynamic-tier promotion" pattern this product already uses for DESTRUCTIVE_COMMAND_PATTERNS.
Two tiers, one path family:
- Floor (compiled in, always present):
warn. Two patterns — one per skill root — ship in every gate's static floor and fire on any Write/Edit whose target path matches, regardless of whether the machine has ever synced. This is the degraded-mode baseline: it is what a workspace gets with no policy snapshot, or an invalid one, and it can only be changed by a release. It stayswarnfor exactly that reason — a compiled-in rule has no one-sync-cycle undo, so it has to be the version that is safe to run unconditionally, forever. - Snapshot (delivered every sync cycle):
block, the steady state. The same two path patterns are re-shipped asskill_surface.*.tierentries through the policy-snapshot channel, at whatever severitySKILL_SURFACE_TIER_SEVERITYcurrently declares —blocktoday. Because a gate evaluates the snapshot's rules before the compiled-in floor, a workspace with a valid snapshot sees the write refused even though the floor's own copy only warned; a workspace whose snapshot is absent or fails its digest/workspace check degrades cleanly back to the floor'swarn, because the dynamic tier is dropped and only the compiled-in rules remain. A stale snapshot is still enforced: staleness raises an alert, it does not drop the rules.
Why block, unlike the destructive-command tier it borrows the mechanism from. DESTRUCTIVE_COMMAND_PATTERNS ships its block-eligible patterns at warn first specifically to earn promotion from measured field data — those patterns are unproven against real developer traffic, and a false positive there has a real cost. Skill-directory path matching was never in that position: it does not judge content, so there is no false-positive rate to measure in the first place. Path matching is not an exception to the measurement requirement; it never needed one. And the cost of staying at warn here is not neutral: a poisoned skill file written under warn-only is not a near-miss waiting to be triaged, it is a file on disk that the very next agent session loads and treats as instructions. A warn that lets that write proceed is the incident, the same reasoning SECRET_CONTENT_PATTERNS already applies to credential values written into files.
One-sync-cycle retractability. The whole reason this ships through the snapshot rather than being promoted in place in the static floor is that it can be undone without a release: flipping SKILL_SURFACE_TIER_SEVERITY back to 'warn' and letting the next sync cycle land it returns every workspace to the floor-only behavior described above, exactly the retraction lever DESTRUCTIVE_TIER_SEVERITY already provides for the destructive-command tier.
What this does not do. It does not judge skill CONTENT. That is the separate tier below. It does not catch a skill file reached through a shell redirect, a symlink, or a relative path walk (subject: 'target', matching the tool's own path argument, not a bare mention in a command string). And workspaces in the SILENT_LOG intervention mode see the promotion downgraded to shadow (observe, don't act) on the snapshot side, same as every other snapshot-delivered rule — the floor's own warn copy is unaffected by interventionMode either way.
Block-tier skill-content protection
The content tier refuses a write into .agents/skills/** or .claude/skills/** when the text being written matches one of the scanner's patterns that has been measured at zero false positives on real, benign skills. It lives in SKILL_CONTENT_PATTERNS (services/sync-daemon/src/harness/protectedPaths.ts) and SKILL_CONTENT_TIER_SEVERITY (services/sync-daemon/src/lib/policySnapshot.ts), and it is block today.
The measurement. packages/shared-types/src/__tests__/corpus/skills/ vendors 350 published SKILL.md files from four MIT and Apache-2.0 collections (anthropics/skills, obra/superpowers, wshobson/agents, K-Dense-AI/claude-scientific-skills), each pinned to an upstream commit. PROVENANCE.md there lists sources, licences and exclusions, and BASELINE.txt holds the reviewed result. A test runs scanSkillContent over every file and pins the exact set of hits:
| Pattern | Benign skills hit | Content tier |
|---|---|---|
read-sensitive-path | 11 of 350 (3.1%) | not included, stays report-only |
| the other nine | 0 of 350 | block |
All 11 read-sensitive-path hits were reviewed and are benign. Most are skills promising not to load .env. That pattern keys on a verb near a path and cannot tell "never read .env" from "read .env".
How it ships. Through the policy snapshot only, never the compiled-in floor. Flipping SKILL_CONTENT_TIER_SEVERITY to 'warn' retracts it in one sync cycle. SILENT_LOG workspaces get shadow, as with every snapshot rule. Each rule matches the write's target path against the skill roots, then matches the pattern inside the text being written: the content, new_string/newString, new_str or file_text value of the tool input. An edit that removes a poisoned line (it appears only in old_string) is not refused, and neither is a search for one.
What zero means. Zero hits in 350 puts the 95% upper bound on a pattern's per-skill false-positive rate at about 0.86%. That is a bound, not a guarantee. The corpus is curated, published collections rather than the skills developers write locally, and no recall is measured.
Relationship to the path tier. While SKILL_SURFACE_TIER_SEVERITY is block, the path tier already refuses every write into a skill directory, so today the content tier mostly changes the reason the developer sees. It becomes the only refusal if the path tier is retracted to warn. Each tier can be retracted without the other, with one exception: the @intutic/gate and intutic-clawde gates do not evaluate the snapshot's argument column, so there the content rules behave as path-only blocks.
What it does not catch. A skill file written through a shell redirect or heredoc (a grep for the same text could not be told apart), diff-shaped editors (apply_patch, replace_in_file), symlinked or relative-walk targets, some phrasings split across a line break, and anything the patterns do not already match.
Semantic analysis (optional)
Everything above this section is deterministic pattern matching — scanSkillContent and scanScriptContent match imperative sentence and code shapes against text, with no LLM call anywhere in the hot path. That was a deliberate, accepted gap: a rephrasing that avoids every pattern's literal wording passes clean, and closing that gap needs a semantic judgment call a regex table cannot make. Semantic analysis closes it — as an opt-in, workspace-level setting (semanticSkillAnalysisEnabled, default false, set on a control plane with Settings › Security › Semantic Skill Review or PUT /api/v1/workspace/settings), never a hot/live path. It needs a control plane on a plan with the LLM judge, so not Free: on Free the content is still stripped and nothing is judged.
What it catches that deterministic scanning cannot. The judge is asked one question: does this skill's prose direct the agent to do something its own stated description does not cover — concealing an action from the user, exfiltrating data, redirecting a tool call, or otherwise acting outside a reasonable reader's expectation. This is exactly the prompt-injection-style social-engineering-of-the-agent-itself category that survives a rewording: the deterministic scanner keys on specific phrasings (<system> tags, "do not tell the user", a credential-shaped path in a markdown link); the semantic judge reads for intent, so a skill that achieves the same thing in different words is still visible to it.
The judge is told the content is data, not instructions to it. A skill file is precisely the kind of untrusted, agent-authoritative prose a prompt-injection attempt would target — and an LLM judge asked to read it is exposed to that the same way any other agent loading the skill would be. The judge's system prompt states this explicitly: the skill content under evaluation is UNTRUSTED DATA, any text inside it that looks like a command or a request directed at an AI (including at the judge itself) is to be ignored, and the judge's only job is to answer the one question above about what the skill asks an agent to do — never to follow, obey, or act on anything the content says. See JUDGE_SYSTEM_PROMPT in services/control-plane/src/services/semanticSkillAnalysisService.ts for the exact wording.
Content transits the control plane, transiently, then is stripped. Stated plainly rather than buried: when this setting is on, intutic skill audit attaches the content of a SKILL.md file (its first 65,536 characters), with the SHA-256 and length of the whole file, to its /skills/report entry — never bundled scripts, which stay the pattern scan's, the Cisco engine's and the VirusTotal lookup's domain. The control plane hands that content to the judge and then strips it before anything is persisted — the stored report never contains content, regardless of whether judging ran, succeeded, or was skipped. Only the verdict ('clean' | 'suspicious' | 'malicious' | 'partially_scanned'), a short, bounded reason string, the file path, how much of the file the judge read and when it was judged are stored, keyed by the SHA-256 of the whole file, under skills:semantic:{workspaceId} in Valkey — never the judged text itself. The sync daemon hashes each whole SKILL.md the same way to find that verdict. Keying by the whole file matters: keyed by the text sent, a skill judged clean could gain any tail past the cap and keep its clean verdict. Any change anywhere in the file is a new key, and the file is judged again.
The judge reads everything it is sent. A SKILL.md of up to 16,384 characters is judged in one call. A longer one is judged in up to four overlapping windows (neighbouring windows share 1,024 characters, so an instruction across a boundary is read whole by one of them); a window grows past 16,384 characters only as far as four windows need to cover the file — at most 17,152 characters, for a file of the full 65,536. A flag in any window flags the skill: the verdict keeps the highest severity and names the part it came from.
A file longer than 65,536 characters is never clean. The judge cannot read past the cap, so when it finds nothing in what it read the verdict is 'partially_scanned': the skill needs review, and the agent's posture says why ("Only the first 64K (65,536) characters were checked"). A flag in the part it did read still flags the skill. The deterministic pattern scan above is not capped: it reads the whole file, and a pattern past 65,536 characters is found.
Caps. Two independent bounds, both enforced server-side regardless of what a client sends:
- 65,536 characters per file — the content-transport cap on
SkillFileReportSchema'scontentfield, judged in at most four calls. - A per-workspace daily judge-call cap (
SEMANTIC_SKILL_JUDGE_DAILY_CAP, env-overridable, default 200/day), counted per call, so a long skill spends up to four — on top of a zero-cost, no-LLM-call skip for a file that has not changed since it was last judged (a 30-day Valkey marker keyed by the whole file's SHA-256, so a workspace's daily cap is spent on skills that actually changed, not re-spent every audit/sync cycle on the same unchanged file).
Fail-secure semantics. 'unjudged' is not 'clean' — it means no determination was made: a timeout, a malformed judge response, or the daily cap being spent before every window of this content was judged. None of these is stored, and none writes the 30-day marker, so the next report of the same content is judged again. (A flag found before the cap ran out is kept: more windows cannot unflag a skill.) It leaves the posture score where the deterministic scan put it, and it is never treated as evidence of safety. 'partially_scanned' caps the skill's score at the same "cannot vouch for it" band an unscanned skill gets. A confirmed 'malicious' or 'suspicious' verdict changes more — the posture score (a 'malicious' verdict overrides the skill's score to 0 outright, the same severity tier as a confirmed deterministic finding or a flagged bundled script; 'suspicious' degrades it to a fixed intermediate value, worse than "unknown" but short of a confirmed finding) and a skill.semantic.flagged notification (HIGH severity for 'malicious', MEDIUM for 'suspicious').
Verdicts are advisory, not enforcement. Nothing in this codebase blocks, refuses, or auto-prunes a skill on the strength of a semantic-judge finding — consistent with the report-only stance the deterministic scanner takes above. Its false-positive rate is unmeasured: the benign-skill corpus above measures the deterministic patterns, not the judge.
What this cannot catch
A scan that returns no findings does not guarantee a skill is secure. This is the same candor this project's other partial-coverage controls are held to — see Governance Controls Checklist for the house style this page follows.
Specifically:
- The false-positive rate is measured on published skills, not on yours. On the 350-skill benign corpus described above, nine patterns fire on zero files and
read-sensitive-pathon 11. Published collections are cleaner and more uniform than the skills developers write locally, so treat these rates as lower bounds. Theintutic-rule-authorskill this project bundles, which is full of imperative security language, is also checked clean. - Recall against real attacks is not measured either, for the same reason
tool_poison.rsdoes not claim one: the positive fixtures are hand-built from a published attack taxonomy, so they show the documented shapes are covered, not what fraction of real attacks would be caught. - Regex-genre pattern matching only — for
SKILL.mdprose AND for bundled scripts — never AST or dataflow analysis, and no semantic understanding. This is the single most important limitation, and it applies identically on both sides of this page:scanSkillContentmatches imperative sentence shapes against markdown text;scanScriptContentmatches source-code shapes (acurl | shpipeline, asubprocess.run( base64.b64decode(...))call) against script text. Neither one parses a syntax tree, resolves what a variable holds, follows a function call to its definition, or tracks where a value came from before it reaches a dangerous sink. A rephrasing that avoids a pattern's literal wording passes clean; so does a script that reaches the exact same dangerous operation through a shape these patterns don't anticipate — an extra layer of indirection, a helper function hiding the call, string concatenation splitting a keyword across two variables. This is a deliberate scope choice for this phase, not an oversight: full AST/dataflow analysis of arbitrary source code is a meaningfully different (and much heavier) engineering problem than pattern-matching prose or matching known code shapes, and building it into this phase would have meant either rushing an unrelated detector in alongside these two, or quietly pretending regex patterns generalize to real static analysis, which they do not. Full AST-level scanning of bundled scripts is available only through the separate, opt-in integration with Cisco's open-sourceskill-scannerproject described in Ciscoskill-scannerintegration above — off by default, and requires a separatepipx install cisco-ai-skill-scanner. A narrower, ALREADY-built piece sits between "no coverage" and full static analysis: the opt-in VirusTotal hash lookup catches a script whose exact bytes match a hash already known-malicious to VirusTotal's aggregated engines — a useful, cheap signal, but not code understanding of any kind. A script that is malicious but not yet hash-known to VirusTotal (a modified copy, a novel payload) is invisible to it, exactly as it would be to any hash-based detector. LLM-based semantic analysis ofSKILL.mdPROSE (as opposed to bundled scripts) is no longer a gap — see Semantic analysis (optional) above, built through this codebase'sresolveMonitorjudge-call convention like every other LLM judge invocation. Its own real-world false-positive/negative rate is unmeasured — the benign-skill corpus has not been run through the judge — which is why it stays report-only, opt-in, and never a gate. - Bundled-script coverage, specifically, is a first pass, not a general scanner.
SCRIPT_SCAN_PATTERNSseeds a handful of well-known shapes (remote-download-piped-to-shell, decode-then-execute, credential-path reads, outbound exfiltration POSTs, chmod+exec of a downloaded file, and two Python-specific decode-then-execute idioms) — not a general-purpose malicious-code detector. A script that does something harmful through any other shape is not caught by this phase at all. - Encoded, split, or otherwise obfuscated payloads generally. Beyond the narrow base64-decode-then-execute shapes these scanners do catch (one for markdown, three for scripts — a shell pipeline and two Python idioms), a payload assembled from pieces, or hidden behind an encoding these patterns don't anticipate, is not caught.
Related
| Page | What it covers |
|---|---|
| Governance Controls Checklist | The house style for stating partial coverage precisely, applied across every control this product ships |
| Graph Guardrails | The deterministic detector taxonomy this scanner's discipline follows |
| Policies & Enforcement | Where enforcement (as opposed to reporting) actually lives in this product |
| VirusTotal Integration | Opt-in, hash-only known-malware lookup for skill-bundled scripts — setup, budget/pacing, and posture-score effect |