Hex Dependency Audit skill
Audit Hex deps for supply-chain security risk — bidi chars, compile-time exec, maintainer changes, typosquats, CVEs.
by oliver-kriska·MIT license·★ 560 Stars on the repo·GitHub ↗
npx degit oliver-kriska/claude-elixir-phoenix/plugins/elixir-phoenix/skills/deps-audit#main ~/.claude/skills/deps-auditChecked ·commit main
Files of Hex Dependency Audit
Show the full text185 lines
Hex Dependency Audit
Non-mutating supply-chain audit for Hex packages. Runs an 8-rule MVP catalogue
against changed packages, enriches with Hex API metadata, wraps existing tools
(mix hex.audit, mix_audit, OSV-Scanner), and emits a triage table.
When to Use
- After
mix deps.updateormix deps.getbrought in new versions - On PRs that touch
mix.lock(pre-merge gate) - Before manually updating a single package (
--preview <pkg>) - When investigating a dependency you don't recognize
Iron Laws
- NEVER claim a diff is clean without inspecting it. Run all 8 rules
on the unpacked NEW tarball. "Looks fine" without a tool run is a false
pass. Always write
.claude/deps-audit/last-run.json— its absence is evidence the audit didn't actually run. - NEVER install
mix_audit/osv-scanner— even if asked. Detect, warn with install instructions, skip cleanly if missing. If the user says "install it," respond with the install command (e.g.,mix deps.add mix_audit --only dev) and do not execute it. The audit skill is non-mutating;mix.exs/mix.lockare off-limits regardless of consent. - NEVER promote a finding to BLOCK without rule citation. Every finding
shows
rule_id,severity,file:line,snippet,message. No handwaving. - NEVER fetch from Hex API without rate-limiting. Cap at 5 req/sec. Cache metadata 7 days, top-500 list 1 day.
- NEVER run the audit on already-committed lock changes silently —
tell the user which mode (A/B/C) is active and which
(old, new)pairs resolved. - LLM triage only above threshold. Native rules + Semgrep + YARA
are deterministic. The
hex-deps-triageragent runs only when score10 (1 BLOCK or 3+ WARNs), and its verdicts are advisory — never auto-suppress a finding without human review.
Operating Modes
| Mode | Trigger | Old source | New source |
|---|---|---|---|
| B (default) | /phx:deps-audit |
git show HEAD:mix.lock |
working mix.lock |
| C (PR) | /phx:deps-audit --base main |
git show <ref>:mix.lock |
working mix.lock |
| A (preview) | /phx:deps-audit --preview httpoison |
locked version | Hex API latest |
See ${CLAUDE_SKILL_DIR}/references/operating-modes.md for full resolver logic.
Execution Flow
Default = full 8-rule scan with streaming progress. --quick opts out
to CVE + retirement only. See ${CLAUDE_SKILL_DIR}/references/execution-flow.md.
Step 1: Resolve the diff
Parse the mix.lock Erlang term format for both old and new sources. Emit a
list of {pkg, old_version, new_version} tuples. Surface
new-only and removed-only packages separately (a removed package is not
audited; a brand-new package gets old_version = nil and skips diff-only
rules).
See ${CLAUDE_SKILL_DIR}/references/diff-resolver.md for shell + mix run -e snippets per mode and the JSON output contract.
Step 2: Fetch tarballs (per-run tmpdir)
For each (pkg, old, new):
mix hex.package fetch <pkg> <old> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<old>/
mix hex.package fetch <pkg> <new> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<new>/
All ephemeral artifacts live under ${AUDIT_TMPDIR} (driver-owned, removed
on exit). See ${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md and
${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md.
Step 3: Run the 8 MVP rules on each NEW tarball
| # | Rule | Sev | Method |
|---|---|---|---|
| 1 | Bidi Unicode control chars in .ex/.exs/.erl |
BLOCK | grep |
| 2 | Code.eval_* / :erlang.apply with non-literal MFA at module scope |
BLOCK | AST (Sourceror or regex+scope) |
| 3 | System.cmd / :os.cmd / Port.open at compile time |
BLOCK | AST |
| 4 | :erlang.binary_to_term/1 on literal without :safe |
BLOCK | AST |
| 5 | New :git/:path dep in mix.exs (vs old) |
BLOCK | AST diff |
| 6 | Maintainer change between versions | BLOCK | Hex API |
| 7 | Base64 blobs >256 chars outside priv/static/, test/fixtures/, assets/ |
WARN | regex |
| 8 | Levenshtein ≤2 from top-500 + download delta >1000× | BLOCK | Hex API + fuzzy |
Full catalogue (35 rules, MVP marked) in ${CLAUDE_SKILL_DIR}/references/heuristics.md.
Bash + mix run -e implementations for all 8 MVP rules in
${CLAUDE_SKILL_DIR}/references/rules-impl.md (single-pass NEW + diff rules +
Hex API rules, with run_all_rules master loop).
Step 4: External tool wrappers (parallel)
mix hex.audit— retired-package check, always availablemix_audit— CVE check via GHSA, if installed (else warn + skip; do NOT install)osv-scanner— CVE check via OSV.dev, if installed (else warn + skip; do NOT install)
See ${CLAUDE_SKILL_DIR}/references/external-tools.md for detection, output parsing, and severity mapping per tool.
Step 5: Hex API enrichment (per package)
GET /api/packages/:name— owners, downloads, inserted_atGET /api/packages/:name/releases/:version— per-release publisher- Compute:
days_since_publish,owner_age_days,download_velocity
Cap at 5 req/sec. Per-run cache under ${AUDIT_TMPDIR}/hex-api/.
See ${CLAUDE_SKILL_DIR}/references/hex-api.md for endpoint contracts,
caching strategy, Rule 6/8 detection, and Levenshtein implementation.
Step 5.5: Apply hex_vet.exs ledger (if present)
If hex_vet.exs exists at project root, vetted-version findings are
downgraded to INFO. Unvetted versions retain their severity.
Lock-vs-ledger disagreement: lock wins. See the deps-vet skill's
hex-vet schema doc for the "Lock-vs-ledger disagreement" section.
Use /phx:deps-vet <pkg> <version> (separate skill) to add entries.
Step 5.7: Differential subtract
When run with DIFFERENTIAL=1 (default), findings that existed in the
OLD tarball are downgraded to INFO. Net-new signals reach the renderer
at full severity. See ${CLAUDE_SKILL_DIR}/references/differential.md.
Step 5.8: LLM triage (when score > threshold)
For packages where the aggregate score exceeds 10, the
hex-deps-triager sonnet agent reads finding + diff windows and
produces structured verdicts (confidence, verdict, rationale,
fp_reasons[]). A context-supervisor consolidates verdicts
across packages into triage/consolidated.md. Main skill reads only
the consolidated file.
See ${CLAUDE_SKILL_DIR}/references/llm-triage.md.
Step 6: Score & render
Per-package weighted sum: BLOCK = 10, WARN = 3, INFO = 1. Risk band: 0 clean · 1–5 low · 6–15 medium · 16+ high.
Output:
- Stdout: markdown table —
pkg | old → new | risk | findings | diff.hex.pm | maintainer-changeplus a per-package detail section for any non-clean row. - Sidecar (MANDATORY): Write
.claude/deps-audit/last-run.json. The Phase 3 gate reads this; an audit that doesn't write it is a no-op for the gate. Always emit, even on clean runs.
--json flag emits JSON to stdout instead of markdown. See
${CLAUDE_SKILL_DIR}/references/output-renderer.md for table format,
sidecar schema, exit-code rubric, and --quiet mode.
Out of scope / Phase 3 surface
- NEVER modify
mix.lock,mix.exs, or any project file (non-mutating) - NEVER auto-install missing tools (warn + skip)
- Gate
mix deps.{get,update,compile}viadeps-audit-gate.sh. See${CLAUDE_SKILL_DIR}/references/hook.md. - Prompt for
/phx:compoundafter BLOCK findings — corpus self-feeds. - Emit SARIF 2.1.0 via
--sarif <path>and gate CI via--ci.
References
${CLAUDE_SKILL_DIR}/references/heuristics.md— full 35-rule catalogue${CLAUDE_SKILL_DIR}/references/rules-impl.md— bash +mix run -efor the 8 MVP rules${CLAUDE_SKILL_DIR}/references/operating-modes.md— Mode A/B/C resolver${CLAUDE_SKILL_DIR}/references/diff-resolver.md— shell snippets, lock parser${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md— fetch wrapper, parallel cap, cache prune${CLAUDE_SKILL_DIR}/references/external-tools.md—mix_audit,osv-scannerwrappers${CLAUDE_SKILL_DIR}/references/hex-api.md— endpoint contracts, rate limit, Rule 6/8${CLAUDE_SKILL_DIR}/references/output-renderer.md— markdown, JSON v1, exit codes, SARIF${CLAUDE_SKILL_DIR}/references/testing.md— smoke runner, fixture matrix${CLAUDE_SKILL_DIR}/references/differential.md/llm-triage.md— Phase 2 NDJSON subtract + triager${CLAUDE_SKILL_DIR}/references/semgrep.md/yara.md— Phase 2 precision layers (soft deps)${CLAUDE_SKILL_DIR}/references/cassettes.md/sarif.md/hook.md/ci-integration.md— Phase 3 surface${CLAUDE_SKILL_DIR}/references/trusted-publishers.md/skill-checklist.md— upstream + eval${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md— Phase 5 per-run ephemeral storage contract${CLAUDE_SKILL_DIR}/references/execution-flow.md/differential-cve.md— Phase 5 default scan + CVE diff
| 1 | |
| 2 | name deps-audit |
| 3 | description Audit Hex deps for supply-chain security risk — bidi chars, compile-time exec, maintainer changes, typosquats, CVEs. Use after mix deps.update, when checking if a package upgrade is safe, or reviewing mix.lock PR diffs. |
| 4 | effort medium |
| 5 | argument-hint "[--base <ref> | --preview [pkg...]] [--quick] [--json] [--sarif <path>] [--ci] [--strict] [--no-differential] [--no-llm | --llm] [--trace]" |
| 6 | |
| 7 | |
| 8 | # Hex Dependency Audit |
| 9 | |
| 10 | Non-mutating supply-chain audit for Hex packages. Runs an 8-rule MVP catalogue |
| 11 | against changed packages, enriches with Hex API metadata, wraps existing tools |
| 12 | (`mix hex.audit`, `mix_audit`, OSV-Scanner), and emits a triage table. |
| 13 | |
| 14 | ## When to Use |
| 15 | |
| 16 | After `mix deps.update` or `mix deps.get` brought in new versions |
| 17 | On PRs that touch `mix.lock` (pre-merge gate) |
| 18 | Before manually updating a single package (`--preview <pkg>`) |
| 19 | When investigating a dependency you don't recognize |
| 20 | |
| 21 | ## Iron Laws |
| 22 | |
| 23 | **NEVER claim a diff is clean without inspecting it.** Run all 8 rules |
| 24 | on the unpacked NEW tarball. "Looks fine" without a tool run is a false |
| 25 | pass. **Always write `.claude/deps-audit/last-run.json`** — its absence |
| 26 | is evidence the audit didn't actually run. |
| 27 | **NEVER install `mix_audit` / `osv-scanner` — even if asked.** Detect, |
| 28 | warn with install instructions, skip cleanly if missing. If the user |
| 29 | says "install it," respond with the install command (e.g., |
| 30 | `mix deps.add mix_audit --only dev`) and **do not execute it**. The |
| 31 | audit skill is non-mutating; `mix.exs` / `mix.lock` are off-limits |
| 32 | regardless of consent. |
| 33 | **NEVER promote a finding to BLOCK without rule citation.** Every finding |
| 34 | shows `rule_id`, `severity`, `file:line`, `snippet`, `message`. No |
| 35 | handwaving. |
| 36 | **NEVER fetch from Hex API without rate-limiting.** Cap at 5 req/sec. |
| 37 | Cache metadata 7 days, top-500 list 1 day. |
| 38 | **NEVER run the audit on already-committed lock changes silently** — |
| 39 | tell the user which mode (A/B/C) is active and which `(old, new)` pairs |
| 40 | resolved. |
| 41 | **LLM triage only above threshold.** Native rules + Semgrep + YARA |
| 42 | are deterministic. The `hex-deps-triager` agent runs only when score |
| 43 | > 10 (1 BLOCK or 3+ WARNs), and its verdicts are advisory — never |
| 44 | auto-suppress a finding without human review. |
| 45 | |
| 46 | ## Operating Modes |
| 47 | |
| 48 | | Mode | Trigger | Old source | New source | |
| 49 | |------|---------|-----------|-----------| |
| 50 | | **B** (default) | `/phx:deps-audit` | `git show HEAD:mix.lock` | working `mix.lock` | |
| 51 | | **C** (PR) | `/phx:deps-audit --base main` | `git show <ref>:mix.lock` | working `mix.lock` | |
| 52 | | **A** (preview) | `/phx:deps-audit --preview httpoison` | locked version | Hex API latest | |
| 53 | |
| 54 | See `${CLAUDE_SKILL_DIR}/references/operating-modes.md` for full resolver logic. |
| 55 | |
| 56 | ## Execution Flow |
| 57 | |
| 58 | Default = full 8-rule scan with streaming progress. `--quick` opts out |
| 59 | to CVE + retirement only. See `${CLAUDE_SKILL_DIR}/references/execution-flow.md`. |
| 60 | |
| 61 | ### Step 1: Resolve the diff |
| 62 | |
| 63 | Parse the `mix.lock` Erlang term format for both old and new sources. Emit a |
| 64 | list of `{pkg, old_version, new_version}` tuples. Surface |
| 65 | new-only and removed-only packages separately (a removed package is not |
| 66 | audited; a brand-new package gets `old_version = nil` and skips diff-only |
| 67 | rules). |
| 68 | |
| 69 | See `${CLAUDE_SKILL_DIR}/references/diff-resolver.md` for shell + `mix run -e` snippets per mode and the JSON output contract. |
| 70 | |
| 71 | ### Step 2: Fetch tarballs (per-run tmpdir) |
| 72 | |
| 73 | For each `(pkg, old, new)`: |
| 74 | |
| 75 | |
| 76 | mix hex.package fetch <pkg> <old> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<old>/ |
| 77 | mix hex.package fetch <pkg> <new> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<new>/ |
| 78 | |
| 79 | |
| 80 | All ephemeral artifacts live under `${AUDIT_TMPDIR}` (driver-owned, removed |
| 81 | on exit). See `${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md` and |
| 82 | `${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md`. |
| 83 | |
| 84 | ### Step 3: Run the 8 MVP rules on each NEW tarball |
| 85 | |
| 86 | | # | Rule | Sev | Method | |
| 87 | |---|------|-----|--------| |
| 88 | | 1 | Bidi Unicode control chars in `.ex`/`.exs`/`.erl` | BLOCK | grep | |
| 89 | | 2 | `Code.eval_*` / `:erlang.apply` with non-literal MFA at module scope | BLOCK | AST (Sourceror or regex+scope) | |
| 90 | | 3 | `System.cmd` / `:os.cmd` / `Port.open` at compile time | BLOCK | AST | |
| 91 | | 4 | `:erlang.binary_to_term/1` on literal without `:safe` | BLOCK | AST | |
| 92 | | 5 | New `:git`/`:path` dep in `mix.exs` (vs old) | BLOCK | AST diff | |
| 93 | | 6 | Maintainer change between versions | BLOCK | Hex API | |
| 94 | | 7 | Base64 blobs >256 chars outside `priv/static/`, `test/fixtures/`, `assets/` | WARN | regex | |
| 95 | | 8 | Levenshtein ≤2 from top-500 + download delta >1000× | BLOCK | Hex API + fuzzy | |
| 96 | |
| 97 | Full catalogue (35 rules, MVP marked) in `${CLAUDE_SKILL_DIR}/references/heuristics.md`. |
| 98 | Bash + `mix run -e` implementations for all 8 MVP rules in |
| 99 | `${CLAUDE_SKILL_DIR}/references/rules-impl.md` (single-pass NEW + diff rules + |
| 100 | Hex API rules, with `run_all_rules` master loop). |
| 101 | |
| 102 | ### Step 4: External tool wrappers (parallel) |
| 103 | |
| 104 | `mix hex.audit` — retired-package check, always available |
| 105 | `mix_audit` — CVE check via GHSA, if installed (else warn + skip; do NOT install) |
| 106 | `osv-scanner` — CVE check via OSV.dev, if installed (else warn + skip; do NOT install) |
| 107 | |
| 108 | See `${CLAUDE_SKILL_DIR}/references/external-tools.md` for detection, output parsing, and severity mapping per tool. |
| 109 | |
| 110 | ### Step 5: Hex API enrichment (per package) |
| 111 | |
| 112 | `GET /api/packages/:name` — owners, downloads, inserted_at |
| 113 | `GET /api/packages/:name/releases/:version` — per-release publisher |
| 114 | Compute: `days_since_publish`, `owner_age_days`, `download_velocity` |
| 115 | |
| 116 | Cap at 5 req/sec. Per-run cache under `${AUDIT_TMPDIR}/hex-api/`. |
| 117 | See `${CLAUDE_SKILL_DIR}/references/hex-api.md` for endpoint contracts, |
| 118 | caching strategy, Rule 6/8 detection, and Levenshtein implementation. |
| 119 | |
| 120 | ### Step 5.5: Apply `hex_vet.exs` ledger (if present) |
| 121 | |
| 122 | If `hex_vet.exs` exists at project root, vetted-version findings are |
| 123 | **downgraded to INFO**. Unvetted versions retain their severity. |
| 124 | Lock-vs-ledger disagreement: lock wins. See the deps-vet skill's |
| 125 | hex-vet schema doc for the "Lock-vs-ledger disagreement" section. |
| 126 | |
| 127 | Use `/phx:deps-vet <pkg> <version>` (separate skill) to add entries. |
| 128 | |
| 129 | ### Step 5.7: Differential subtract |
| 130 | |
| 131 | When run with `DIFFERENTIAL=1` (default), findings that existed in the |
| 132 | OLD tarball are downgraded to INFO. Net-new signals reach the renderer |
| 133 | at full severity. See `${CLAUDE_SKILL_DIR}/references/differential.md`. |
| 134 | |
| 135 | ### Step 5.8: LLM triage (when score > threshold) |
| 136 | |
| 137 | For packages where the aggregate score exceeds 10, the |
| 138 | `hex-deps-triager` sonnet agent reads finding + diff windows and |
| 139 | produces structured verdicts (`confidence`, `verdict`, `rationale`, |
| 140 | `fp_reasons[]`). A `context-supervisor` consolidates verdicts |
| 141 | across packages into `triage/consolidated.md`. Main skill reads only |
| 142 | the consolidated file. |
| 143 | |
| 144 | See `${CLAUDE_SKILL_DIR}/references/llm-triage.md`. |
| 145 | |
| 146 | ### Step 6: Score & render |
| 147 | |
| 148 | Per-package weighted sum: BLOCK = 10, WARN = 3, INFO = 1. |
| 149 | Risk band: 0 clean · 1–5 low · 6–15 medium · 16+ high. |
| 150 | |
| 151 | Output: |
| 152 | |
| 153 | **Stdout:** markdown table — `pkg | old → new | risk | findings | diff.hex.pm | maintainer-change` plus a per-package detail section for any non-clean row. |
| 154 | **Sidecar (MANDATORY):** Write `.claude/deps-audit/last-run.json`. The Phase 3 gate reads this; an audit that doesn't write it is a no-op for the gate. Always emit, even on clean runs. |
| 155 | |
| 156 | `--json` flag emits JSON to stdout instead of markdown. See |
| 157 | `${CLAUDE_SKILL_DIR}/references/output-renderer.md` for table format, |
| 158 | sidecar schema, exit-code rubric, and `--quiet` mode. |
| 159 | |
| 160 | ## Out of scope / Phase 3 surface |
| 161 | |
| 162 | **NEVER modify** `mix.lock`, `mix.exs`, or any project file (non-mutating) |
| 163 | **NEVER auto-install** missing tools (warn + skip) |
| 164 | **Gate** `mix deps.{get,update,compile}` via `deps-audit-gate.sh`. See `${CLAUDE_SKILL_DIR}/references/hook.md`. |
| 165 | **Prompt** for `/phx:compound` after BLOCK findings — corpus self-feeds. |
| 166 | **Emit** SARIF 2.1.0 via `--sarif <path>` and gate CI via `--ci`. |
| 167 | |
| 168 | ## References |
| 169 | |
| 170 | `${CLAUDE_SKILL_DIR}/references/heuristics.md` — full 35-rule catalogue |
| 171 | `${CLAUDE_SKILL_DIR}/references/rules-impl.md` — bash + `mix run -e` for the 8 MVP rules |
| 172 | `${CLAUDE_SKILL_DIR}/references/operating-modes.md` — Mode A/B/C resolver |
| 173 | `${CLAUDE_SKILL_DIR}/references/diff-resolver.md` — shell snippets, lock parser |
| 174 | `${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md` — fetch wrapper, parallel cap, cache prune |
| 175 | `${CLAUDE_SKILL_DIR}/references/external-tools.md` — `mix_audit`, `osv-scanner` wrappers |
| 176 | `${CLAUDE_SKILL_DIR}/references/hex-api.md` — endpoint contracts, rate limit, Rule 6/8 |
| 177 | `${CLAUDE_SKILL_DIR}/references/output-renderer.md` — markdown, JSON v1, exit codes, SARIF |
| 178 | `${CLAUDE_SKILL_DIR}/references/testing.md` — smoke runner, fixture matrix |
| 179 | `${CLAUDE_SKILL_DIR}/references/differential.md` / `llm-triage.md` — Phase 2 NDJSON subtract + triager |
| 180 | `${CLAUDE_SKILL_DIR}/references/semgrep.md` / `yara.md` — Phase 2 precision layers (soft deps) |
| 181 | `${CLAUDE_SKILL_DIR}/references/cassettes.md` / `sarif.md` / `hook.md` / `ci-integration.md` — Phase 3 surface |
| 182 | `${CLAUDE_SKILL_DIR}/references/trusted-publishers.md` / `skill-checklist.md` — upstream + eval |
| 183 | `${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md` — Phase 5 per-run ephemeral storage contract |
| 184 | `${CLAUDE_SKILL_DIR}/references/execution-flow.md` / `differential-cve.md` — Phase 5 default scan + CVE diff |
| 185 |
Discussion
Browse more free Claude skills.