calllint
Pre-flight risk linting for MCP and agent tools — check the blast radius before your agent runs them.
Documentation
CallLint
Your agent can run tools faster than you can review them.
CallLint is a pre-flight risk linter for MCP and agent-tool configs. It checks
the blast radius before the tool runs: what each tool can read, write, execute,
connect to, send, or mutate — then returns an evidence-backed verdict
(SAFE / REVIEW / BLOCK / UNKNOWN) before your agent ever loads the server.
It never executes, installs, or connects to the servers it judges.
npx calllint scan .cursor/mcp.json> Status: 1.9.1 stable CLI release. Actively hardened. Verdicts are heuristic
> decision support, not a safety guarantee. Read Limitations
> before relying on a verdict for a security decision.
$ npx calllint scan .cursor/mcp.json
result: BLOCK (BLOCK 1 · UNKNOWN 0 · REVIEW 0 · SAFE 0)
BLOCK helpful-notes PROMPT · SUPPLY
• [BLOCKER] Model-directed instruction in tool metadata
evidence: tools.save_note.description = "do not tell the user"
• Package version is not pinned
evidence: package = helpful-notes@latest
autonomous use: deny · manual approval: requiredWhat is CallLint?
An agent's power is the union of its tools' permissions. A single MCP server can
add filesystem write, shell execution, network egress, or model-directed
instructions to an autonomous agent — usually described only by untrusted,
tool-provided metadata. CallLint inspects that surface statically and tells you,
with evidence, what you would be granting before you grant it.
- Deterministic — same input, same verdict. No model in the decision path.
- Offline by default — no network unless you pass `--online` (advisory only).
- Evidence-backed — every finding cites the exact config field it came from.
- Never executes the target — it reasons about configuration, not behavior.
What it checks
CallLint runs thirteen static detectors over each server entry:
| Detector | Risk symbol | What it flags |
|---|---|---|
| `secretEnvKeys` | 🔐 Secrets | Env keys whose names imply credentials (tokens, keys, passwords), incl. docker inline `-e KEY` |
| `broadFilesystemPath` | 📁 Files | Filesystem roots that grant broad read/write (`/`, `~`, home, drive roots), incl. docker bind-mount host paths |
| `unknownRemote` | 🌐 Network | Remote/HTTP transports to unrecognized or unpinned hosts |
| `promptPoisoning` | 🧠 Prompt | Model-directed instructions hidden in tool names, descriptions, or schemas |
| `hiddenInstructions` | 🧠 Prompt | Hidden/obfuscated content (zero-width, bidi, tag-char, HTML comments) in model-visible metadata |
| `dangerousCommand` | ⚙️ Exec | Shell-out / interpreter / package-runner commands (`bash -c`, `npx`, …) |
| `unverifiedLocalSource` | ⚙️ Exec | Local script/binary that is not a recognized package, pinned image, or remote |
| `externalMutation` | ✉️ Action | Tools that send or mutate external state (email, messages, posts) |
| `messagingSend` | ✉️ Action | Tools that send messages/email on your behalf (Slack, Twilio, SMTP, …) |
| `oauthScope` | ✉️ Action | OAuth scopes that are undeclared, broad, or expansive (`admin`, `*`, `repo`, …) |
| `gatewayRuntime` | ✉️ Action | Long-running gateway runtimes that proxy many downstream tools under one auth |
| `financialAction` | 💸 Money | Payment / transfer / irreversible financial actions |
| `unpinnedPackage` | 🧩 Supply | Unpinned package specs (`@latest`, no version) — rug-pull surface |
Findings roll up into a risk class (S0 metadata-only → S5
financial/irreversible) and an aggregate verdict per server and per config.
Drift detection (`baseline` / `verify`) records an approved risk surface and
flags rug-pulls (🔁) — a previously-approved server whose risk surface later
changed.
What it does not check
This list matters more than the feature list. CallLint is a *pre-flight check*,
not a proof of safety.
- It does not execute, install, or connect to servers — so it cannot observe
actual runtime behavior (what a server really reads, writes, or sends).
- It does not read or validate secret values — it inspects config *shape*
(key names), never the contents of your `.env` or credential stores.
- It does not analyze server source code — only the configuration and any
tool metadata you provide under `x-calllint.tools`.
- It does not fetch anything unless you pass `--online`, and online results
are advisory — they never upgrade a verdict toward SAFE.
- It does not certify third-party tools, replace human security review, or
guarantee an agent is safe.
- A clean run is necessary, not sufficient. Pair it with code review,
least-privilege tokens, and runtime controls.
`UNKNOWN` is a real verdict: when CallLint cannot verify what a server will do,
it says so and never silently upgrades `UNKNOWN` to `SAFE`.
What CallLint is — and is not
| CallLint is not | CallLint is |
|---|---|
| a runtime sandbox | a pre-run risk linter for agent-tool configs |
| a secret scanner (it never reads secret values) | a config-shape inspector that flags credential-shaped keys |
| `npm audit` (known package CVEs) | a blast-radius check on the authority you are granting |
| a server source-code analyzer | a static config + tool-metadata analyzer |
| a safety certificate | heuristic decision support, not a safety guarantee |
| a replacement for human review | the start of a review, with evidence attached |
Install
# run without installing (recommended):
npx calllint scan ./mcp.json
# or install globally:
npm install -g calllintRequires Node.js ≥ 20. The published package is a single self-contained bundle
with zero runtime dependencies. `calllint` on the `latest` tag is the current
stable CLI release; `@next` carries release candidates and `@preview`
older previews.
Quick start
Zero-config scanning — discover and scan all your agent configs:
# Auto-discover and scan all agents (Cursor, Claude Code, Claude Desktop, VS Code, Windsurf)
calllint scan --auto
# List all discovered agent configs
calllint inventory
# Scan a specific agent type
calllint scan --agent cursor
calllint scan --agent vscodeManual path scanning — scan a specific config file:
# scan a config file (auto-detects common locations if no path given)
calllint scan ./mcp.json
# scan from stdin, machine-readable JSON out
cat .cursor/mcp.json | calllint scan --stdin --json
# CI gate: non-zero exit per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
calllint scan ./mcp.json --ci --no-emoji
# synthesize a config for an npm package (offline) or a GitHub repo (--online)
calllint scan npm:mcp-weather@1.0.0
calllint scan github:owner/repo --online
# record an approved baseline, then detect drift / rug-pulls later
calllint baseline ./mcp.json
calllint verify ./mcp.json --ci
# explain one server's verdict from the last scan
calllint explain filesystem
# structured diagnostics for editor / agent-host integration
calllint diagnostics ./mcp.json --jsonOutput formats: default terminal, `--compact`, `--json` (stable schema),
`--sarif` (GitHub Code Scanning), `--markdown` (PR comments / GitHub Step
Summary), `--html` (self-contained report). The
`diagnostics` command emits a separate editor/agent-host JSON
(`calllint.diagnostics.v0`).
See CallLint running in CI on a deliberately risky config —
publishes one Code Scanning alert per finding on every push.
Beyond config scanning
The same engine and verdict semantics extend past MCP-config scanning to other
points where an agent grants authority:
# Preflight a planned external action before the agent runs it
calllint action inspect payment.json # calllint.action.v0 descriptor
calllint action inspect email-reply.json --json
# Preflight a normalized agent inbox event (delegates to the action analyzer)
calllint inbox inspect gmail-reply.normalized.json
# Record a scan as a local, verifiable receipt, then validate it later
calllint scan ./mcp.json --receipt # writes calllint-receipt.json
calllint receipt verify calllint-receipt.json
# Attach an external content-scanner report as evidence (joint Trust Packet)
calllint scan ./mcp.json --evidence skillspector-report.jsonReceipts (`calllint.receipt.v0`) are a reporting layer derived from a scan —
they prove which CallLint version produced which verdict over which input under
which policy. They are not a second scanner and never re-judge a verdict. A
receipt can carry an optional ed25519 signature; `receipt keygen` / `receipt
sign` generate and sign one locally for development, and `receipt verify`
checks the signature when present (offline, with `--public-key`). A signature
proves provenance and integrity — never safety.
Trust Gateway — prepare, approve, apply, verify
Scanning tells you the blast radius; the Trust Gateway acts on it, safely. It
resolves an agent-tool target to an immutable, digest-pinned identity, judges it
deterministically, and emits a reversible install plan. Applying that plan is
the only thing that ever writes live config: it re-validates every digest, writes
atomically, verifies the result, and rolls back on failure. The gateway never
executes, installs, or connects to the target it judges.
# read-only: resolve + judge a target and emit a reversible plan (touches no live config)
calllint trust prepare github:owner/repo --host claude-code --write-plan
calllint trust show .calllint/plans/.json
calllint trust explain .calllint/plans/.json
# the only writer of live config — applies an approved plan, atomically and reversibly
calllint trust apply --plan .calllint/plans/.json --approve --receipt
# validate a decision receipt later (read-only; never re-judges or executes)
calllint trust verify calllint-decision-receipt.json --public-key key.pubThe gateway is a deterministic, fail-closed pipeline over six sealed digests
(artifact → evidence → authority → decision/policy → install-plan → receipt). An
approval binds all six at once; if any digest changes between prepare and apply,
the approval is void and nothing is written. `UNKNOWN` never becomes `SAFE`, and
external evidence can tighten a verdict but never set it alone. Five Tier-A hosts
ship the audited apply surface — Claude Code, Cursor, Windsurf, Claude Desktop, and
VS Code. See the
CHANGELOG (Trust Gateway Core) and ADRs 0035–0039.
Continuous Guard — catch a rug-pull after approval
A tool you approved once can change later. `calllint guard` records the approved
authority surface and re-decides it — silent when nothing changed, and loud the
moment a previously-approved server's risk surface shifts (a rug-pull, 🔁). It adds
no new verdict engine: it reuses the same deterministic drift check as
`baseline` / `verify` and the same stable exit codes.
# re-assess the current authority surface vs the approved baseline (silent when unchanged)
calllint guard
# install a guard hook into a host — a declarative shim that only shells out to `calllint guard`
calllint guard install --host git # git pre-commit hook
calllint guard install --host github # GitHub Actions drift-gate workflow
calllint guard status # baseline / disable / installed-hook state
calllint guard disable # writes .calllint/guard.json { enabled: false }Guard installs on seven hosts: `git` (pre-commit), `git-pre-push`, `github`
(Actions), `claude-code`, `copilot`, `gemini`, and `vscode`. Every hook binds
only to a commit / push / CI / session-start event — never a per-call gate — so
a guard hook can never silently block a tool call (ADR 0045, ADR 0052). Hosts with a
dedicated file (git, GitHub, Copilot) are written whole; hosts whose hook lives
inside a shared user-owned config (Claude Code, Gemini, VS Code) get a fragment
printed for you to merge — `guard install` never clobbers a shared file.
Install the preflight into your agent — `integrate` and the Claude plugin
`calllint integrate` installs CallLint's own MCP server (`calllint-mcp`) into the
agent hosts you already use, so the agent can run the preflight itself before it
approves another server. It is plan-only by default: it detects installed hosts,
builds a reversible install plan, prints it with a digest, and writes nothing.
Applying is a separate, explicit, approved step that reuses the Trust Gateway's exact
audited writer (re-validate → atomic write → verify → roll back on failure).
calllint integrate # detect hosts + print an install plan (writes nothing)
calllint integrate --write-plan # persist each plan to .calllint/plans/.json
calllint integrate --apply --plan --approve # the only writerIt is idempotent (a host that already has the `calllint` server yields no change) and
project-scoped (it acts on configs under the repo you run it in, not your global
machine state).
For Claude Code, CallLint also ships as a plugin with a `PreToolUse` hook. When
Claude is about to write or edit an agent-tool config, the hook surfaces a one-line
recommendation to scan first. It is advisory and non-blocking: it always exits 0,
never vetoes a tool call, runs no scan itself, and neither the hook nor an LLM ever
enters the verdict path (ADR 0051). Installing it does not install a runtime blocker.
/plugin marketplace add calllint/calllint
/plugin install calllint@calllintRun CallLint as an MCP server (`calllint-mcp`)
CallLint also ships as its own MCP server, so an agent can run the preflight
check itself — *before* it installs or approves another MCP server. It is a thin
wrapper over the same engine: every tool delegates to `calllint`, it carries zero
runtime dependencies, and it never executes the server it judges.
{
"mcpServers": {
"calllint": {
"command": "npx",
"args": ["-y", "calllint-mcp"]
}
}
}Tools exposed: `scan_mcp_config_path`, `scan_mcp_config_json`, `verify_baseline`,
`explain_finding`, `generate_agent_rule`, `generate_ci_gate_snippet`. The server
speaks stdio JSON-RPC and returns the same evidence-backed
SAFE / REVIEW / BLOCK / UNKNOWN verdicts as the CLI. See
`packages/calllint-mcp` for details. Published on npm as
Example report
CallLint scan
config: ./mcp.json
result: BLOCK (BLOCK 1 · UNKNOWN 0 · REVIEW 0 · SAFE 0)
────────────────────────────────────────────────────────────
BLOCK helpful-notes PROMPT
S2 Sensitive read · reproducibility HIGH · confidence medium
"helpful-notes" is blocked. Risk: Prompt (S2 Sensitive read).
• [BLOCKER] Suspicious model-directed instruction in tool metadata
(prompt.poisoning, observed, confidence medium)
evidence: tools.save_note.description = do not tell the user
impact: Tool metadata reaches the model directly and can hijack
autonomous tool selection or coerce data disclosure.
fix: Remove model-directed instructions from tool names,
descriptions, schemas, and server instructions.
autonomous use: deny · manual approval: required · sandbox: recommendedCorpus and release gate
CallLint's verdicts are tested against a machine-checkable corpus. Each case
pins an expected verdict, required evidence, and a "dangerous input never
resolves to SAFE" policy. The corpus is enforced as a release gate:
`pnpm corpus:test`.
- 60 calibrated cases
- 38 real or redacted snapshots
- 0 dangerous false-SAFE
- UNKNOWN ratio 10.0% (target ≤ 15%)
The corpus is a regression and calibration gate, not a claim of full MCP
ecosystem coverage. See
`project-facts.json` (the single source of
truth for these numbers). Website and README copy is kept in sync by
`pnpm check:public-copy`.
Rule list
Each rule has a detector and a human-readable doc under
`packages/risk-engine/rules/`:
- `prompt.poisoning` — model-directed instructions in tool metadata (blocker)
- `prompt.hidden-instructions` — hidden/obfuscated content (zero-width, bidi,
tag-char, HTML comments) in model-visible metadata (R4 prompt surface, ADR 0014)
- `prompt.surface-instructions` — model-directed or hidden content in a project
document read via `--surface-dir` (README.md / SKILL.md / AGENTS.md /
`package.json` description); non-blocker, ADR 0015
- `exec.dangerous-command` — shell-out / interpreter / package-runner commands
- `exec.unverified-local-source` — runs a local script/binary that is not a
recognized package, pinned image, or remote (ADR 0011)
- `files.broad-path` — over-broad filesystem grants, incl. docker bind-mount host
paths (`--mount type=bind,src=…`, `-v host:container`; ADR 0012)
- `supply.unpinned-package` — unpinned package specs (rug-pull surface)
- plus `secretEnvKeys`, `unknownRemote`, `externalMutation`, `financialAction`
detectors (see What it checks)
Verdicts are governed by policy as code (`calllint.policy.json`); run
`calllint policy init` to write the defaults and `calllint policy explain` to see
the effective policy.
Badge
`calllint scan --badge` emits a [shields.io endpoint][endpoint] JSON
object so an MCP author can show a truthful CallLint verdict in a README. It is
built for transparency: the badge shows whatever the verdict is, and **only
`SAFE` is green** — `REVIEW`, `UNKNOWN`, and `BLOCK` each carry a distinct
non-green colour. It is a projection of the aggregate verdict (no schema change),
and `SAFE` means no blockers observed, not a proof of runtime safety. See
badge.md for the wiring and the verdict→colour map.
[endpoint]: https://shields.io/badges/endpoint-badge
Security model
CallLint is a security tool, so its own boundaries are explicit and auditable.
- No host execution. It parses and reasons about configuration only; it never
runs the server it judges. (See ADR 0003.)
- Treats all config as attacker-controlled. Tool names, descriptions, and
schemas are untrusted input; report rendering escapes them.
- Offline by default. `--online` adds advisory registry lookups only and can
never make a verdict *more* permissive.
- Deterministic and reproducible. No model, clock, or network in the decision
path; the JSON output schema is stable (`calllint.report.v0`).
Full statement: SECURITY.md ·
trust boundaries: LIMITATIONS.md. Report issues to
security@calllint.com.
Anonymous usage telemetry — opt-in, off by default
CallLint collects nothing unless you say yes. Scanning works fully offline with
telemetry off, and no verdict ever depends on it.
- Off by default. On the first run in an interactive terminal, CallLint asks
once. Only an explicit `y`/`yes` enables it; a bare Enter, a timeout, `n`, or
EOF all leave it off, and the answer is remembered so you are asked at most
once.
- Never prompts non-interactively. In CI, when piped, under `--json`/`--sarif`,
or with the kill-switch set, there is no prompt and no collection.
- Kill-switch. `CALLLINT_TELEMETRY=0` (also `false`/`off`) disables every
tier regardless of stored state.
- Check or change it any time: `calllint telemetry status` · `enable` ·
`disable` · `reset` (rotates the installation ID).
What an event may carry: event name, host family (e.g. `cursor`), result
category (e.g. `BLOCK`), duration, input kind, discovery surface, CallLint
version, and a random installation ID. The payload is built by an allowlist
— an unlisted field is dropped, not forwarded.
Never sent: config contents · file paths · commands · arguments · secret
values · prompts · finding evidence · server names · anything identifying you or
your machine.
Server side, the request IP and User-Agent are used only for rate limiting and
are never persisted; installation IDs are HMAC'd at ingestion and the raw value
is discarded. There is no raw event log.
Limitations
CallLint sees configuration, not behavior. It can miss risks a server only
reveals at runtime, and can flag surface that turns out benign. It depends on
the tool metadata you provide being accurate, and a server can change after you
approve it (use `baseline` / `verify` to catch that). It is heuristic: expect
both false positives and false negatives, and treat `REVIEW`/`BLOCK` as the
start of a review, not a complete threat assessment. See
LIMITATIONS.md for the full trust-boundary document.
Roadmap
- Broaden config-format coverage (more agent/host config dialects)
- Richer online supply-chain signals (still advisory, never auto-SAFE)
- More detectors and tunable policy packs
- Editor/CI integrations beyond SARIF
CallLint stays focused on pre-run risk linting for agent-tool configurations.
Hosted registries, gateways, and runtime enforcement are outside the current
release scope.
Project
CallLint is the official Apache-2.0 open-source project published at
github.com/calllint/calllint, and npm
packages `calllint` (CLI) and
`calllint-mcp` (MCP server). It is
maintainer-led — see GOVERNANCE.md and
License
Apache-2.0 — see LICENSE and NOTICE. The CallLint name and
logo are not licensed with the code; see TRADEMARKS.md.
Frequently asked questions
What is calllint?
calllint is Pre-flight risk linting for MCP and agent tools — check the blast radius before your agent runs them.
How do I install calllint?
Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.
Is calllint open source?
Yes — it is hosted on GitHub at https://github.com/calllint/calllint and has 2 stars.
Related MCP tools
Official Microsoft Learn MCP Server and CLI tool – powering LLMs and AI agents with real-time, trusted Microsoft docs & code samples.
Fast, local-first web content extraction for LLMs. Scrape, crawl, extract structured data — all from Rust. CLI, REST API, and MCP server.
AI-powered OSINT agent with interactive REPL, MCP server, and CLI. 19 tools. Works with Claude, GPT-4, or local models. For authorized security research only.
Open-source AI agent firewall for MCP security and agent egress. Scans mediated HTTP, MCP, A2A, and WebSocket traffic for exfiltration, SSRF, and prompt injection, and emits mediator-signed action receipts: verifiable audit evidence from outside the agent.
The go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.
superglue (YC W25) builds integrations and tools from natural language. Get production-grade tools for long tail and enterprise systems.
Run your own MCP server? See who uses it and what to fix.
Measure it with TrackMCP