Claude Skill

phx-deps-vet

Record a vetted Hex package version in hex_vet.exs after a security review

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-targets_amp_skills_phx-deps-vet-9767a82.zip · 11 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/targets/amp/skills/phx-deps-vet
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Deps Vet — Hex package audit ledger

Review a Hex package version, run Phase 1 supply-chain rules against it, prompt the user for a verdict, append the result to hex_vet.exs (project-root audit ledger). Vetted versions get downgraded to INFO on subsequent phx-deps-audit runs.

Run this AFTER phx-deps-audit to clear findings. Run this BEFORE merging a mix.lock PR to certify new versions.

Usage

phx-deps-vet phoenix 1.7.21      # vet a single package version
phx-deps-vet --seed              # import curated baseline seed (~30 pkgs)
phx-deps-vet --list              # show existing ledger entries
phx-deps-vet --check             # cross-check mix.lock vs ledger

Iron Laws

  1. NEVER auto-approve. Every entry MUST come from an AskUserQuestion confirmation. Drive-by trust ruins the ledger's value.
  2. Lock wins on disagreement. If mix.lock has version X and the ledger vets X-1, emit INFO and treat X as unvetted. Don't silently trust the older entry.
  3. Ledger lives at project root. hex_vet.exs is a first-class security artifact, visible in PR review. Don't move it into .claude/.
  4. Round-trip via inspect/2. When appending, read the file with Code.eval_file/1, mutate the map, and write back via inspect(term, pretty: true, limit: :infinity). Hand-rolled string appends drift over time.
  5. Always show findings before prompting. The user must see what's being vetted. No silent :safe_to_deploy defaults.
  6. Confirmation counts are COMPUTED, never estimated. Any number in an AskUserQuestion (criteria split, new/overwrite/no-op) MUST be derived from the loaded data before prompting — e.g. Enum.frequencies_by(seed.audits, & &1.criteria). Eyeballing the file and approving on wrong numbers corrupts the consent.

Execution flow

Step 1: Locate or seed hex_vet.exs

If hex_vet.exs exists at project root:
    Read it via Code.eval_file/1
Else:
    Write the empty-ledger stub (see references/hex-vet.md §"Empty ledger")
    Inform user: "Created hex_vet.exs at project root."

Step 2: Branch by mode

  • <pkg> <version> → single-vet path (Step 3-7).
  • --seed → import priv/hex_vet_seed.exs. Before prompting, Code.eval_file/1 the seed and compute (Iron Law #6): the criteria split (Enum.frequencies_by(seed.audits, & &1.criteria)) and, against any existing ledger, exact new / overwrite / no-op counts. Put those computed numbers in the AskUserQuestion. Also state up front that the seed is a provenance baseline, not certification of your current mix.lock (per Iron Law #2, seed versions older than the locked ones stay unvetted). Ask before overwriting existing entries.
  • --list → render the audits table; exit.
  • --check → compare ledger entries with mix.lock; warn on drift. Read the lock via Code.eval_file("mix.lock") with 2>/dev/null — modern locks have quoted keys and emit a found quoted keyword warning per package (tens of KB of noise that gets persisted as an oversized tool result otherwise).

Step 3: Fetch the tarball (single-vet)

Run the deps-audit corpus loader. Cache lives at ~/.cache/phx-deps-audit/corpus/<pkg>/<version>/contents/. Use:

bash ../phx-deps-audit/scripts/fetch_tarball.sh \
    <pkg> <version>

Step 4: Run Phase 1 rules

Source the rules from ../phx-deps-audit/references/rules-impl.md. Run run_all_rules over the cached dir. Write findings to a temp vet-findings.jsonl. Set FINDINGS_FILE to override default path.

Step 5: Present findings

Print the findings table per ../phx-deps-audit/references/output-renderer.md. On zero findings: say "No findings — vet from a clean baseline." On any finding: show severity, file, line, snippet inline.

Step 6: Prompt for verdict

Call AskUserQuestion with these 4 options:

  • :safe_to_deploy — full trust; findings investigated and cleared.
  • :safe_to_run — trust in non-production envs only (test deps).
  • :does_not_implement_crypto — Mozilla-style sub-criterion.
  • Skip — defer decision; don't write an entry.

If any finding is BLOCK severity: default-highlight Skip. Require explicit override before writing :safe_to_deploy over a BLOCK.

Step 7: Append to ledger

Read existing hex_vet.exs via Code.eval_file/1. Append the audit map below to :audits. Write back via Code.format_string!(inspect(...)).

%{
  package: "<pkg>",
  version: "<version>",
  criteria: <verdict_atom>,
  reviewer: "<git config user.email>",
  notes: "<user-provided one-liner OR findings summary>",
  reviewed_at: ~D[<today>]
}

Write back via Code.format_string!(inspect(term, pretty: true)). Confirm to user: "Added <pkg> <version> to hex_vet.exs."

Integration

  • Run after phx-deps-audit to clear vetted findings.
  • Run before merging a mix.lock PR to certify new versions.
  • Run phx-deps-vet --check to detect ledger drift vs mix.lock.
  • phx-deps-audit auto-downgrades vetted findings to INFO.
  • policy.block_on_unvetted is enforced by the plugin's deps-audit-gate.sh PreToolUse hook on mix deps.get / mix deps.update.

References

  • references/hex-vet.md — schema, parser, lookup
  • references/seed.md — --seed flag, curated baseline
  • ../phx-deps-audit/references/rules-impl.md — the same rules phx-deps-audit runs

Out of scope (Phase 3+)

  • Mix task surface — defer mix phx.deps_vet to a separate Hex package phx_deps_vet for non-CC users.
  • Distributed imports — defer cargo-vet imports: until trust-chain semantics are designed.
Files (claude-elixir-phoenix)
  • priv
    • hex_vet_seed.exs 7.4 KB · in bundle
  • references
    • hex-vet.md 11.2 KB
      # `hex_vet.exs` — schema, parser, and lookup
      
      The audit ledger. Lives at **project root**, alongside `mix.exs` and
      `mix.lock`. Modeled on cargo-vet's `audits.toml` — same trust-chain
      intent, idiomatic Elixir surface.
      
      ## Why project root, not `.claude/`
      
      `hex_vet.exs` is a **deliverable security artifact**, not an ephemeral
      sidecar. Three properties of root placement that `.claude/` doesn't
      give us:
      
      1. **Visible in PR review.** Adding a vetted dep shows up as a diff
         line on the same file as `mix.lock`, prompting the reviewer to
         look at both.
      2. **CI-discoverable without configuration.** The triple
         `mix.lock` / `mix.exs` / `hex_vet.exs` is recognizable —
         security tooling can find the ledger without project-specific
         config.
      3. **Survives `.claude/` deletion.** Some teams treat `.claude/` as
         per-developer state and gitignore it. The audit ledger has to be
         shared.
      
      Phase 1's `last-run.json` stays under `.claude/deps-audit/`
      intentionally — that file is ephemeral run state, not durable trust.
      
      ## Schema
      
      ```elixir
      # hex_vet.exs
      %{
        imports: %{
          # Phase 3+ feature — distributed audit imports. Ignored in Phase 2.
          # mozilla: "https://hg.mozilla.org/.../audits.toml"
        },
        audits: [
          %{
            package: "phoenix",
            version: "1.7.21",
            criteria: :safe_to_deploy,
            reviewer: "oliver@ideax.sk",
            notes: "Reviewed against rules 1-8; diff.hex.pm checked clean.",
            reviewed_at: ~D[2026-05-12]
          },
          %{
            package: "jason",
            version: "1.4.4",
            criteria: :safe_to_deploy,
            reviewer: "team@example.com",
            notes: "No findings; widely-used (>50M downloads).",
            reviewed_at: ~D[2026-05-10]
          }
        ],
        policy: %{
          criteria_required: :safe_to_deploy,
          block_on_unvetted: :new_only  # Phase 3 default; see Tri-mode section
        }
      }
      ```
      
      ### Tri-mode `block_on_unvetted` (Phase 3)
      
      `block_on_unvetted` takes one of four modes (a legacy boolean is
      normalized — see "Migration from Phase 2 boolean" below):
      
      | Mode | Hook behavior | When to pick |
      |------|---------------|--------------|
      | `false` | Warn-only; `mix deps.get` exits 0 | Phase 2 compat; opt-out |
      | `:new_only` | Block if PR ADDS an unvetted version; allow re-locks of already-locked unvetted pkgs | **Recommended default** for new projects |
      | `:strict` | Block ANY `mix deps.get` while any locked version is unvetted | Mature ledger; enforce on whole graph |
      | `:full` | Run Tier 2 audit pipeline (Semgrep + YARA + LLM) then apply `:strict` rules | High-stakes (financial, healthcare); accept 30-90s per `mix deps.get` |
      
      **Why `:new_only` is the default:** strict mode fails CI on
      existing repos before seed import; warn is too loose for teams
      serious about supply-chain. `:new_only` blocks the specific risk
      (introducing an unvetted dep) without holding the team hostage on
      historical un-audited locks.
      
      ### Migration from Phase 2 boolean
      
      The hook reads `policy.block_on_unvetted` and normalizes:
      
      ```elixir
      defp normalize_block_mode(false), do: false
      defp normalize_block_mode(true) do
        IO.warn("""
        block_on_unvetted: true is deprecated and will be removed in v4.0.
        Replaced with :strict (same semantics). For new projects, consider
        :new_only — blocks only NEW unvetted versions, allows re-locks.
        """)
        :strict
      end
      defp normalize_block_mode(mode) when mode in [:new_only, :strict, :full], do: mode
      defp normalize_block_mode(other) do
        raise "Invalid block_on_unvetted: #{inspect(other)} — must be one of: false, :new_only, :strict, :full"
      end
      ```
      
      The one-time `IO.warn` surfaces in `mix deps.get` output. Migrate
      to `:strict` (drop-in) or `:new_only` (recommended) to silence.
      
      ### `:new_only` semantics
      
      "New" means: the package+version pair in the **current** `mix.lock`
      was NOT in `mix.lock` at the hook's reference commit (default:
      `origin/main`, override via `PHX_DEPS_AUDIT_BASE`). The reference
      diff isolates additions:
      
      ```bash
      git show "${PHX_DEPS_AUDIT_BASE:-origin/main}":mix.lock 2>/dev/null \
        > /tmp/mix.lock.base
      diff <(awk '/^  "[^"]+":/' /tmp/mix.lock.base) \
           <(awk '/^  "[^"]+":/' mix.lock) \
        | grep '^>' | sed 's/^> *//'
      ```
      
      Each added line is one `<pkg>: <version>` pair; block if any added
      pair is missing from `audits`. Re-locks of already-locked unvetted
      pkgs are ignored — those are pre-existing tech debt, not new risk.
      
      ### Criteria atoms
      
      Following cargo-vet's vocabulary, three Phase 2 criteria are
      recognized:
      
      | Atom | Meaning |
      |------|---------|
      | `:safe_to_deploy` | Reviewed; safe in production. Highest trust. |
      | `:safe_to_run` | Safe in non-production envs (test/dev deps). |
      | `:does_not_implement_crypto` | Sub-criterion; package contains no cryptographic implementation, so transitive crypto-review obligations don't apply. |
      
      Other atoms are valid but unrecognized — Phase 2 treats them as a
      softer match (logged, never trusted).
      
      ### Empty ledger stub
      
      Used when `hex_vet.exs` doesn't exist. New ledgers default to
      `:new_only` (Phase 3) — opt-in to enforcement on the additions
      without blocking historical un-audited locks:
      
      ```elixir
      %{
        imports: %{},
        audits: [],
        policy: %{criteria_required: :safe_to_deploy, block_on_unvetted: :new_only}
      }
      ```
      
      ## Parser
      
      Use `Code.eval_file/1` — Elixir's own parser, no Sourceror needed:
      
      ```bash
      # One-line read:
      mix run --no-mix-exs -e '
        {ledger, _} = Code.eval_file("hex_vet.exs")
        IO.inspect(ledger.audits, limit: :infinity)
      '
      ```
      
      Inside a skill script the same call works via `mix run -e`. For lookup
      performance, the ledger is small (target: <2,000 entries; 50K LOC
      file). No streaming parser needed.
      
      ### Lookup function
      
      ```elixir
      def vetted?(ledger, pkg, version, required \\ :safe_to_deploy) do
        Enum.any?(ledger.audits, fn audit ->
          audit.package == pkg and
            audit.version == version and
            meets_criteria?(audit.criteria, required)
        end)
      end
      
      defp meets_criteria?(:safe_to_deploy, _required), do: true
      defp meets_criteria?(:safe_to_run, :safe_to_run), do: true
      defp meets_criteria?(other, other), do: true
      defp meets_criteria?(_, _), do: false
      ```
      
      `:safe_to_deploy` satisfies every requirement (deploy implies run).
      `:safe_to_run` only satisfies `:safe_to_run`.
      
      ## Lock-vs-ledger disagreement (Day-1 decision: lock wins)
      
      When `mix.lock` says `phoenix 1.7.21` and the ledger has an entry for
      `phoenix 1.7.20`:
      
      - The unmatched lock version (1.7.21) is **unvetted**.
      - The orphaned ledger entry (1.7.20) is **informational** — emit an
        INFO finding "ledger entry exists for older version 1.7.20; treating
        1.7.21 as unvetted."
      - The audit runs the full Phase 1 rule pass on 1.7.21 with normal
        severities.
      
      This is the conservative call. The alternative ("lock-version-or-higher
      trust") would let attackers exploit version-bump attacks where the
      ledger entry was approved on a safe version.
      
      ## Append flow
      
      Round-trip through `inspect/2` to preserve Elixir term semantics:
      
      ```bash
      mix run --no-mix-exs -e '
        {ledger, _} = Code.eval_file("hex_vet.exs")
        new_audit = %{
          package: "<pkg>",
          version: "<ver>",
          criteria: :safe_to_deploy,
          reviewer: System.cmd("git", ["config", "user.email"]) |> elem(0) |> String.trim(),
          notes: "<notes>",
          reviewed_at: Date.utc_today()
        }
        updated = Map.update!(ledger, :audits, &[new_audit | &1])
        formatted = updated
                    |> inspect(pretty: true, limit: :infinity, width: 80)
                    |> Code.format_string!()
                    |> IO.iodata_to_binary()
        File.write!("hex_vet.exs", formatted <> "\n")
      '
      ```
      
      `Code.format_string!/1` ensures the output matches the project's
      formatter config (incl. `.formatter.exs` overrides). Test the
      round-trip on a fixture before relying on it — version pinning matters.
      
      ## Migration from existing trust artifacts
      
      For projects using ad-hoc trust mechanisms (a comment in `mix.exs`,
      README sections, internal wiki pages), the seed-import flow
      (`phx-deps-vet --seed`) lets a team bootstrap a real ledger from
      the top-100 list and then layer in project-specific audits.
      
      The seed is regenerated monthly; entries older than 90 days emit a
      stale-warning. See `seed.md` for the regeneration job.
      
      ## Distributed imports (Phase 3 — single-source v1)
      
      cargo-vet supports trusting other organizations' audit ledgers via
      the `imports:` table. Phase 3 ships **explicit allow-list v1**: any
      import URL listed in `imports` must be opted into per-project — no
      implicit trust, no transitive imports.
      
      ### Schema
      
      ```elixir
      imports: %{
        # key = canonical handle, value = ledger URL
        "elixir-phoenix-plugin" =>
          "https://raw.githubusercontent.com/oliver-kriska/claude-elixir-phoenix/main/plugins/elixir-phoenix/skills/deps-vet/priv/hex_vet_seed.exs"
      }
      ```
      
      The handle (left side) is the attribution the renderer uses when a
      finding is downgraded via an imported audit: "vetted via
      `elixir-phoenix-plugin` (imported)". The URL (right side) must
      resolve to a file with the same `hex_vet.exs` map shape (`audits:` is
      the only key consumed; `imports:` of the imported ledger is ignored
      to prevent transitive trust chains).
      
      ### v1 allow-list
      
      Phase 3 v1 hardcodes the recognized import set:
      
      ```elixir
      @allowed_imports %{
        "elixir-phoenix-plugin" =>
          "https://raw.githubusercontent.com/oliver-kriska/claude-elixir-phoenix/main/plugins/elixir-phoenix/skills/deps-vet/priv/hex_vet_seed.exs"
      }
      ```
      
      Imports listed in `hex_vet.exs` that aren't in `@allowed_imports` are
      **ignored with stderr warning**, never silently trusted. Multi-org
      imports (Phoenix team, EEF, etc.) stay drafted until the trust-chain
      semantics are battle-tested through one full cycle of single-import
      production use.
      
      ### Fetch + cache
      
      Imports fetch on first use and cache 24h under
      `.claude/deps-audit/cache/imports/<handle>.exs`. On cache miss or
      TTL expiry, re-fetch via `curl -fsSL`. Fetch failures fall back to
      the cached copy with a "stale import" stderr warning.
      
      ### Lookup precedence
      
      ```text
      project audits         (hex_vet.exs `audits:`)
              ↓ not found
      imported audits        (each allowed import, parallel)
              ↓ not found
      unvetted               (Phase 1 rules apply at full severity)
      ```
      
      A local audit always wins over an import — explicit project trust
      is more durable than implicit shared trust. Conflicts (local
      `:safe_to_run` vs import `:safe_to_deploy`) resolve to local.
      
      ### Attribution in output
      
      The renderer surfaces import provenance:
      
      ```text
      phoenix 1.7.21 — vetted via elixir-phoenix-plugin (imported), :safe_to_deploy
      plug    1.16.1 — vetted locally (oliver@ideax.sk, 2026-05-12), :safe_to_deploy
      unknown 0.1.0  — unvetted (no local or imported audit)
      ```
      
      Reviewers see exactly where the trust came from. Out-of-policy
      imports (anything not in the allow-list) get a one-time stderr
      warning `ignored import: <handle> — not in v1 allow-list` but
      never silently downgrade severity.
      
      ### Publishing your org's audit ledger
      
      The plugin's seed file is the reference shape. Mirror the pattern:
      
      1. Maintain an `hex_vet.exs` (or any `.exs` returning the same map)
         in a repository your team controls.
      2. Open a PR to add your URL to the plugin's `@allowed_imports`.
      3. Add a `README` covering: review process, criteria meaning for
         your org, signing/checksums.
      4. Cross-reference: your `hex_vet.exs` lists `imports:` of
         `elixir-phoenix-plugin` for symmetry.
      
      Allow-list reviews enforce trust-chain hygiene: a malicious import
      URL would compromise every plugin user. The plugin maintainers are
      the final gate.
      
    • seed.md 4.4 KB
      # Seed ledger — curated baseline of vetted Hex packages
      
      `priv/hex_vet_seed.exs` ships with the plugin and provides a
      **curated baseline of audits** for a hand-picked set of high-trust,
      high-download Hex packages (currently ~30 — the monthly CI job in
      "Regeneration" grows this toward the top-100).
      
      It is a **provenance baseline, not certification of your project's
      current `mix.lock`.** Seed versions are pinned; per `hex_vet.exs`
      Iron Law #2 (lock wins), any locked version *newer* than the seed
      entry stays unvetted. On an up-to-date stack the seed may certify
      few or none of your actual locked deps — surface this to the user
      *before* import, not after. The seed's lasting value is the trusted
      provenance record + `:new_only` enforcement of *future* additions.
      
      ## Iron Laws (seed-specific)
      
      1. **Seed entries are NOT a substitute for project-specific review.**
         They're a baseline; project-aware audits still take precedence.
      2. **Stale seeds warn.** If `reviewed_at` is more than 90 days old at
         import time, emit a warning. Stale trust is more dangerous than
         no trust.
      3. **Seed import is opt-in.** `phx-deps-vet --seed` requires an
         explicit `AskUserQuestion` confirmation; never silent.
      
      ## Schema
      
      Same map shape as `hex_vet.exs`, but lives at
      `../priv/hex_vet_seed.exs`:
      
      ```elixir
      # Auto-generated by .github/workflows/seed-regen.yml on YYYY-MM-DD.
      # Reviewer: <CI identity>.
      %{
        generated_at: ~D[2026-05-12],
        source: "https://hex.pm/api/packages?sort=downloads",
        audits: [
          %{package: "jason",   version: "1.4.4",  criteria: :safe_to_deploy, ...},
          %{package: "phoenix", version: "1.7.21", criteria: :safe_to_deploy, ...},
          # ... ~28 more
        ]
      }
      ```
      
      ## Import flow
      
      ```text
      phx-deps-vet --seed
      
        Step 1: Code.eval_file priv/hex_vet_seed.exs.
        Step 2: Check generated_at; warn if > 90 days.
        Step 3: COMPUTE before prompting (Iron Law #6):
                  - total = length(seed.audits)
                  - split = Enum.frequencies_by(seed.audits, & &1.criteria)
                  - vs existing ledger: new / overwrite / no-op counts
        Step 4: AskUserQuestion (numbers are the computed values, never
                estimates):
                  "Import <total> curated audits into hex_vet.exs?
                   - <split[:safe_to_deploy]> :safe_to_deploy,
                     <split[:safe_to_run]> :safe_to_run (+ any others)
                   - <new> new · <overwrite> overwrite · <no-op> no-op
                   Note: provenance baseline — seed versions are pinned;
                   newer locked versions stay unvetted (lock wins)."
        Step 5: On Yes: dedupe + merge + write back via Code.format_string!.
                On No: exit without modification.
        Step 6: Confirm: "Imported <N> entries; ledger now has <total>."
      ```
      
      Existing audits never get silently overwritten — overwrites surface
      in the confirmation prompt.
      
      ## Regeneration
      
      Monthly CI job (`.github/workflows/seed-regen.yml`):
      
      1. Query `https://hex.pm/api/packages?sort=downloads` for top-100.
      2. For each, run Phase 1 rules + check Hex retirement.
      3. Compose audit entries (criteria: `:safe_to_deploy` if zero BLOCKs).
      4. Open a PR to update `priv/hex_vet_seed.exs`.
      
      The job runs under the plugin's CI identity; the PR is human-reviewed
      before merge. No auto-merge for security artifacts.
      
      ### Org-policy 403 fallback
      
      Some org GitHub policies deny `GITHUB_TOKEN` PR creation. The seed-
      regen and cassette-regen workflows both apply the same fallback
      pattern when `peter-evans/create-pull-request@v6` exits with 403:
      
      1. Upload the regenerated artifact (`priv/hex_vet_seed.exs` for seed,
         the cassettes tree for cassettes) as a workflow artifact.
      2. Write a job summary explaining the policy and pointing to
         `Settings → Actions → Allow GitHub Actions to create and approve
         pull requests`.
      3. Exit 0 — the regen succeeded even if the PR didn't.
      
      Maintainers download the artifact, commit manually. The fallback is
      the same in `cassette-regen.yml`; see `../../phx-deps-audit/references/cassettes.md`
      "403 fallback".
      
      ## When to NOT use the seed
      
      - Air-gapped or fork-restricted environments. The seed assumes
        upstream Hex; if your registry is internal-only, audit your local
        catalog instead.
      - Hard-trust corporate-controlled packages — for those, project-level
        audits with internal reviewer emails are more legible in PR review.
      
      ## Future: distributed imports
      
      cargo-vet's `imports:` field lets organizations trust **each other's**
      audits via signed bundles. Phase 2 stubs the `imports:` key in the
      schema but ignores its contents. Phase 3+ wiring TBD.
      
  • SKILL.md 5.8 KB
    ---
    name: phx-deps-vet
    description: Record a vetted Hex package version in hex_vet.exs after a security review
      — manages the audit ledger, not the scanner. Use to approve a dep after phx-deps-audit
      findings or to initialize hex_vet.exs.
    ---
    
    # Deps Vet — Hex package audit ledger
    
    Review a Hex package version, run Phase 1 supply-chain rules against it,
    prompt the user for a verdict, append the result to `hex_vet.exs`
    (project-root audit ledger). Vetted versions get downgraded to `INFO`
    on subsequent `phx-deps-audit` runs.
    
    Run this AFTER `phx-deps-audit` to clear findings.
    Run this BEFORE merging a `mix.lock` PR to certify new versions.
    
    ## Usage
    
    ```text
    phx-deps-vet phoenix 1.7.21      # vet a single package version
    phx-deps-vet --seed              # import curated baseline seed (~30 pkgs)
    phx-deps-vet --list              # show existing ledger entries
    phx-deps-vet --check             # cross-check mix.lock vs ledger
    ```
    
    ## Iron Laws
    
    1. **NEVER auto-approve.** Every entry MUST come from an `AskUserQuestion`
       confirmation. Drive-by trust ruins the ledger's value.
    2. **Lock wins on disagreement.** If `mix.lock` has version X and the
       ledger vets X-1, emit INFO and treat X as unvetted. Don't silently
       trust the older entry.
    3. **Ledger lives at project root.** `hex_vet.exs` is a first-class
       security artifact, visible in PR review. Don't move it into `.claude/`.
    4. **Round-trip via `inspect/2`.** When appending, read the file with
       `Code.eval_file/1`, mutate the map, and write back via
       `inspect(term, pretty: true, limit: :infinity)`. Hand-rolled string
       appends drift over time.
    5. **Always show findings before prompting.** The user must see what's
       being vetted. No silent `:safe_to_deploy` defaults.
    6. **Confirmation counts are COMPUTED, never estimated.** Any number in
       an `AskUserQuestion` (criteria split, new/overwrite/no-op) MUST be
       derived from the loaded data *before* prompting — e.g.
       `Enum.frequencies_by(seed.audits, & &1.criteria)`. Eyeballing the
       file and approving on wrong numbers corrupts the consent.
    
    ## Execution flow
    
    ### Step 1: Locate or seed `hex_vet.exs`
    
    ```text
    If hex_vet.exs exists at project root:
        Read it via Code.eval_file/1
    Else:
        Write the empty-ledger stub (see references/hex-vet.md §"Empty ledger")
        Inform user: "Created hex_vet.exs at project root."
    ```
    
    ### Step 2: Branch by mode
    
    - **`<pkg> <version>`** → single-vet path (Step 3-7).
    - **`--seed`** → import `priv/hex_vet_seed.exs`. Before prompting,
      `Code.eval_file/1` the seed and **compute** (Iron Law #6): the
      `criteria` split (`Enum.frequencies_by(seed.audits, & &1.criteria)`)
      and, against any existing ledger, exact new / overwrite / no-op
      counts. Put those computed numbers in the `AskUserQuestion`. Also
      state up front that the seed is a **provenance baseline, not
      certification of your current `mix.lock`** (per Iron Law #2, seed
      versions older than the locked ones stay unvetted). Ask before
      overwriting existing entries.
    - **`--list`** → render the audits table; exit.
    - **`--check`** → compare ledger entries with `mix.lock`; warn on
      drift. Read the lock via `Code.eval_file("mix.lock")` with
      **`2>/dev/null`** — modern locks have quoted keys and emit a
      `found quoted keyword` warning per package (tens of KB of noise that
      gets persisted as an oversized tool result otherwise).
    
    ### Step 3: Fetch the tarball (single-vet)
    
    Run the deps-audit corpus loader. Cache lives at
    `~/.cache/phx-deps-audit/corpus/<pkg>/<version>/contents/`. Use:
    
    ```text
    bash ../phx-deps-audit/scripts/fetch_tarball.sh \
        <pkg> <version>
    ```
    
    ### Step 4: Run Phase 1 rules
    
    Source the rules from `../phx-deps-audit/references/rules-impl.md`.
    Run `run_all_rules` over the cached dir. Write findings to a temp
    `vet-findings.jsonl`. Set `FINDINGS_FILE` to override default path.
    
    ### Step 5: Present findings
    
    Print the findings table per `../phx-deps-audit/references/output-renderer.md`.
    On zero findings: say "No findings — vet from a clean baseline."
    On any finding: show severity, file, line, snippet inline.
    
    ### Step 6: Prompt for verdict
    
    Call `AskUserQuestion` with these 4 options:
    
    - **`:safe_to_deploy`** — full trust; findings investigated and cleared.
    - **`:safe_to_run`** — trust in non-production envs only (test deps).
    - **`:does_not_implement_crypto`** — Mozilla-style sub-criterion.
    - **`Skip`** — defer decision; don't write an entry.
    
    If any finding is BLOCK severity: default-highlight `Skip`. Require
    explicit override before writing `:safe_to_deploy` over a BLOCK.
    
    ### Step 7: Append to ledger
    
    Read existing `hex_vet.exs` via `Code.eval_file/1`. Append the audit
    map below to `:audits`. Write back via
    `Code.format_string!(inspect(...))`.
    
    ```elixir
    %{
      package: "<pkg>",
      version: "<version>",
      criteria: <verdict_atom>,
      reviewer: "<git config user.email>",
      notes: "<user-provided one-liner OR findings summary>",
      reviewed_at: ~D[<today>]
    }
    ```
    
    Write back via `Code.format_string!(inspect(term, pretty: true))`.
    Confirm to user: "Added `<pkg>` `<version>` to hex_vet.exs."
    
    ## Integration
    
    - **Run after** `phx-deps-audit` to clear vetted findings.
    - **Run before** merging a `mix.lock` PR to certify new versions.
    - **Run `phx-deps-vet --check`** to detect ledger drift vs `mix.lock`.
    - **`phx-deps-audit`** auto-downgrades vetted findings to INFO.
    - **`policy.block_on_unvetted`** is enforced by the plugin's `deps-audit-gate.sh`
      PreToolUse hook on `mix deps.get` / `mix deps.update`.
    
    ## References
    
    - `references/hex-vet.md` — schema, parser, lookup
    - `references/seed.md` — `--seed` flag, curated baseline
    - `../phx-deps-audit/references/rules-impl.md` — the
      same rules `phx-deps-audit` runs
    
    ## Out of scope (Phase 3+)
    
    - **Mix task surface** — defer `mix phx.deps_vet` to a separate Hex
      package `phx_deps_vet` for non-CC users.
    - **Distributed imports** — defer cargo-vet `imports:` until
      trust-chain semantics are designed.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related