trackmcp
Back to directory

Pre-flight risk linting for MCP and agent tools — check the blast radius before your agent runs them.

2 stars TypeScriptOthers Updated Sep 1, 2026
ai-agentsai-securityclillmmcpmcp-serverprompt-injectionsecuritystatic-analysissupply-chain-security

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.

bash
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.

text
$ 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: required

What 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:

DetectorRisk symbolWhat it flags
`secretEnvKeys`🔐 SecretsEnv keys whose names imply credentials (tokens, keys, passwords), incl. docker inline `-e KEY`
`broadFilesystemPath`📁 FilesFilesystem roots that grant broad read/write (`/`, `~`, home, drive roots), incl. docker bind-mount host paths
`unknownRemote`🌐 NetworkRemote/HTTP transports to unrecognized or unpinned hosts
`promptPoisoning`🧠 PromptModel-directed instructions hidden in tool names, descriptions, or schemas
`hiddenInstructions`🧠 PromptHidden/obfuscated content (zero-width, bidi, tag-char, HTML comments) in model-visible metadata
`dangerousCommand`⚙️ ExecShell-out / interpreter / package-runner commands (`bash -c`, `npx`, …)
`unverifiedLocalSource`⚙️ ExecLocal script/binary that is not a recognized package, pinned image, or remote
`externalMutation`✉️ ActionTools that send or mutate external state (email, messages, posts)
`messagingSend`✉️ ActionTools that send messages/email on your behalf (Slack, Twilio, SMTP, …)
`oauthScope`✉️ ActionOAuth scopes that are undeclared, broad, or expansive (`admin`, `*`, `repo`, …)
`gatewayRuntime`✉️ ActionLong-running gateway runtimes that proxy many downstream tools under one auth
`financialAction`💸 MoneyPayment / transfer / irreversible financial actions
`unpinnedPackage`🧩 SupplyUnpinned 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 notCallLint is
a runtime sandboxa 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 analyzera static config + tool-metadata analyzer
a safety certificateheuristic decision support, not a safety guarantee
a replacement for human reviewthe start of a review, with evidence attached

Install

bash
# run without installing (recommended):
npx calllint scan ./mcp.json

# or install globally:
npm install -g calllint

Requires 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:

bash
# 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 vscode

Manual path scanning — scan a specific config file:

bash
# 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 --json

Output 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 —

`calllint-demo-risky-mcp`

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:

bash
# 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.json

Receipts (`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.

bash
# 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.pub

The 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.

bash
# 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).

bash
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 writer

It 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.

code
/plugin marketplace add calllint/calllint
/plugin install calllint@calllint

Run 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.

jsonc
{
  "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

`calllint-mcp`.

Example report

code
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: recommended

Corpus 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

calllint.com,

github.com/calllint/calllint, and npm

packages `calllint` (CLI) and

`calllint-mcp` (MCP server). It is

maintainer-led — see GOVERNANCE.md and

CONTRIBUTING.md.

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

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP