Metadata-Version: 2.4
Name: husk-scanner
Version: 1.5.0
Summary: Static security scanner for AI agent skill packages - built and adversarially self-tested against documented bypass techniques, validated against real confirmed-malicious samples.
Author: Adam
License-Expression: AGPL-3.0-or-later
Project-URL: Homepage, https://github.com/ctrl-adam/Husk
Project-URL: Repository, https://github.com/ctrl-adam/Husk
Project-URL: Benchmark, https://github.com/ctrl-adam/Husk/blob/main/BENCHMARK.md
Project-URL: Bug Tracker, https://github.com/ctrl-adam/Husk/issues
Keywords: security,ai-agents,skill-packages,static-analysis,malware-detection,mcp,claude,llm-security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Environment :: Console
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: esprima>=4.0
Requires-Dist: pypdf>=4.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: reportlab>=4.0; extra == "dev"
Provides-Extra: sign
Requires-Dist: sigstore>=2.0; extra == "sign"
Dynamic: license-file

# Husk

**A lifecycle security layer for AI agent skills.** Most tools check a skill once - at publish, or at install. Husk covers the whole lifecycle, and does the things a server-side AI review structurally can't: run *before* publish in your own CI, produce *deterministic, verifiable* proof of what it found, compare a skill *across marketplaces*, and keep watching *after* install for a malicious update.

| Stage | Command | What it does that a point-in-time AI review can't |
|---|---|---|
| **Before publish** | `husk gate` | One deterministic pass/fail, offline, in CI on every push - no API cost |
| **At publish** | `husk attest` | A signed, reproducible [in-toto](https://github.com/in-toto/attestation) attestation bound to the skill's exact bytes |
| **Across registries** | `husk crossref` | Detects the same skill serving *different content* on different marketplaces (a substitution attack) |
| **After install** | `husk watch` | Alerts when an installed skill that was safe turns malicious after an update |
| **Any time** | `husk explain` | Shows the exact chain of lines behind a finding (`line 2 fetches ... -> line 2 saves it to /tmp/p -> line 3 runs /tmp/p with sh`) - the auditable *why* |

Underneath is an **analysis engine that models what code does, not what it looks like**: it parses shell (pipes, `$(...)`, `<(...)`, heredocs, variables, files written then run) and Python, follows data from where it came from (a download, a credential file, a base64 decode) to where it goes (an interpreter, the network, a startup file), and reports each finding with the chain of lines that proves it ([how it works](docs/ENGINE.md)). Next to it: AST taint analysis for Python and JavaScript/TypeScript, that **reads bundled PDFs** where attackers hide instructions, and that deterministically catches **no-code prompt-injection** attacks usually assumed to require an LLM. Built to defeat bypass techniques shown, in published 2026 research, to beat production scanners from Snyk, Cisco, and Vercel's skills.sh - benchmarked on **11,224 real malicious samples** (see below), clearing **99.6%** of real legitimate skills. Everything is offline, deterministic, and open-source (AGPL-3.0). No marketplace lock-in, no API keys, no data leaves your machine.

[CHANGELOG](CHANGELOG.md) - what's new in each release, actively maintained.

## Block malicious skills in CI, in 3 lines

`husk gate` turns a scan into one deterministic **pass / fail** decision - offline, in seconds, no API key, no secrets. It's the check to run *before* a skill is published, and in CI on every push. Add this to `.github/workflows/husk.yml`:

```yaml
- uses: actions/checkout@v7
- uses: ctrl-adam/Husk/action@main
  with: { block-on: "high" }
```

That fails the build on clear threats (credential harvesting, C2 IPs, `curl | bash` from untrusted hosts) while letting ordinary skill code through, and comments the result on the PR. Locally it's one command:

```bash
pip install husk-scanner
husk gate ./my-skill            # -> PASS or FAIL + exit code, ready for CI
husk gate ./my-skill --json     # machine-readable, for your own pipeline
```

**Tune the bar per project** with a `.huskpolicy` file in the package:

```json
{
  "block_on": "high",
  "example_paths": ["references/*.md"]
}
```

`example_paths` is for security tools whose docs legitimately quote attack strings - findings in those declared files are reported, not blocking. It's author-declared on purpose: Husk never silently trusts a package just because it *looks* like a security tool (real malware is disguised that way).

**A package can't relax its own gate.** Whoever wrote a skill also wrote its `.huskpolicy` and `.huskignore`, so by default those files can only make the gate *stricter*. Their ignore lists, example paths and looser thresholds apply only when you say the package is yours:

```bash
husk gate ./my-skill --trust-package-policy     # your own repo, your own policy
husk gate ./downloaded-skill --policy team.json # a policy file you control
```

The gate always reports how many findings a policy ignored or demoted. In the GitHub Action, set `trust-package-policy: "true"`, and remember that a pull request from a fork can edit those files.

Also runs as a [pre-commit hook](.pre-commit-hooks.yaml).

## Reproducible attestation: proof an AI review can't give

An LLM review gives a different answer on rerun and can't prove what it looked at. Husk produces a **deterministic, content-bound attestation** in the standard [in-toto Statement v1](https://github.com/in-toto/attestation) / SLSA verification-summary format that supply-chain tooling already understands:

```bash
husk attest ./my-skill --output my-skill.att.json
# ... later, anyone, offline, with no trust in you or any server:
husk verify-attestation ./my-skill my-skill.att.json   # MATCH or MISMATCH
```

The attestation binds a SHA-256 of the skill's exact content to Husk's verdict and the exact ruleset digest that produced it. Re-run `husk attest` on the same skill and you get a byte-identical statement; change one byte of the skill and `verify-attestation` reports MISMATCH. That's a verifiable, auditable record for a compliance report or a registry's provenance trail - something no post-publish AI review can produce. Optional Sigstore keyless signing (`pip install husk-scanner[sign]`, then `husk attest --sign`) adds *who* attested it on top.

## Benchmark - how it compares

Real numbers, real datasets, no cherry-picking: full results and
methodology in [BENCHMARK.md](BENCHMARK.md).

The 2026-09 head-to-head ran **Husk and 16 other skill scanners on the same fixed sample**:
300 confirmed-malicious skills, 249 hand-checked legitimate skills and 150 research-benign
skills, current versions, no LLM, scored by rules fixed before any result was seen.

![Husk vs. 14 skill scanners](assets/precision_recall_tradeoff.png)

| Strict reading, same 699-skill sample | Malware caught | Hand-checked legit passed |
|---|---|---|
| **Husk** | **67.3%** | **99.6%** |
| Next-highest catch rate (syedabbast/skill-scanner) | 64.3% | 86.7% |
| Only tool with the same false-alarm rate (SecureAI-Scan) | 22.3% | 99.6% |
| Cisco AI Defense (200-skill subset, Husk 60.0% / 98.0% there) | 51.6% | 88.0% |
| NVIDIA SkillSpector (200-skill subset) | 58.3% | 65.1% |

Across both datasets in full, Husk catches 64.8-67.5% of 11,224 real malicious samples and
passes 95.3% of a larger 4,000-skill research set. On the newer SkillTrustBench (2,863
malicious, 1,643 normal) it flags 90.7% of the malicious skills, blocks 50.7% outright, and
blocks 1.7% of the normal ones. Husk's known blind spots, found in this
round, are listed in BENCHMARK.md.

Snyk's own Agent Scan is excluded from the numeric comparison - its
real analysis endpoint returns 403 even with a valid account token
(documented in BENCHMARK.md), so no real detection number was
obtainable, not because it performed worse.

## Why this exists

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.

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.

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.

## What it defends against

Husk has grown well past the original bypass techniques it was built to answer. 43 detection functions now, spanning:

| Category | Examples |
|---|---|
| Obfuscation & evasion | Whitespace inflation, base64/bytecode hiding, split-payload evasion, Unicode steganography, disguised archives, self-extracting payloads staged in `.git/` (the SkillCloak technique, arXiv:2607.02357) |
| Data exfiltration | Credential/wallet/browser-password theft, exfiltration chains, DNS covert channels, markdown image beacons, shell command-substitution theft, hardcoded secret literals (Stripe/AWS/GitHub/Slack/Google key formats), tunnel-service exfiltration endpoints (ngrok/serveo/etc) |
| Code & command execution | Dangerous eval/exec, `shell=True`, dropper patterns, reverse shells, language-specific shell execution (Python, PowerShell, Rust, Go, Ruby), dynamic code compilation (`compile()` in eval/exec mode) |
| Prompt injection | Hidden instructions in comments, overt instruction-override language, safety-bypass instructions, bare imperative action-concealment language |
| Social engineering | Fake-prerequisite "required utility" downloads (the dominant real-world attack pattern) |
| Persistence & privilege escalation | Cron/systemd persistence, SUID/SGID bits, Docker socket access, privileged containers, agent self-modification |
| Supply chain | npm postinstall/preinstall hooks, declared-vs-actual capability mismatch, untrusted remote package installs (raw archive URLs instead of registry names) |
| Other | Ransomware behavior, SQL injection (including via query-building helper functions), sensitive-data logging, macOS JXA execution, unconstrained path reads, CPU-bound resource exhaustion |

Every module above is traceable to either published research or a specific real malicious sample found during this project's own testing against 11,224 real confirmed-malicious skills across two independent academic datasets, or against a third, independently-labeled corpus (`cisco-ai-defense/skill-scanner`'s own `evals/`, 13/16 malicious fixtures caught with 0 false positives - see BENCHMARK.md). Full list with the reasoning behind each: [`src/husk/skill_scanner.py`](src/husk/skill_scanner.py).

## Validated against real-world research, not just self-built test cases

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.

## How detection works, honestly

Husk does not use machine learning or an LLM to decide whether something is safe. Every check is deterministic, in two layers:

- **The analysis engine** ([docs/ENGINE.md](docs/ENGINE.md)) parses shell and Python, including code inside Markdown, and tracks labelled data (`remote`, `secret`, `decoded`) to sinks (`exec`, `send`, `persist`). Rules are data in [`engine/rules/core.yml`](src/husk/engine/rules/core.yml); `husk rules -v` lists them, and teams can add their own with `HUSK_RULES=team.yml`. Since it reasons about where data goes rather than how a command is spelled, `bash <(curl ...)`, `sh -c "$(curl ...)"`, `eval "$(curl ...)"` and a download saved and run three code blocks later are all the same finding. On its own it blocks 17-22% of malicious skills at 0.6% false alarms; added to the pattern layer it raised SkillTrustBench blocking from 50.7% to 59.9% with no new false alarms.
- **The pattern layer**: opcode inspection for pickle files, pattern matching against known-dangerous constructs, real binary signature checks for archives, AST taint tracking for Python and JS, 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.

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.

## Install

```bash
pip install husk-scanner
```

For development (editable install, includes the test suite and dev tools):

```bash
git clone https://github.com/ctrl-adam/Husk.git
cd Husk
pip install -e ".[dev]"
python3 -m pytest tests/ -v   # 534 passed, 10 xfailed
```

## Usage

```bash
# The pre-publish gate: one PASS/FAIL decision (see the top of this README)
husk gate path/to/skill_package/

# Scan a single skill file (full findings, not a pass/fail)
husk skill path/to/SKILL.md

# Scan a whole skill package (handles nested/disguised archives)
husk package path/to/skill_package/

# Compare against a marketplace's own verdict (e.g. ClawHub)
husk aggregate owner/skill-name

# Cross-marketplace: is the same skill identical across registries?
husk crossref owner/skill-name

# Watch installed skills over time - alert if one turns malicious after an update
husk watch

# Explain WHY: the exact chain of lines behind each engine and taint finding
husk explain path/to/skill_package/

# List the analysis-engine rules (-v explains each); validate your own rule file
husk rules -v
husk rules --check team-rules.yml

# Scan a whole registry / skills dir incrementally (only changed skills re-scanned)
husk registry ~/.claude/skills

# Produce a signed, reproducible attestation of the scan (see below)
husk attest path/to/skill_package/ --output skill.att.json

# Scan a pickle-based model file
husk model path/to/model.pkl
```

Exit code is `0` for SAFE, `1` for FLAGGED - safe to use directly in CI.

## Optional: basic dynamic sandbox

```bash
husk package path/to/skill_package/ --sandbox
```

For the strongest isolation level (real filesystem isolation, not just
network/process), install `bubblewrap` - optional, not a Python
dependency, and the sandbox degrades gracefully without it:

```bash
sudo apt-get install bubblewrap   # Debian/Ubuntu
```

Static analysis and the LLM-review layer both READ a skill before
anything runs. Some real attacks are specifically built to defeat
that - logic bombs that stay dormant until a condition is met, so
nothing in the file's text ever reveals the dangerous behavior. This
flag actually **runs** the package's Python scripts in a restricted,
observed environment and reports what they did, not just what they say.

**Real, honest limits - read this before trusting it**: network and
filesystem isolation are genuinely kernel-enforced when bubblewrap is
available (the same underlying technology Flatpak uses in production
to sandbox untrusted applications) - a real network namespace with no
route out, and a real filesystem view where unbound paths are
genuinely invisible (verified directly: a real `FileNotFoundError`,
not a permission error). Falls back honestly to weaker levels when
bubblewrap isn't available (network+process isolation via `unshare`,
or resource-limits-only as a last resort) - every result reports
exactly which level actually ran via `isolation_level`, never silently
claiming protection that isn't there. Observation is
limited to exit code, stdout/stderr, and a filesystem diff - no deep
syscall tracing. Python, JavaScript, shell, Ruby, Rust, and Go scripts
are sandboxed (Rust/Go are compiled to a binary first, outside the
sandbox - the compiler itself needs broader access than a script
should get - then only the resulting binary's runtime behavior is
sandboxed; only standalone single-file source with no external crate/
module dependencies compiles this way, a real v1 limitation reported
plainly as a compilation note, not silently skipped or treated as a
security finding either way - the languages an interpreter/compiler is
actually verified available for in the running environment; an
unsupported or missing one is skipped cleanly, not silently ignored or
crashed on). A clean run means
nothing bad happened *this time*, under *these* inputs - not a
guarantee the script is safe.

## CI integration: SARIF output and suppressions

```bash
husk package path/to/skill_package/ --output report.sarif
```

Writes a standard SARIF 2.1.0 report. GitHub Code Scanning (and most
CI security tooling generally) consumes this format directly, so
findings show up in a PR's own Files Changed view and the repo's
Security tab, not just a terminal log.

At Husk's real, honestly-measured false-positive rate (see
BENCHMARK.md, this isn't hidden), running the same scan repeatedly in
CI without a way to say "reviewed, this one's fine" gets a tool
uninstalled fast. Drop a `.huskignore` file in your own package
directory, same convention as `.gitignore`, one rule ID per line, and
run with `--trust-package-policy` (or pass any suppression file with
`--suppress PATH`). A package you didn't write can't silence itself:

```
# .huskignore
A_LITERAL_VALUE_MATCHING_THE_0EBF77
```

Run a scan once, the terminal output already shows each finding, add
the ones you've reviewed and accepted, future scans won't re-flag
them. Honest limitation, stated plainly: rule IDs are derived from
each finding's own stable description text, not built as explicit
per-check identifiers, so a suppression could in principle need
updating if that description text changes in a later version.

### GitHub Action

A ready-to-use composite action wraps the SARIF flow above end to
end: installs Husk, scans, uploads results to your repo's Security
tab, no custom scripting needed. Drop this in
`.github/workflows/husk-scan.yml`:

```yaml
name: Husk Skill Scan

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  security-events: write
  actions: read  # only required for private repositories

jobs:
  husk-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: ctrl-adam/Husk/action@main
        with:
          path: "."
```

Full example with every option, including the optional VirusTotal
check, in [`action/examples/husk-scan.yml`](action/examples/husk-scan.yml).

## Optional: VirusTotal signature check, for the one gap static analysis structurally can't close

Static analysis reads a skill's own text and code - it has no way to
recognize a known-malware *binary* bundled in the package by
signature (found via testing: a package containing the EICAR standard
antivirus test file, correctly unreachable by any code-pattern check
since there's no dangerous language pattern in a signature-based test
file at all). This isn't a Husk-specific gap; no static-analysis tool
can do this, it's a fundamentally different kind of check.

```bash
export VIRUSTOTAL_API_KEY=your-key-here
husk package path/to/skill_package/ --virustotal
```

VirusTotal's public API tier is free, no card required. Same posture
as everything else optional in this project: never runs unless you
explicitly ask, and only a SHA-256 hash of each file is ever sent,
never the file's actual content.

## Optional: LLM semantic review - a backup, not the main event

Static analysis (above) is Husk's real, primary, measured discipline -
every number in this README's headline results comes from static
analysis alone, tested against thousands of real payloads (see
BENCHMARK.md). That's deliberate: this project's purpose is to prove
static detection can be built well and reinforced honestly against real
data, not to lean on a model to do the hard part.

That said, static analysis has a real, honest ceiling - some attacks use
no code and no recognizable pattern at all, only manipulated intent in
plain language (see `tests/known_misses/`). For exactly those cases,
and only as a last resort, Husk can optionally ask an LLM for a second
opinion:

```bash
export ANTHROPIC_API_KEY=your-key-here
husk skill path/to/SKILL.md --llm-review
```

**To be clear about where credit belongs**: when this catches something
static analysis can't, that result reflects the model's own reasoning
ability, not engineering work done in this project. Husk's job here is
limited to building the prompt and calling the API - the judgment
itself is the model's, and it should be credited as such rather than
folded into this project's own detection claims.

Anthropic's Claude is the default and the only provider this project's
own numbers are tested against. If you already have a key with a
different provider, `--llm-provider` lets you use it instead:

```bash
export GEMINI_API_KEY=your-key-here
husk skill path/to/SKILL.md --llm-review --llm-provider gemini
```

Supported: `anthropic` (default), `gemini`, `deepseek`, `grok`, `kimi` -
each reads its own environment variable (`ANTHROPIC_API_KEY`,
`GEMINI_API_KEY`, `DEEPSEEK_API_KEY`, `XAI_API_KEY`, `MOONSHOT_API_KEY`).
This flag exists for convenience, not as a claim that every provider
performs equally here - none of the others have been evaluated against
real data the way Claude has for this project.

**The honest tradeoff, stated up front**: this is not free, not local,
and not private. It costs tokens and sends the skill's content to
whichever provider's API you point it at. It never runs unless you
explicitly pass `--llm-review`, and if no API key is set for the
provider you chose, it skips cleanly - the free static result is
never affected either way.

### Why this uses your own API key, not a hosted service

Husk will never ask for payment, an account, or a login - and this isn't
an accident or a limitation, it's a deliberate design choice. Your
`ANTHROPIC_API_KEY` goes straight from your machine to Anthropic's API;
Husk (and its author) never sees your key, your usage, your bill, or
your skill content at any point. There's no server in the middle to
breach, no proprietary code of yours passing through infrastructure
someone else runs.

This is also, honestly, a deliberate non-business decision. Husk isn't
trying to become a company - it's meant to be the most useful, most
transparent, most honestly-documented free tool it can be in this space.
If that changes something might get built on top of this later, but the
free, local-first, BYOK core described in this README is meant to stay
exactly that.

## What v1 does NOT do yet

- **Real-world recall is 64.8-67.5%, not 100%** (measured against 11,224 real confirmed-malicious samples across two independent datasets - see BENCHMARK.md for the full methodology). The main gap: **novel, semantically-disguised attacks with no code and no recognizable keywords** - a subtle instruction-blurring attack and a fake "compliance auditor" asking in plain prose for secrets, both missed by static analysis and saved openly in `tests/known_misses/` rather than hidden. This matches published research 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. The optional `--llm-review` layer exists specifically for this gap.
- **Two structural limits no pre-execution scanner, static or LLM, can ever close**: Self-Mutating Poisoning (the malicious content doesn't exist in the file until the skill actually runs) and dynamically-generated payloads. Confirmed directly against real samples exhibiting both patterns.
- Archive extraction currently unpacks ZIP; GZIP/7z/RAR are detected (a mismatched-extension archive still gets flagged) but not yet recursively unpacked
- The AST taint tracker follows data flow within a file, including into a function through its parameters, but doesn't re-trace taint propagating deeper inside a callee's own body, and doesn't cross module/file boundaries
- The dynamic sandbox compiles and runs Python, JavaScript, shell, and Ruby directly; Rust and Go are compiled first, but only single-file source with no external crate/module dependencies compiles this way - a real v1 limitation, reported as a plain compilation note rather than silently skipped
- Credential-harvesting detection covers a fixed list of filename patterns (`.env`, `.pem`, `credentials.json`, etc.) - a renamed or unlisted credential file type would be missed

## Research this project is grounded in

- Trail of Bits / Cloud Security Alliance, *"AI Agent Skill Scanners: Bypassed Across the Board"* (June 2026)
- OWASP Agentic Skills Top 10 (v0.5, June 2026)
- Cisco skill registry security research (January 2026)
- *"'Do Not Mention This to the User': Detecting and Understanding Malicious Agent Skills in the Wild"* - 98,380-skill academic study, 157 confirmed malicious

## License

AGPL-3.0-or-later. Changed from an earlier MIT-licensed version
specifically to require that anyone running a modified version of
Husk as a network service (a hosted scanning integration, for
example) makes their modifications available too - use, modify, and
self-host freely; a hosted integration built on top of this code
stays open under the same terms. See [LICENSE](LICENSE) for the full
text.
