husk-scanner 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- husk_scanner-0.1.0/LICENSE +21 -0
- husk_scanner-0.1.0/PKG-INFO +238 -0
- husk_scanner-0.1.0/README.md +208 -0
- husk_scanner-0.1.0/pyproject.toml +99 -0
- husk_scanner-0.1.0/setup.cfg +4 -0
- husk_scanner-0.1.0/src/husk/__init__.py +15 -0
- husk_scanner-0.1.0/src/husk/cli.py +130 -0
- husk_scanner-0.1.0/src/husk/llm_review.py +213 -0
- husk_scanner-0.1.0/src/husk/package_scanner.py +141 -0
- husk_scanner-0.1.0/src/husk/pickle_scanner.py +135 -0
- husk_scanner-0.1.0/src/husk/sandbox.py +556 -0
- husk_scanner-0.1.0/src/husk/skill_scanner.py +2030 -0
- husk_scanner-0.1.0/src/husk/taint_analysis.py +505 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/PKG-INFO +238 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/SOURCES.txt +24 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/dependency_links.txt +1 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/entry_points.txt +2 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/requires.txt +6 -0
- husk_scanner-0.1.0/src/husk_scanner.egg-info/top_level.txt +1 -0
- husk_scanner-0.1.0/tests/test_cli.py +170 -0
- husk_scanner-0.1.0/tests/test_llm_review.py +113 -0
- husk_scanner-0.1.0/tests/test_package_scanner.py +101 -0
- husk_scanner-0.1.0/tests/test_pickle_scanner.py +93 -0
- husk_scanner-0.1.0/tests/test_sandbox.py +230 -0
- husk_scanner-0.1.0/tests/test_scanner.py +79 -0
- husk_scanner-0.1.0/tests/test_taint_analysis.py +184 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: husk-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Static security scanner for AI agent skill packages — built and adversarially self-tested against documented bypass techniques, validated against real confirmed-malicious samples.
|
|
5
|
+
Author: Adam
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YOUR-USERNAME/husk
|
|
8
|
+
Project-URL: Repository, https://github.com/YOUR-USERNAME/husk
|
|
9
|
+
Project-URL: Benchmark, https://github.com/YOUR-USERNAME/husk/blob/main/BENCHMARK.md
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/YOUR-USERNAME/husk/issues
|
|
11
|
+
Keywords: security,ai-agents,skill-packages,static-analysis,malware-detection,mcp,claude,llm-security
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Husk
|
|
32
|
+
|
|
33
|
+
[](https://github.com/YOUR-USERNAME/husk/actions/workflows/tests.yml)
|
|
34
|
+
|
|
35
|
+
A static security scanner for AI agent skill packages — built to specifically defend against bypass techniques that were shown, in published 2026 security research, to defeat production scanners from Snyk, Cisco, and Vercel's skills.sh.
|
|
36
|
+
|
|
37
|
+
## Benchmark — how it compares
|
|
38
|
+
|
|
39
|
+
Real numbers, real datasets, no cherry-picking: full results and
|
|
40
|
+
methodology in [BENCHMARK.md](BENCHMARK.md).
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
At matched precision (each tool's strictest reading), Husk has the
|
|
45
|
+
best combination of recall and precision of every real competitor
|
|
46
|
+
tested — and it's the only tool near the "usable as an automated gate"
|
|
47
|
+
corner:
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
| | Recall (real malicious) | Precision (real legitimate) |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| **Husk** | **61.5–64.4% (full datasets, 11,470 real samples)** | **98.0%** |
|
|
54
|
+
| Best competitor at matched precision | ~57% | 81–94% |
|
|
55
|
+
| Best competitor raw recall (loose mode, high false positives) | 92.5% | 22–59% |
|
|
56
|
+
|
|
57
|
+
Snyk's own Agent Scan is excluded from the numeric comparison — its
|
|
58
|
+
real analysis endpoint returns 403 even with a valid account token
|
|
59
|
+
(documented in BENCHMARK.md), so no real detection number was
|
|
60
|
+
obtainable, not because it performed worse.
|
|
61
|
+
|
|
62
|
+
## Why this exists
|
|
63
|
+
|
|
64
|
+
AI agents can now install "skills" — reusable packages that tell an agent how to accomplish tasks, often bundling instructions with executable code. This is a fast-growing, under-protected attack surface: research published by Cisco (January 2026) found that roughly a quarter of agent skills across major registries contained at least one security vulnerability, and coordinated malicious-skill campaigns have already compromised thousands of skills across registries like ClawHub.
|
|
65
|
+
|
|
66
|
+
In June 2026, Trail of Bits researchers (published via the Cloud Security Alliance) demonstrated that the existing detection tools protecting these registries — including tools from Snyk, Cisco, and Vercel — could be bypassed using well-understood obfuscation techniques, in most cases in under an hour of effort. Some of these tools rely on an LLM to judge whether a skill is safe, which introduces a further weakness: the judge itself can be talked out of flagging something dangerous.
|
|
67
|
+
|
|
68
|
+
Husk is a response to that specific finding — not a general-purpose scanner, but one built and tested against the exact documented bypass techniques that beat the existing tools.
|
|
69
|
+
|
|
70
|
+
## What it defends against
|
|
71
|
+
|
|
72
|
+
| Technique | What it does | Status |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Whitespace inflation | Pads a file with blank content to push malicious code past a scanner's context limit | Detected |
|
|
75
|
+
| Bytecode / base64 hiding | Encodes the payload so plain-text pattern matching can't read it | Detected, decoded, and recursively re-scanned |
|
|
76
|
+
| Archive indirection | Hides the payload inside a nested archive disguised with an innocent file extension | Detected via real file signatures, not filenames — recurses through nested archives |
|
|
77
|
+
| Prompt-injection against the scanner itself | Talks an LLM-based judge into approving a malicious skill | Not applicable by design — Husk never uses an LLM to make a safety decision; all detection is deterministic pattern analysis |
|
|
78
|
+
| Hidden instructions (prompt injection *targeting the agent*) | Directive language hidden in markdown/HTML comments, invisible on render, instructing the AI agent to act against the user's interest | Detected — verified against a real published example from academic research on 98,380 real-world skills |
|
|
79
|
+
| Credential harvesting | Scans for `.env`, `.pem`, `credentials.json`, SSH keys, etc. and exfiltrates them, often disguised as a backup/CI step | Detected — requires both file-access-to-a-credential-pattern AND network-send capability present, specifically to avoid flagging normal setup docs that just mention `.env` |
|
|
80
|
+
| Fake-prerequisite social engineering | Plain-English instructions telling a human to manually download and run a "required utility" — the actual dominant real-world pattern (86.3% of wild malicious skills per published research) | Detected — added after real-dataset testing revealed modules 1-6 caught 0/8 real malicious samples; now 8/8 |
|
|
81
|
+
| Exfiltration chains | The specific documented sequence: read a file, base64-encode it, send it over the network | Detected as a three-step chain, not a single pattern |
|
|
82
|
+
|
|
83
|
+
## Validated against real-world research, not just self-built test cases
|
|
84
|
+
|
|
85
|
+
Beyond the self-built test suite, Husk's hidden-instruction detector has been verified against an actual documented attack published in "*'Do Not Mention This to the User': Detecting and Understanding Malicious Agent Skills in the Wild*" — a large-scale academic study that confirmed 157 malicious skills out of a 98,380-skill snapshot. The paper's title comes directly from a real malicious skill instructing an agent to silently exfiltrate data. Husk correctly flags that exact pattern, and correctly leaves a normal, honest code comment unflagged.
|
|
86
|
+
|
|
87
|
+
## How detection works, honestly
|
|
88
|
+
|
|
89
|
+
Husk does not use machine learning or an LLM to decide whether something is safe. Every check is deterministic: opcode inspection for pickle files, pattern matching against known-dangerous constructs, real binary signature checks for archives, and a lightweight constant-propagation pass that resolves simple string concatenation before pattern matching, specifically to catch payloads split across variables to dodge plain-text detection.
|
|
90
|
+
|
|
91
|
+
This is a first version. It has been adversarially self-tested — evasion variants were built specifically to try to defeat each check, two real gaps were found in that process (Unicode whitespace padding, and base64 payloads split across multiple short fragments), and both were fixed and re-verified. That process is ongoing; no static scanner is ever a finished, unbeatable thing, and Husk does not claim to be one.
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install git+https://github.com/YOUR-USERNAME/husk.git
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or, for development (editable install, includes the test suite):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/YOUR-USERNAME/husk.git
|
|
103
|
+
cd husk
|
|
104
|
+
pip install -e ".[dev]"
|
|
105
|
+
python3 -m pytest tests/test_scanner.py -v # 13 passed, 2 xfailed (see below)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Scan a single skill file
|
|
112
|
+
husk skill path/to/SKILL.md
|
|
113
|
+
|
|
114
|
+
# Scan a whole skill package (handles nested/disguised archives)
|
|
115
|
+
husk package path/to/skill_package/
|
|
116
|
+
|
|
117
|
+
# Scan a pickle-based model file
|
|
118
|
+
husk model path/to/model.pkl
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Exit code is `0` for SAFE, `1` for FLAGGED — safe to use directly in CI.
|
|
122
|
+
|
|
123
|
+
## Optional: basic dynamic sandbox
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
husk package path/to/skill_package/ --sandbox
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For the strongest isolation level (real filesystem isolation, not just
|
|
130
|
+
network/process), install `bubblewrap` — optional, not a Python
|
|
131
|
+
dependency, and the sandbox degrades gracefully without it:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
sudo apt-get install bubblewrap # Debian/Ubuntu
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Static analysis and the LLM-review layer both READ a skill before
|
|
138
|
+
anything runs. Some real attacks are specifically built to defeat
|
|
139
|
+
that — logic bombs that stay dormant until a condition is met, so
|
|
140
|
+
nothing in the file's text ever reveals the dangerous behavior. This
|
|
141
|
+
flag actually **runs** the package's Python scripts in a restricted,
|
|
142
|
+
observed environment and reports what they did, not just what they say.
|
|
143
|
+
|
|
144
|
+
**Real, honest limits — read this before trusting it**: network and
|
|
145
|
+
filesystem isolation are genuinely kernel-enforced when bubblewrap is
|
|
146
|
+
available (the same underlying technology Flatpak uses in production
|
|
147
|
+
to sandbox untrusted applications) — a real network namespace with no
|
|
148
|
+
route out, and a real filesystem view where unbound paths are
|
|
149
|
+
genuinely invisible (verified directly: a real `FileNotFoundError`,
|
|
150
|
+
not a permission error). Falls back honestly to weaker levels when
|
|
151
|
+
bubblewrap isn't available (network+process isolation via `unshare`,
|
|
152
|
+
or resource-limits-only as a last resort) — every result reports
|
|
153
|
+
exactly which level actually ran via `isolation_level`, never silently
|
|
154
|
+
claiming protection that isn't there. Observation is
|
|
155
|
+
limited to exit code, stdout/stderr, and a filesystem diff — no deep
|
|
156
|
+
syscall tracing. Python, JavaScript, shell, Ruby, Rust, and Go scripts
|
|
157
|
+
are sandboxed (Rust/Go are compiled to a binary first, outside the
|
|
158
|
+
sandbox — the compiler itself needs broader access than a script
|
|
159
|
+
should get — then only the resulting binary's runtime behavior is
|
|
160
|
+
sandboxed; only standalone single-file source with no external crate/
|
|
161
|
+
module dependencies compiles this way, a real v1 limitation reported
|
|
162
|
+
plainly as a compilation note, not silently skipped or treated as a
|
|
163
|
+
security finding either way — the languages an interpreter/compiler is
|
|
164
|
+
actually verified available for in the running environment; an
|
|
165
|
+
unsupported or missing one is skipped cleanly, not silently ignored or
|
|
166
|
+
crashed on). A clean run means
|
|
167
|
+
nothing bad happened *this time*, under *these* inputs — not a
|
|
168
|
+
guarantee the script is safe.
|
|
169
|
+
|
|
170
|
+
## Optional: LLM semantic review — a backup, not the main event
|
|
171
|
+
|
|
172
|
+
Static analysis (above) is Husk's real, primary, measured discipline —
|
|
173
|
+
every number in this README's headline results comes from static
|
|
174
|
+
analysis alone, tested against thousands of real payloads (see
|
|
175
|
+
BENCHMARK.md). That's deliberate: this project's purpose is to prove
|
|
176
|
+
static detection can be built well and reinforced honestly against real
|
|
177
|
+
data, not to lean on a model to do the hard part.
|
|
178
|
+
|
|
179
|
+
That said, static analysis has a real, honest ceiling — some attacks use
|
|
180
|
+
no code and no recognizable pattern at all, only manipulated intent in
|
|
181
|
+
plain language (see `tests/known_misses/`). For exactly those cases,
|
|
182
|
+
and only as a last resort, Husk can optionally ask an LLM for a second
|
|
183
|
+
opinion:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
export ANTHROPIC_API_KEY=your-key-here
|
|
187
|
+
husk skill path/to/SKILL.md --llm-review
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**To be clear about where credit belongs**: when this catches something
|
|
191
|
+
static analysis can't, that result reflects Anthropic's Claude model's
|
|
192
|
+
own reasoning ability, not engineering work done in this project. Husk's
|
|
193
|
+
job here is limited to building the prompt and calling the API — the
|
|
194
|
+
judgment itself is Claude's, and it should be credited as such rather
|
|
195
|
+
than folded into this project's own detection claims.
|
|
196
|
+
|
|
197
|
+
**The honest tradeoff, stated up front**: this is not free, not local,
|
|
198
|
+
and not private. It costs tokens and sends the skill's content to
|
|
199
|
+
Anthropic's API. It never runs unless you explicitly pass `--llm-review`,
|
|
200
|
+
and if no API key is set, it skips cleanly — the free static result is
|
|
201
|
+
never affected either way.
|
|
202
|
+
|
|
203
|
+
### Why this uses your own API key, not a hosted service
|
|
204
|
+
|
|
205
|
+
Husk will never ask for payment, an account, or a login — and this isn't
|
|
206
|
+
an accident or a limitation, it's a deliberate design choice. Your
|
|
207
|
+
`ANTHROPIC_API_KEY` goes straight from your machine to Anthropic's API;
|
|
208
|
+
Husk (and its author) never sees your key, your usage, your bill, or
|
|
209
|
+
your skill content at any point. There's no server in the middle to
|
|
210
|
+
breach, no proprietary code of yours passing through infrastructure
|
|
211
|
+
someone else runs.
|
|
212
|
+
|
|
213
|
+
This is also, honestly, a deliberate non-business decision. Husk isn't
|
|
214
|
+
trying to become a company — it's meant to be the most useful, most
|
|
215
|
+
transparent, most honestly-documented free tool it can be in this space.
|
|
216
|
+
If that changes something might get built on top of this later, but the
|
|
217
|
+
free, local-first, BYOK core described in this README is meant to stay
|
|
218
|
+
exactly that.
|
|
219
|
+
|
|
220
|
+
## What v1 does NOT do yet
|
|
221
|
+
|
|
222
|
+
- No packaged CLI install (`pip install` support) yet
|
|
223
|
+
- No large-scale false-positive testing against a broad set of legitimate skills yet (currently 11 hand-built test cases, 3 clean / 8 flagged, zero false positives so far — but this is a small sample)
|
|
224
|
+
- Archive extraction currently supports ZIP; GZIP/7z/RAR extraction is detected but not yet unpacked
|
|
225
|
+
- Credential-harvesting detection only covers a fixed list of filename patterns (.env, .pem, credentials.json, etc.) — a renamed or unlisted credential file type would be missed
|
|
226
|
+
- Exfiltration-chain detection currently checks for presence of all three steps anywhere in the file, not strict call-order — a coincidental combination could theoretically false-positive, though none has been observed yet
|
|
227
|
+
- **Does not reliably catch novel, semantically-disguised attacks with no code and no recognizable keywords** — tested directly against an independent academic dataset (AgentTrap) with genuinely different attack styles than the campaign used for the "8/8" validation elsewhere in this README, and missed both samples tried (a subtle instruction-blurring attack, and a fake "compliance auditor" that asks in plain prose for secrets to be sent to an external endpoint). This matches independent published research (see BENCHMARK.md) showing static pattern-matching tops out around 13-32% recall on novel/disguised attacks generally — not a Husk-specific flaw, but a real, honest limit of this entire approach. Husk is a fast, free, local first-pass filter, not a complete security boundary.
|
|
228
|
+
|
|
229
|
+
## Research this project is grounded in
|
|
230
|
+
|
|
231
|
+
- Trail of Bits / Cloud Security Alliance, *"AI Agent Skill Scanners: Bypassed Across the Board"* (June 2026)
|
|
232
|
+
- OWASP Agentic Skills Top 10 (v0.5, June 2026)
|
|
233
|
+
- Cisco skill registry security research (January 2026)
|
|
234
|
+
- *"'Do Not Mention This to the User': Detecting and Understanding Malicious Agent Skills in the Wild"* — 98,380-skill academic study, 157 confirmed malicious
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Husk
|
|
2
|
+
|
|
3
|
+
[](https://github.com/YOUR-USERNAME/husk/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
A static security scanner for AI agent skill packages — built to specifically defend against bypass techniques that were shown, in published 2026 security research, to defeat production scanners from Snyk, Cisco, and Vercel's skills.sh.
|
|
6
|
+
|
|
7
|
+
## Benchmark — how it compares
|
|
8
|
+
|
|
9
|
+
Real numbers, real datasets, no cherry-picking: full results and
|
|
10
|
+
methodology in [BENCHMARK.md](BENCHMARK.md).
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
At matched precision (each tool's strictest reading), Husk has the
|
|
15
|
+
best combination of recall and precision of every real competitor
|
|
16
|
+
tested — and it's the only tool near the "usable as an automated gate"
|
|
17
|
+
corner:
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
| | Recall (real malicious) | Precision (real legitimate) |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **Husk** | **61.5–64.4% (full datasets, 11,470 real samples)** | **98.0%** |
|
|
24
|
+
| Best competitor at matched precision | ~57% | 81–94% |
|
|
25
|
+
| Best competitor raw recall (loose mode, high false positives) | 92.5% | 22–59% |
|
|
26
|
+
|
|
27
|
+
Snyk's own Agent Scan is excluded from the numeric comparison — its
|
|
28
|
+
real analysis endpoint returns 403 even with a valid account token
|
|
29
|
+
(documented in BENCHMARK.md), so no real detection number was
|
|
30
|
+
obtainable, not because it performed worse.
|
|
31
|
+
|
|
32
|
+
## Why this exists
|
|
33
|
+
|
|
34
|
+
AI agents can now install "skills" — reusable packages that tell an agent how to accomplish tasks, often bundling instructions with executable code. This is a fast-growing, under-protected attack surface: research published by Cisco (January 2026) found that roughly a quarter of agent skills across major registries contained at least one security vulnerability, and coordinated malicious-skill campaigns have already compromised thousands of skills across registries like ClawHub.
|
|
35
|
+
|
|
36
|
+
In June 2026, Trail of Bits researchers (published via the Cloud Security Alliance) demonstrated that the existing detection tools protecting these registries — including tools from Snyk, Cisco, and Vercel — could be bypassed using well-understood obfuscation techniques, in most cases in under an hour of effort. Some of these tools rely on an LLM to judge whether a skill is safe, which introduces a further weakness: the judge itself can be talked out of flagging something dangerous.
|
|
37
|
+
|
|
38
|
+
Husk is a response to that specific finding — not a general-purpose scanner, but one built and tested against the exact documented bypass techniques that beat the existing tools.
|
|
39
|
+
|
|
40
|
+
## What it defends against
|
|
41
|
+
|
|
42
|
+
| Technique | What it does | Status |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Whitespace inflation | Pads a file with blank content to push malicious code past a scanner's context limit | Detected |
|
|
45
|
+
| Bytecode / base64 hiding | Encodes the payload so plain-text pattern matching can't read it | Detected, decoded, and recursively re-scanned |
|
|
46
|
+
| Archive indirection | Hides the payload inside a nested archive disguised with an innocent file extension | Detected via real file signatures, not filenames — recurses through nested archives |
|
|
47
|
+
| Prompt-injection against the scanner itself | Talks an LLM-based judge into approving a malicious skill | Not applicable by design — Husk never uses an LLM to make a safety decision; all detection is deterministic pattern analysis |
|
|
48
|
+
| Hidden instructions (prompt injection *targeting the agent*) | Directive language hidden in markdown/HTML comments, invisible on render, instructing the AI agent to act against the user's interest | Detected — verified against a real published example from academic research on 98,380 real-world skills |
|
|
49
|
+
| Credential harvesting | Scans for `.env`, `.pem`, `credentials.json`, SSH keys, etc. and exfiltrates them, often disguised as a backup/CI step | Detected — requires both file-access-to-a-credential-pattern AND network-send capability present, specifically to avoid flagging normal setup docs that just mention `.env` |
|
|
50
|
+
| Fake-prerequisite social engineering | Plain-English instructions telling a human to manually download and run a "required utility" — the actual dominant real-world pattern (86.3% of wild malicious skills per published research) | Detected — added after real-dataset testing revealed modules 1-6 caught 0/8 real malicious samples; now 8/8 |
|
|
51
|
+
| Exfiltration chains | The specific documented sequence: read a file, base64-encode it, send it over the network | Detected as a three-step chain, not a single pattern |
|
|
52
|
+
|
|
53
|
+
## Validated against real-world research, not just self-built test cases
|
|
54
|
+
|
|
55
|
+
Beyond the self-built test suite, Husk's hidden-instruction detector has been verified against an actual documented attack published in "*'Do Not Mention This to the User': Detecting and Understanding Malicious Agent Skills in the Wild*" — a large-scale academic study that confirmed 157 malicious skills out of a 98,380-skill snapshot. The paper's title comes directly from a real malicious skill instructing an agent to silently exfiltrate data. Husk correctly flags that exact pattern, and correctly leaves a normal, honest code comment unflagged.
|
|
56
|
+
|
|
57
|
+
## How detection works, honestly
|
|
58
|
+
|
|
59
|
+
Husk does not use machine learning or an LLM to decide whether something is safe. Every check is deterministic: opcode inspection for pickle files, pattern matching against known-dangerous constructs, real binary signature checks for archives, and a lightweight constant-propagation pass that resolves simple string concatenation before pattern matching, specifically to catch payloads split across variables to dodge plain-text detection.
|
|
60
|
+
|
|
61
|
+
This is a first version. It has been adversarially self-tested — evasion variants were built specifically to try to defeat each check, two real gaps were found in that process (Unicode whitespace padding, and base64 payloads split across multiple short fragments), and both were fixed and re-verified. That process is ongoing; no static scanner is ever a finished, unbeatable thing, and Husk does not claim to be one.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install git+https://github.com/YOUR-USERNAME/husk.git
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or, for development (editable install, includes the test suite):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/YOUR-USERNAME/husk.git
|
|
73
|
+
cd husk
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
python3 -m pytest tests/test_scanner.py -v # 13 passed, 2 xfailed (see below)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Scan a single skill file
|
|
82
|
+
husk skill path/to/SKILL.md
|
|
83
|
+
|
|
84
|
+
# Scan a whole skill package (handles nested/disguised archives)
|
|
85
|
+
husk package path/to/skill_package/
|
|
86
|
+
|
|
87
|
+
# Scan a pickle-based model file
|
|
88
|
+
husk model path/to/model.pkl
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Exit code is `0` for SAFE, `1` for FLAGGED — safe to use directly in CI.
|
|
92
|
+
|
|
93
|
+
## Optional: basic dynamic sandbox
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
husk package path/to/skill_package/ --sandbox
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For the strongest isolation level (real filesystem isolation, not just
|
|
100
|
+
network/process), install `bubblewrap` — optional, not a Python
|
|
101
|
+
dependency, and the sandbox degrades gracefully without it:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
sudo apt-get install bubblewrap # Debian/Ubuntu
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Static analysis and the LLM-review layer both READ a skill before
|
|
108
|
+
anything runs. Some real attacks are specifically built to defeat
|
|
109
|
+
that — logic bombs that stay dormant until a condition is met, so
|
|
110
|
+
nothing in the file's text ever reveals the dangerous behavior. This
|
|
111
|
+
flag actually **runs** the package's Python scripts in a restricted,
|
|
112
|
+
observed environment and reports what they did, not just what they say.
|
|
113
|
+
|
|
114
|
+
**Real, honest limits — read this before trusting it**: network and
|
|
115
|
+
filesystem isolation are genuinely kernel-enforced when bubblewrap is
|
|
116
|
+
available (the same underlying technology Flatpak uses in production
|
|
117
|
+
to sandbox untrusted applications) — a real network namespace with no
|
|
118
|
+
route out, and a real filesystem view where unbound paths are
|
|
119
|
+
genuinely invisible (verified directly: a real `FileNotFoundError`,
|
|
120
|
+
not a permission error). Falls back honestly to weaker levels when
|
|
121
|
+
bubblewrap isn't available (network+process isolation via `unshare`,
|
|
122
|
+
or resource-limits-only as a last resort) — every result reports
|
|
123
|
+
exactly which level actually ran via `isolation_level`, never silently
|
|
124
|
+
claiming protection that isn't there. Observation is
|
|
125
|
+
limited to exit code, stdout/stderr, and a filesystem diff — no deep
|
|
126
|
+
syscall tracing. Python, JavaScript, shell, Ruby, Rust, and Go scripts
|
|
127
|
+
are sandboxed (Rust/Go are compiled to a binary first, outside the
|
|
128
|
+
sandbox — the compiler itself needs broader access than a script
|
|
129
|
+
should get — then only the resulting binary's runtime behavior is
|
|
130
|
+
sandboxed; only standalone single-file source with no external crate/
|
|
131
|
+
module dependencies compiles this way, a real v1 limitation reported
|
|
132
|
+
plainly as a compilation note, not silently skipped or treated as a
|
|
133
|
+
security finding either way — the languages an interpreter/compiler is
|
|
134
|
+
actually verified available for in the running environment; an
|
|
135
|
+
unsupported or missing one is skipped cleanly, not silently ignored or
|
|
136
|
+
crashed on). A clean run means
|
|
137
|
+
nothing bad happened *this time*, under *these* inputs — not a
|
|
138
|
+
guarantee the script is safe.
|
|
139
|
+
|
|
140
|
+
## Optional: LLM semantic review — a backup, not the main event
|
|
141
|
+
|
|
142
|
+
Static analysis (above) is Husk's real, primary, measured discipline —
|
|
143
|
+
every number in this README's headline results comes from static
|
|
144
|
+
analysis alone, tested against thousands of real payloads (see
|
|
145
|
+
BENCHMARK.md). That's deliberate: this project's purpose is to prove
|
|
146
|
+
static detection can be built well and reinforced honestly against real
|
|
147
|
+
data, not to lean on a model to do the hard part.
|
|
148
|
+
|
|
149
|
+
That said, static analysis has a real, honest ceiling — some attacks use
|
|
150
|
+
no code and no recognizable pattern at all, only manipulated intent in
|
|
151
|
+
plain language (see `tests/known_misses/`). For exactly those cases,
|
|
152
|
+
and only as a last resort, Husk can optionally ask an LLM for a second
|
|
153
|
+
opinion:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export ANTHROPIC_API_KEY=your-key-here
|
|
157
|
+
husk skill path/to/SKILL.md --llm-review
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**To be clear about where credit belongs**: when this catches something
|
|
161
|
+
static analysis can't, that result reflects Anthropic's Claude model's
|
|
162
|
+
own reasoning ability, not engineering work done in this project. Husk's
|
|
163
|
+
job here is limited to building the prompt and calling the API — the
|
|
164
|
+
judgment itself is Claude's, and it should be credited as such rather
|
|
165
|
+
than folded into this project's own detection claims.
|
|
166
|
+
|
|
167
|
+
**The honest tradeoff, stated up front**: this is not free, not local,
|
|
168
|
+
and not private. It costs tokens and sends the skill's content to
|
|
169
|
+
Anthropic's API. It never runs unless you explicitly pass `--llm-review`,
|
|
170
|
+
and if no API key is set, it skips cleanly — the free static result is
|
|
171
|
+
never affected either way.
|
|
172
|
+
|
|
173
|
+
### Why this uses your own API key, not a hosted service
|
|
174
|
+
|
|
175
|
+
Husk will never ask for payment, an account, or a login — and this isn't
|
|
176
|
+
an accident or a limitation, it's a deliberate design choice. Your
|
|
177
|
+
`ANTHROPIC_API_KEY` goes straight from your machine to Anthropic's API;
|
|
178
|
+
Husk (and its author) never sees your key, your usage, your bill, or
|
|
179
|
+
your skill content at any point. There's no server in the middle to
|
|
180
|
+
breach, no proprietary code of yours passing through infrastructure
|
|
181
|
+
someone else runs.
|
|
182
|
+
|
|
183
|
+
This is also, honestly, a deliberate non-business decision. Husk isn't
|
|
184
|
+
trying to become a company — it's meant to be the most useful, most
|
|
185
|
+
transparent, most honestly-documented free tool it can be in this space.
|
|
186
|
+
If that changes something might get built on top of this later, but the
|
|
187
|
+
free, local-first, BYOK core described in this README is meant to stay
|
|
188
|
+
exactly that.
|
|
189
|
+
|
|
190
|
+
## What v1 does NOT do yet
|
|
191
|
+
|
|
192
|
+
- No packaged CLI install (`pip install` support) yet
|
|
193
|
+
- No large-scale false-positive testing against a broad set of legitimate skills yet (currently 11 hand-built test cases, 3 clean / 8 flagged, zero false positives so far — but this is a small sample)
|
|
194
|
+
- Archive extraction currently supports ZIP; GZIP/7z/RAR extraction is detected but not yet unpacked
|
|
195
|
+
- Credential-harvesting detection only covers a fixed list of filename patterns (.env, .pem, credentials.json, etc.) — a renamed or unlisted credential file type would be missed
|
|
196
|
+
- Exfiltration-chain detection currently checks for presence of all three steps anywhere in the file, not strict call-order — a coincidental combination could theoretically false-positive, though none has been observed yet
|
|
197
|
+
- **Does not reliably catch novel, semantically-disguised attacks with no code and no recognizable keywords** — tested directly against an independent academic dataset (AgentTrap) with genuinely different attack styles than the campaign used for the "8/8" validation elsewhere in this README, and missed both samples tried (a subtle instruction-blurring attack, and a fake "compliance auditor" that asks in plain prose for secrets to be sent to an external endpoint). This matches independent published research (see BENCHMARK.md) showing static pattern-matching tops out around 13-32% recall on novel/disguised attacks generally — not a Husk-specific flaw, but a real, honest limit of this entire approach. Husk is a fast, free, local first-pass filter, not a complete security boundary.
|
|
198
|
+
|
|
199
|
+
## Research this project is grounded in
|
|
200
|
+
|
|
201
|
+
- Trail of Bits / Cloud Security Alliance, *"AI Agent Skill Scanners: Bypassed Across the Board"* (June 2026)
|
|
202
|
+
- OWASP Agentic Skills Top 10 (v0.5, June 2026)
|
|
203
|
+
- Cisco skill registry security research (January 2026)
|
|
204
|
+
- *"'Do Not Mention This to the User': Detecting and Understanding Malicious Agent Skills in the Wild"* — 98,380-skill academic study, 157 confirmed malicious
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "husk-scanner"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Static security scanner for AI agent skill packages — built and adversarially self-tested against documented bypass techniques, validated against real confirmed-malicious samples."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Adam"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"security", "ai-agents", "skill-packages", "static-analysis",
|
|
17
|
+
"malware-detection", "mcp", "claude", "llm-security",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Information Technology",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Topic :: Security",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
"Environment :: Console",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = ["pytest>=7.0", "pytest-cov>=4.0", "ruff>=0.6.0", "mypy>=1.10"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/YOUR-USERNAME/husk"
|
|
37
|
+
Repository = "https://github.com/YOUR-USERNAME/husk"
|
|
38
|
+
Benchmark = "https://github.com/YOUR-USERNAME/husk/blob/main/BENCHMARK.md"
|
|
39
|
+
"Bug Tracker" = "https://github.com/YOUR-USERNAME/husk/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
husk = "husk.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 120
|
|
49
|
+
target-version = "py39"
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = ["E", "F", "W", "I", "UP", "SIM", "S", "BLE", "PL"]
|
|
53
|
+
ignore = [
|
|
54
|
+
# Deliberately, consistently allowed project-wide: this tool needs
|
|
55
|
+
# to degrade gracefully rather than crash on unexpected failures -
|
|
56
|
+
# LLM API calls, sandbox subprocess execution, pickle/base64
|
|
57
|
+
# decoding of untrusted content. Each site is already documented
|
|
58
|
+
# with a comment explaining why a broad catch is the right choice
|
|
59
|
+
# there specifically, not a one-off shortcut.
|
|
60
|
+
"BLE001",
|
|
61
|
+
# Also deliberate: every long line here is a regex pattern or a
|
|
62
|
+
# long, explicit description string for a finding message.
|
|
63
|
+
# Mechanically wrapping these to fit a line-length limit risks
|
|
64
|
+
# introducing a real bug (a misplaced line break inside a regex
|
|
65
|
+
# pattern is exactly the class of mistake this project has been
|
|
66
|
+
# careful to avoid throughout - several real precision bugs this
|
|
67
|
+
# session came from exactly this kind of subtle regex error).
|
|
68
|
+
# Explicit over terse is the deliberate style choice here.
|
|
69
|
+
"E501",
|
|
70
|
+
# Also deliberate: every flagged "magic value" here is either a
|
|
71
|
+
# well-known network/protocol constant (127, 169, 254, 172,
|
|
72
|
+
# 16-31, 192, 168 - the actual RFC 1918 private-IP-range octets,
|
|
73
|
+
# genuinely MORE readable as literals matching the real spec than
|
|
74
|
+
# behind an arbitrarily-named local constant) or a small,
|
|
75
|
+
# self-explanatory detection threshold with surrounding context
|
|
76
|
+
# explaining it. Wrapping each in a locally-scoped constant name
|
|
77
|
+
# would add indirection without adding real clarity.
|
|
78
|
+
"PLR2004",
|
|
79
|
+
# Also deliberate: PLR0911/0912/0915 flag functions with many
|
|
80
|
+
# return statements/branches/lines - expected and appropriate for
|
|
81
|
+
# this project's orchestrator functions (scan_skill_file legitimately
|
|
82
|
+
# chains ~28 sequential detection checks; that's the whole point of
|
|
83
|
+
# the function). Splitting it into artificial sub-functions purely
|
|
84
|
+
# to satisfy a line-count metric, on a 2000+ line file with
|
|
85
|
+
# extensive existing test coverage validating current behavior,
|
|
86
|
+
# would add real refactor risk for no correctness or security
|
|
87
|
+
# benefit - a genuine architectural simplification is a larger,
|
|
88
|
+
# deliberate undertaking, not a lint-driven one.
|
|
89
|
+
"PLR0911", "PLR0912", "PLR0915",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.per-file-ignores]
|
|
93
|
+
# Test files: sys.path manipulation before imports is the established
|
|
94
|
+
# pattern across this test suite (no installed-package assumption),
|
|
95
|
+
# and S101 (no-assert) is a false positive here - pytest's whole
|
|
96
|
+
# mechanism relies on bare assert statements in tests, this rule
|
|
97
|
+
# exists for production code where asserts get stripped in optimized
|
|
98
|
+
# mode.
|
|
99
|
+
"tests/*" = ["E402", "S101"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Husk — a static security scanner for AI agent skill packages.
|
|
3
|
+
|
|
4
|
+
Built and adversarially self-tested against documented 2026 bypass
|
|
5
|
+
techniques, then validated against real confirmed-malicious samples.
|
|
6
|
+
See README.md and BENCHMARK.md for the full story, including honest
|
|
7
|
+
limitations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .package_scanner import scan_package
|
|
11
|
+
from .pickle_scanner import scan_file as scan_pickle_file
|
|
12
|
+
from .skill_scanner import scan_skill_file
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__all__ = ["scan_skill_file", "scan_package", "scan_pickle_file"]
|