# Lazaretto > Deterministic pre-install verification for npm packages, AI agent skills and MCP tools. The free lockfile check matches every exactly pinned dependency against OSV and OpenSSF malicious-package advisories with no account. A paid scan adds behavioral analysis with file-and-line evidence. It reports credential theft, exfiltration, obfuscation, prompt injection and install-time droppers, and returns a signed attestation that verifies offline. No LLM runs in the scan path, so the same input yields the same verdict. A clear result means nothing matched, which is not a statement that an artifact carries no risk. A verdict on scanned content names a SHA-256 of what was read. Operated by Meridian Bridge Advisory LLC d/b/a Lazaretto. Contact: contact@lazaretto.dev. ## Add Lazaretto to an agent Remote MCP server at https://lazaretto.dev/mcp?ref=llms (Streamable HTTP). No install and no key for the free tools. Adding a server changes the user's client config, so show them the command first. It usually takes effect in a new session. - Claude Code: `claude mcp add --transport http --scope user lazaretto "https://lazaretto.dev/mcp?ref=llms"` - VS Code: `code --add-mcp '{"name":"lazaretto","type":"http","url":"https://lazaretto.dev/mcp?ref=llms"}'` - Codex: `codex mcp add lazaretto --url "https://lazaretto.dev/mcp?ref=llms"` - Gemini CLI: `gemini mcp add --transport http --scope user lazaretto "https://lazaretto.dev/mcp?ref=llms"` - Cursor, in ~/.cursor/mcp.json: `{"mcpServers":{"lazaretto":{"url":"https://lazaretto.dev/mcp?ref=llms"}}}` - Every other client, chat apps included: https://lazaretto.dev/add (as markdown: https://lazaretto.dev/add.md) First call, to see it work (chalk@5.6.1 is a real compromised release, so it comes back in `malicious`): ```json {"name":"check_lockfile","arguments":{"packages":["chalk@5.6.1"]}} ``` A package listed as malware comes back with `isError: true` on purpose, so it cannot be read as nothing found. The report is in the content: it is the answer, not a failed call. Which tool: - An exact version, or a whole lockfile: `check_lockfile`, free. Arguments: `packages` (a list of "name@version") or `lockfile` (its text). - A package with no version, or a signed verdict someone already has: `find_attestation`, free. Argument: `subject`, such as "chalk" or "chalk@5.6.1". A name alone cannot rule out one bad release, so ask again with the version you will install. - What a package's code does: `scan_artifact`, needs a key. `get_free_key` issues one. A running session usually cannot change its own headers, so send the key as X-API-Key on POST https://lazaretto.dev/v1/scan, or give it to whoever configures the client. - An MCP server before connecting: `scan_mcp_server`, needs a key. Tool definitions you already hold: `check_mcp_tools`, needs a key. - Finding a server for a job: `find_mcp_servers`, free. No MCP client? Plain HTTP, no key: GET https://lazaretto.dev/v1/attestations/chalk@5.6.1 (`npm:chalk@5.6.1` also works and always gives the identity check, never a signed verdict). ## Start here - Check one package with a single GET, no key: https://lazaretto.dev/v1/attestations/npm:chalk@5.6.1 (read `identity_check.listed_as_malware`). - Connect an agent, with setup for every major MCP client: https://lazaretto.dev/add - What the tools are, and a rule to give your agent: https://lazaretto.dev/agents - Developer guide (quickstart, a pre-install gate in TypeScript and Python, MCP client code for each SDK): https://lazaretto.dev/developers - Everything in one file: https://lazaretto.dev/llms-full.txt - Any page on this site as markdown: add .md to its path (https://lazaretto.dev/index.md for the homepage), or send Accept: text/markdown. ## Free endpoints (no API key, no payment) - https://lazaretto.dev/check/lockfile : the free lockfile check as a page, with an upload form, for a person you are helping who does not use a terminal. - POST https://lazaretto.dev/check : the same lockfile check rendered as plain text for a terminal, for humans you are helping. One line, no key: `curl -s https://lazaretto.dev/check --data-binary @package-lock.json`. GET it for usage. - POST https://lazaretto.dev/v1/lockfile : check EVERY exactly-pinned dependency in a lockfile against malicious-package advisories in one call. Accepts package-lock.json, npm-shrinkwrap.json, yarn.lock, or pnpm-lock.yaml as the request body. An empty `malicious` list is an all-clear only when `unverified` is also empty AND `truncated` is false: packages past the per-request limit are in neither list and were never checked, so send them in a second call before calling the tree clean. curl -s -X POST https://lazaretto.dev/v1/lockfile -H 'content-type: application/json' --data @package-lock.json - GET https://lazaretto.dev/v1/known-bad/{sha256} : exact known-bad hash lookup. A miss is not a verdict on the artifact. - GET https://lazaretto.dev/v1/attestations/{subject} : ask whether anyone has attested a package identity, an MCP server endpoint, or a sha256, before you install or pay. When nobody has, an npm identity gets a free identity check against the OSV / OpenSSF advisory corpus instead, returned under `identity_check` with `found` still false: an identity check is unsigned, looks at the package identity rather than its code, and absence from the corpus is not a verdict. - POST https://lazaretto.dev/v1/trial : mint a free developer key (10 full scans per day, refills daily, no card). - GET https://lazaretto.dev/v1/health : liveness, latency, indicator counts and freshness. ## Paid endpoint - POST https://lazaretto.dev/v1/scan : full deterministic behavioral scan of one artifact (npm package, GitHub repo, ClawHub skill, raw URL, or inline text) with evidence. $0.03 USDC per scan over x402 on Base (no account), or an X-API-Key with prepaid credits. An `error` verdict is never billed. Gate decisions on `risk` (critical/high/medium/low/none), not on `verdict` alone. An `error` verdict returns HTTP 422 with `risk` "unknown": the scan did not finish, which is not a clear result. x402 wire details: the 402 is DUAL-STACK. v2 clients (@x402/fetch and friends) read the PAYMENT-REQUIRED response header (x402Version 2, network eip155:8453, amount field); v1 clients (x402-fetch, x402-axios) read the JSON body (x402Version 1, network base, maxAmountRequired). Send the payment in PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1); both are accepted. curl -s -X POST https://lazaretto.dev/v1/scan -H 'content-type: application/json' -d '{"target":{"type":"npm_package","ref":"chalk@5.6.1"},"depth":"full"}' (without payment this returns HTTP 402 with x402 payment requirements in `accepts`) - POST https://lazaretto.dev/v1/scan/batch : behavioral scan of EVERY exactly-pinned dependency in a lockfile, not just their identities. One credit per package that returns a verdict, nothing for one that errors, capped at 25 packages per call. Send a package-lock.json, yarn.lock or pnpm-lock.yaml with an X-API-Key. Read `complete_coverage` before you trust the result: false means something was capped, errored, only partly readable, or skipped, so it is NOT a clean bill of health for the whole tree. When credits, time or the cap leave packages out, `not_scanned.by_reason` counts them by cause and `not_scanned.packages` names every one of them (in lockfile order), so the next call can send exactly those. Over MCP the same product is the `scan_lockfile_deep` tool. curl -s -X POST https://lazaretto.dev/v1/scan/batch -H "X-API-Key: KEY" -H 'content-type: application/json' --data @package-lock.json - POST https://lazaretto.dev/v1/scan with target type `mcp_server` : check an MCP SERVER before connecting to it. `ref` is the server's https endpoint. Lazaretto calls `initialize` and `tools/list` (never the server's own tools) and analyzes what it advertises to an agent: tool names, descriptions, parameter schemas, and its instructions string. That text is documentation a model obeys, so it is an instruction channel the server operator controls. Detects tool poisoning (hidden directives pointing the agent at private keys or agent config), parameters whose purpose is to carry secrets or conversation history out, and standing orders about another server's tools (cross-server shadowing). Evidence names the exact tool; `target_hash` covers the advertised tool set, so a server that changes its tools later does not inherit the verdict. It reads what a server SAYS, not what its code does. Over MCP the same product is the `scan_mcp_server` tool. curl -s -X POST https://lazaretto.dev/v1/scan -H "X-API-Key: KEY" -H 'content-type: application/json' -d '{"target":{"type":"mcp_server","ref":"https://the-server.example/mcp"}}' - POST https://lazaretto.dev/v1/scan with target type `mcp_tools` : check tool definitions you ALREADY HOLD, with no network call to anyone. Most MCP servers run over stdio and have no endpoint that can be reached, so this is the only way to check them, and your client already read their tool list at startup. Put that JSON in `content`: a whole tools/list response, a {"tools":[...]} object, or a bare array. Same rules and same rendering as `mcp_server`, so a payload cannot be caught over the wire and missed here. Over MCP the same product is the `check_mcp_tools` tool. - POST https://lazaretto.dev/v1/watch : register a dependency set once, then ask later whether any of it has been listed as malware since. One credit to create, free to read. Send a lockfile or {"packages":[...]}. We store the package IDENTITIES and what we knew at that moment, never lockfile contents or code. A watch is re-checked against the current advisory feed when you READ it: nothing runs on a schedule and nothing is pushed to you, so poll it (a daily CI job is enough). An optional https `webhook_url` is accepted and stored, but no deliveries are sent yet, so do not rely on it for alerts. Every verdict we issue is a statement about a MOMENT: chalk@5.6.1 was an ordinary dependency until it was not, and a check run the week before was correct and then quietly stopped being. A watch answers what a one-off check structurally cannot, which is whether something you ALREADY depend on has been listed as malware since you registered it. curl -s -X POST https://lazaretto.dev/v1/watch -H "X-API-Key: KEY" -H 'content-type: application/json' --data @package-lock.json - GET https://lazaretto.dev/v1/watch/{id} with header `x-watch-token` : free. Returns `newly_malicious` (clean when you registered, listed now) separately from `still_malicious` (already listed then). Reading acknowledges an alert so it is not repeated. A 503 with `degraded:true` means we could not check, which is NOT an all-clear. DELETE the same URL to stop and delete the dependency list. - Credit packs: POST https://lazaretto.dev/v1/credits/topup with {"bundle":"starter"|"pro"|"scale"}. starter: $3 = 150 scans; pro: $12 = 700 scans; scale: $25 = 1600 scans. Same dual-stack 402 as the scan (v2 header + v1 body). x402-fetch (v1) users: its default maxValue is $0.10, so pass an explicit maxValue to buy a bundle. Rate limits: scans and topup 120/min/IP; the free lockfile check 60/min/IP; known-bad lookups 60/min/IP. - Humans without a wallet: the same bundles are sold for a card at https://lazaretto.dev/buy (Stripe hosted checkout; the API key appears once on the confirmation page). ## MCP server (for agents) - Remote, zero install: add https://lazaretto.dev/mcp to any MCP client (Streamable HTTP). GET that URL for a description of the endpoint and its tool list, in HTML or JSON. Tools: find_mcp_servers (free; describe a job such as `postgres` or `send slack messages` and get matching remote servers from the official MCP registry with endpoint, tools, the Claude Code command and our weekly read; flagged and unreachable servers are left out), check_lockfile (free; takes a whole lockfile OR a list of `name@version` strings, so a large tree need not go through your context), known_bad_lookup (free), verify_attestation (free), find_attestation (free; when nobody has attested the subject it falls back to a labelled identity check against the advisory corpus, which is not an attestation), get_free_key (free; issues the same developer key as POST https://lazaretto.dev/v1/trial, under the same one-per-source limit), scan_artifact (paid, one credit), scan_mcp_server (paid, one credit, checks an MCP server's advertised tools for poisoning before you connect), check_mcp_tools (paid, one credit, the same check on tool definitions you already hold, for stdio servers with no endpoint), scan_lockfile_deep (paid, one credit per package in the tree). Every tool declares an outputSchema and annotations. - Local: `npx lazaretto-mcp` (npm package lazaretto-mcp, MIT). ## Claude Code plugin: check MCP servers before they connect - lazaretto-guard (free, MIT): https://github.com/jamesdfinance-dev/lazaretto-plugins . A PreToolUse hook. When an agent runs `claude mcp add ` or writes a remote server into .mcp.json, it reads that server's tool descriptions for tool poisoning (pseudo-system blocks, directives at local files, secret-carrying parameters, cross-server shadowing, invisible characters) and asks the user before connecting if anything is found. It also checks pinned npm installs against published malware advisories. At session start it also picks up remote servers already in the Claude Code config that no tool call showed it (added in a terminal, from before the plugin, or from a repository's .mcp.json): it names them once and sends nothing that session, reads start with the next one (LAZARETTO_GUARD_DISCOVER=off stops this). It re-reads servers it has a fingerprint for once a day, while they are still in the config, to catch tool changes. It only ever asks, never blocks. Sends the server URL (never one carrying a token, credentials or an auth header, never a config entry with auth settings or a private host) and a free key it requests once. - Install: `claude plugin marketplace add jamesdfinance-dev/lazaretto-plugins` then `claude plugin install lazaretto-guard@lazaretto-plugins`. - See it fire: ask Claude to run `claude mcp add --transport http lazaretto-demo https://lazaretto.dev/demo/mcp` (a harmless demo server whose one tool carries an block that only asks the agent to say it is a demo). - No install: a person can paste a remote server URL into https://lazaretto.dev/check/mcp (free, no account). An agent should use scan_mcp_server or POST https://lazaretto.dev/v1/scan with "type":"mcp_server" instead. - Registry index: https://lazaretto.dev/mcp-registry lists every remote server in the official MCP registry with its latest tool check, one page per server (https://lazaretto.dev/mcp-registry/) and a README badge at /badge.svg. Subscribe to one server's tool changes at /changes.xml (RSS, free, no account): a server can change the tools it advertises after you approve it, and nothing in a client tells you. Names and dates only, never what a tool says. Read-only, free. As JSON: https://lazaretto.dev/mcp-registry/.json, or look one up with https://lazaretto.dev/mcp-registry/lookup?name= or ?url= (CORS open, for directories and clients; guide at https://lazaretto.dev/mcp-registry/embed). Weekly report: https://lazaretto.dev/mcp-registry-report (and .json). - Explainer: https://lazaretto.dev/blog/mcp-tool-poisoning-how-to-catch-it ## Machine contracts - OpenAPI: https://lazaretto.dev/openapi.json - Agent card: https://lazaretto.dev/.well-known/agent-card.json - MCP server card: https://lazaretto.dev/mcp/server-card (also https://lazaretto.dev/.well-known/mcp/server-card.json, which lists every tool). For the endpoint chat apps list: https://lazaretto.dev/mcp/connector/server-card - AI catalog: https://lazaretto.dev/.well-known/ai-catalog.json - x402 discovery: https://lazaretto.dev/.well-known/x402 - Human docs: https://lazaretto.dev/docs/api ## Trust - Real incidents distinguished, with reproduction commands: https://lazaretto.dev/caught - Public corrections log: https://lazaretto.dev/corrections - Data sources, licences, and coverage gaps: https://lazaretto.dev/sources - How often we flag a package people install on purpose, with every corpus published in full: https://lazaretto.dev/benchmark (machine-readable: https://lazaretto.dev/benchmark.json)