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
Pin what you approved. Re-check it on every pull request.
- 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
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
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.
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.
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:
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
onToolAddedreviewa tool name present now but absent in the lockfile
onToolRemovedreviewa tool name in the lockfile that disappeared
onDescriptionChangedreviewany tool, prompt or resource description text differs
onSchemaExpandedblocka schema gained a property or required field (or went absent → present)
onNewOutboundHostblockan outbound host appears that was not in the baseline
onHighSeverityFindingblocka 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.
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.
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.
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.
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.