open · free · MIT

Toolproof Lock.

A lockfile for the capabilities your agent can reach. toolproof lock records the exact capability surface you approved into a signed toolproof.lock; toolproof check fails CI when that surface changes past the policy you wrote.

An MCP server can add a tool, widen an input schema, rewrite the text your model reads, or start calling a new host — with no change in your repository. The lock converts that invisible vendor-side mutation into a reviewable PR event.

npx toolproof-lock lock https://mcp.example.com/mcp   # write the baseline, commit it
npx toolproof-lock check --policy=toolproof.policy.yml # in CI: exit 1 review, 2 blocked
developer workflow

Pin what you approved. Re-check it on every pull request.

  1. 1

    Write the baseline

    Run toolproof lock <target>. Toolproof fetches the public capability surface and writes a signed toolproof.lock: tool names, descriptions, input/output schemas, prompts, resources, instructions, outbound hosts, OpenAPI paths and a canonical fingerprint.

  2. 2

    Commit the lockfile

    toolproof.lock lives in your repo next to your agent configuration and is reviewed in a pull request like any other file. It is plain JSON — your baseline is yours, and it stays valid whether or not you run an MCP gateway.

  3. 3

    Check it in CI

    toolproof check re-fetches the surface, computes a semantic diff against the baseline, applies your policy, and exits non-zero when the drift needs a human. Wire it into the workflow you already run on pull requests.

what fails in CI

A diff that names the consequence.

The check is not a hash comparison. It classifies each change — a new tool, an expanded schema, a changed description, a new outbound host, a new high-severity finding — under your policy, and prints the reason next to the tag. Exit 0 in sync, 1 review required, 2 blocked, 3 usage or network error.

$ npx -y --package toolproof-lock toolproof check --policy=toolproof.policy.yml
TOOLPROOF LOCK · mcp.example.com

baseline  sha256:08b39299…  grade A+
observed  sha256:4b81de0c…  grade A
state     verified · kind mcp · policy toolproof.policy.yml

  [BLOCKED] description of tool "query-docs" changed — the text the model reads differs from the baseline (tool:query-docs)
  [BLOCKED] inputSchema of tool "query-docs" expanded: gained property "attachments" — the target now accepts input that was not described in the baseline (tool:query-docs.inputSchema)
  [REVIEW] tool "send_sms" appears in the observed manifest — the target now exposes a capability that was not in the baseline (tool:send_sms)

BLOCKED · blocked - 2 blocking changes: description-changed (tool:query-docs), schema-expanded (tool:query-docs.inputSchema) · exit 2
Error: Process completed with exit code 2.

The tags, the reason text and the trailing (where) are verbatim; only the fingerprint is shortened for display. Tags are [BLOCKED], [REVIEW] and [INFO], and (where) names the tool, prompt, resource, host or rule the change belongs to.

what a meaningful diff looks like

Consequences, not hashes.

Tool order and JSON re-serialization do not trip the check. A capability that actually widened does. Examples in the shape the report uses:

search_email gained access to attachment contentschema-expanded · block by default
a new export_customers tool was addedtool-added · review by default
create_invoice changed from a read-only claim to an external write actiondescription-changed · review by default
a tool description now contains model-directed instructionsdescription-changed / high-severity finding
an endpoint now advertises an additional external hostoutbound-host-added · block by default
policy model

Policy is versioned, reviewable code.

A policy is a small file you keep in the repo — pure JSON, or the flat key: value YAML subset shown here. Each change category maps to one policy key, and every key accepts exactly three actions: informational, review, or block. Omit the file and the built-in default applies.

# toolproof.policy.yml — the built-in default
minimumGrade: B
requireVerified: true
onToolAdded: review
onToolRemoved: review
onDescriptionChanged: review
onSchemaExpanded: block
onNewOutboundHost: block
onHighSeverityFinding: block
onToolAddedreview

a tool name present now but absent in the lockfile

onToolRemovedreview

a tool name in the lockfile that disappeared

onDescriptionChangedreview

any tool, prompt or resource description text differs

onSchemaExpandedblock

a schema gained a property or required field (or went absent → present)

onNewOutboundHostblock

an outbound host appears that was not in the baseline

onHighSeverityFindingblock

a high-severity rule id is new since the baseline

minimumGrade and requireVerified always block when unmet. For v1, a newly added rule id beginning TP-1 is treated as high severity — a documented simplification, not a classification of severity on its own.

evidence

The audit trail writes itself.

Every check appends a signed receipt to .toolproof/evidence.jsonl: the fingerprints, the decision, the policy, who decided, and when. Receipts are hash-chained to the one before, so history cannot be rewritten without breaking the chain.

$ toolproof evidence verify
evidence OK — 3 receipts, chain intact, signatures verified

$ toolproof evidence export --from 0 --out audit.json
evidence exported 3 receipts (0..2 of 3) to audit.json

An export is a self-contained bundle an auditor verifies in the browser — no key, no network, nothing uploaded. It carries its own issuer key and a signature over the range, so its contents cannot be swapped after the fact.

Receipts carry fingerprints and decisions only — never prompt content, tool arguments, tool results or credentials. The store is safe to keep, export and hand over by construction.

scope and non-claims

What Lock is not.

Toolproof Lock covers the connection-time capability surface: server identity and endpoint; tool names, descriptions, schemas, prompts, resources and annotations presented to the model; discoverable outbound hosts; static findings and the rule-set that produced them; and a cryptographic fingerprint of the canonical surface.

It does not claim to prove that a remote service behaves safely, does not inspect private credentials, and does not replace sandboxing, network egress controls, or authorization at the service being called.

Concretely: a signature attests what Toolproof observed and when — not that a service is safe. Lock detects a change and requires review; it does not stop a tool from running. Runtime enforcement is the gateway's job, and a gateway is not required to use the lock.

where this sits in the market

Pinning is not new. Portability and consequence are the wedge.

Tool-definition pinning already exists: MCP-Scan (April 2025) tracks tool-description changes by hashing tool definitions, and diffing against a “golden baseline” is already a named product concept. Snyk publishes a free agent/MCP scanner. Gateways from Zuplo, Cloudflare, Kong, AWS Bedrock AgentCore, Docker, IBM, MintMCP, Lasso and others own the enforcement point — they block at call time, and some keep an approved baseline inside the proxy.

So we are not claiming a first. What is different here is the shape of the artifact: a baseline that is committed to your repository, gateway-independent (it stays valid if you switch gateways or run without one), reviewed in a PR, and paired with consequence-level diffs and a signed, exportable evidence receipt you keep when the vendor is absent.

Stated plainly: scanning tools tell you what is wrong right now; the lock tells you what changed since you approved it. Gateways enforce at call time; the lock governs at review time. It complements a gateway rather than replacing one, and MCP is the first adapter — the manifest is protocol-independent by design.

install & quickstart

Locally, with no account.

Zero runtime dependencies, Node 18+, published as toolproof-lock with the toolproof binary.

# write a baseline and commit toolproof.lock
npx toolproof-lock lock https://mcp.example.com/mcp

# in CI (or before you connect): diff against the baseline, apply policy
npx toolproof-lock check --policy=toolproof.policy.yml

# machine-readable, for your own tooling
npx toolproof-lock check --json   # { decision, exitCode, changes, manifest }

Add the ready-to-run GitHub Actions workflow as .github/workflows/toolproof-lock.yml to run the check on every pull request.