mcp-runtime-authz
Provides a starter policy for wrapping the official Git MCP server, preventing credential files from being staged or read from history, refusing glob/pathspec tricks, and blocking secrets in commit messages.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Highlights
Stops secrets leaving through tools, even base64- or percent-encoded, even to a destination that looks legitimate.
Keeps private files private:
.env, keys,.sshand credential files, including when the path is tucked inside a list of other files.Remembers what your agent read and stops that data leaving for anywhere you didn't allow, in a URL, an email body or a subdomain (taint tracking).
Blocks code planted to run later: writes to git hooks, CI workflows, editor tasks and shell rc files.
Notices when a server changes its tools after you've trusted them. A changed tool is hidden from the agent and refused until you accept it (tool pinning).
Logs every decision before it runs. The agent only ever sees
BLOCKED, never which rule fired, so it can't probe its way around the policy.Nothing can skip the check. The Rust compiler enforces it, and tests pin it.
Drafts a policy for any server with
authz init, and works with any stdio MCP server, with prebuilt binaries for Linux, macOS and Windows.
Related MCP server: mcp-airlock
Quick start
1. Install. Download the archive for your platform from
Releases (each includes the starter
policies, a .sha256 checksum and signed build provenance), or install with
Cargo:
cargo install --git https://github.com/Wazza0/mcp-runtime-authz --locked authz-cliTo remove it, run cargo uninstall authz-cli, or delete the authz binary if you used an archive.
2. Get a policy. Use a starter policy, or draft one from the server's own tool list and review it:
authz init --out my-policy.yaml -- <your mcp server command>3. Wrap the server.
authz proxy --policy policies/filesystem.yaml --audit audit.jsonl --pin tools.lock \
-- npx -y @modelcontextprotocol/server-filesystem /path/to/projectPoint your client at the proxy instead of the server. In .mcp.json (Claude Code), or Claude
Desktop's or Cursor's config (on Windows use npx.cmd):
{
"mcpServers": {
"files": {
"command": "/path/to/authz",
"args": ["proxy", "--policy", "/path/to/policies/filesystem.yaml", "--audit", "/path/to/audit.jsonl",
"--pin", "/path/to/tools.lock",
"--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
}
}
}See what happened at any time with authz audit audit.jsonl.
Starter policies
starter policy | wraps | what it adds |
the official filesystem server | credential files stay unreadable; secrets can't be written into files; no writes that run code later | |
the official fetch server | only hosts you list; refuses localhost, private and cloud-metadata addresses even if listed; no credentials or keys in URLs | |
the official git server | credential files can't be staged or read back from history; | |
a template for any server | an |
Each starter policy is verified end to end against the real server it wraps (see Tests and evaluation).
How it works
Every call goes through one decision path, whether it comes from the proxy or the bundled test agent. Four guarantees hold by construction:
Nothing skips the check. Running a tool needs a single-use token bound to that exact call, and only a policy allow (or a human approving an escalation) can mint one. Forging a token or calling the tool client directly is a compile error, pinned by tests.
Rules can only restrict. A rule can deny or escalate, never allow, and the most severe match wins, so no rule can loosen another.
A human can approve a borderline call but never override a deny. That's in the types, not a convention.
The log is written first. If the audit write fails, the call doesn't run.
Does it work?
Measured on 7 attacks and 6 benign tasks, unguarded vs guarded. The block rate counts only attacks that succeed without the guard, so a model resisting by itself earns the guard no credit.
scripted agent (obeys every injection) | qwen3:8b (real local model) | |
attacks that succeed with no guard | 7/7 | 3/7 |
attack-block rate | 100% (7/7) | 100% (3/3) |
false-positive rate (benign tasks broken) | 0% (0/6) | 0% (0/6) |
block rate if a human approves every prompt | 86% (6/7) | 67% (2/3) |
The same author wrote the attacks and the rules, which flatters the result. The full write-up, limitations included, is in report/findings.md, and CI reproduces the scripted numbers on every push.
Found and fixed
Six real bypasses in this project's own code, found by testing and review, each fixed and pinned by a regression test:
A whole private file pasted into an email body counted as a harmless reference. Found by removing rules one at a time.
Text that mentioned a link had everything before the link ignored, so data placed in front of a URL went unchecked.
Refusals named the rule that fired, giving injected instructions a way to probe the policy and retry. Raised in review of related work. The agent now learns nothing.
A denied file hidden in a list (
read_multiple_files) was returned while a direct read was refused. Found only by testing against the real filesystem server; our own test server never takes lists.Windows paths walked past the filesystem policy. It was written through a shell that silently turned
[\\/](either separator) into[\/](forward slash only), soC:\p\.envwas allowed. Found while writing the git policy; the tests had only used forward slashes.A URL's login part posed as its host.
http://allowed.test:x@evil.test/reaches evil.test, but the host check stopped at the first:and saw allowed.test. Hosts are now parsed properly, and where URL parsers disagree (a\in the authority) every possible host must be allowed.
The story behind the first four is in the write-up.
Writing a policy
Policies are YAML data; the engine is generic. Start from authz init, a
starter policy, or example-proxy.yaml:
authz initdrafts, you decide. It classifies each tool from its name, arguments and the server's hints, and adds the rules that fit. When unsure it fails safe: tools it can't classify, and tools the server calls destructive, start refused, with a comment saying so.The
allowlist is your trust anchor. The proxy never sees what you typed, so hosts, email recipients and shell commands are permitted only if the policy lists them.List each tool the server exposes under
tools:with a risk class (read,write,egress,exec). Anything unlisted falls todefault, which should bedeny.Escalations fail closed in the proxy. stdin carries the MCP protocol, so the proxy can't prompt you; an escalated call is refused and logged.
--approve allowturns them into allows.Malformed policies are rejected: duplicate keys, unknown fields, bad patterns, and rules that could never fire all fail at load.
The full policy format, every command and the audit log fields are in docs/reference.md.
Tool pinning
A tool's description is text your agent reads as instructions, so a server that edits it after you
start trusting it (or quietly adds a tool) can steer the agent. With --pin tools.lock, the first
run records a fingerprint of every tool: its name, description, argument schema and annotations.
After that, a tool that changed or appeared is left out of the tool list, so its new
description never reaches the agent, and is refused if called anyway. The change is reported
on stderr and in the audit log. Review it, then run once with --repin to accept it.
Verify a download
From v0.2.0, release archives carry signed build provenance, so you can check that a download was built by this repository's release workflow from its source:
gh attestation verify authz-v0.2.1-x86_64-unknown-linux-gnu.tar.gz --repo Wazza0/mcp-runtime-authzHow this project secures itself
A security tool should hold itself to the standard it enforces. On every push, CI:
checks every dependency against the RustSec advisory database, and limits licenses and sources (
deny.toml);fuzzes the argument decoder, the policy loader and the full decision path (
fuzz/), and runs property tests, e.g. that a secret in any supported encoding never leaves;scans the code and the workflows with CodeQL;
lints with warnings as errors and runs the full test suite, including the compile-time bypass checks.
All CI actions are pinned to exact commits, Dependabot keeps them and the dependencies patched,
master is protected against force-pushes, and the OpenSSF Scorecard
grades the setup publicly. What the guard does and does not guarantee, the threat model, the trust
boundaries and the weaknesses it counters are in docs/security.md. Report a
vulnerability privately via SECURITY.md.
Limitations
Small, self-written corpus; no attacker that adapts to the guard.
Taint tracking catches data reused verbatim or encoded, not paraphrased.
Pattern-based secret detection misses formats and encodings it doesn't know.
In the proxy, the
allowlist must be written by hand, and escalations can't prompt.
More in report/findings.md. How this compares to published work (Progent, CaMeL, FIDES and others) is in report/related-work.md.
cargo test --workspace # milestone gates, published results, proxy, pinning, init, properties, compile-time guarantees
cargo llvm-cov --workspace --summary-only # line coverage; CI fails below 80%
cargo test -p authz-cli --test proxy_filesystem -- --ignored # against the real servers: filesystem (needs Node),
cargo test -p authz-cli --test proxy_fetch -- --ignored # fetch and git (need uv)
cargo test -p authz-cli --test proxy_git -- --ignored
cargo run --release -p authz-cli --bin authz -- eval --ablation # scripted agent + per-rule ablation
cargo run --release -p authz-cli --bin authz -- eval --approve allow # human approves everything
cargo run --release -p authz-cli --bin authz -- eval --model ollama # real local model (needs Ollama)tests/m5_eval.rs runs the whole evaluation and fails if the headline numbers or the ablation
table in report/findings.md stop holding. tests/proxy.rs drives the proxy with the official MCP SDK client and checks that allowed calls
pass through intact and refused ones never reach the server. Attack cases carry references to the
OWASP Top 10 for Agentic Applications 2026, the OWASP LLM Top 10 2025 and MITRE ATLAS v2026.10,
pinned from official sources in eval/frameworks/ and checked by tests/refs.rs.
Layout.
crate | role |
| shared types: |
| the |
| a simulated MCP tool server for the evaluation: nothing is sent, fetched or executed |
| agent loop and model backends; no dependency on the tool client, so it can only act through what it's handed |
| the |
The boundary, structurally. authz-agent cannot name the tool client (tests/boundary.rs).
Execution is crate-private (tests/ui/bypass_exec.rs, E0624) and consumes an Authorization that
only token.rs can mint (tests/ui/forge_token.rs, E0451). authorize returns an unforgeable
Escalation for human review (tests/ui/forge_escalation.rs, E0451); a deny is a different type
with no path to execution. The unguarded baseline is the same path with an allow-everything policy.
Taint tracking. Output of tools the policy labels (e.g. taint: private) is recorded by the
guard, never the model. A later argument that shares a run of at least min characters with it,
after decoding, carries that data. A URL's host counts too, unless the source itself names that
host; a URL copied whole from a source is a link being followed, not a leak.
Escalation in the bundled agent. authz run --approve ask prompts on the terminal, deny
fails closed, allow approves everything. The audit log records escalate, then approved or
denied, before anything runs.
Models for the evaluation. scripted replays each case's plan, with injected steps firing only
if the injected text reached it. ollama runs a real local model (default qwen3:8b, temperature
0, fixed seed, reasoning off, neutral system prompt); set AUTHZ_MODEL and OLLAMA_HOST.
The full design is in docs/architecture.md.
History. Built in milestones M0 to M5 (bare agent, interceptor, context-aware policy, escalation, taint tracking, evaluation), then refocused into a proxy for real MCP servers.
License
Dual-licensed under MIT or Apache-2.0, at your option. Contributions are welcome: see CONTRIBUTING.md, the code of conduct, governance and the roadmap. Found a way past the guard? Please report it privately: see SECURITY.md.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Security & DLP proxy for MCP: tool-poisoning scans, PII redaction on tool args/results. Beta.
Guard wraps stdio MCP servers: risky calls wait for a person; no answer, no run. Remote: cooperative
Security firewall for AI agents — scans MCP calls for injection, secrets, and risks.
Runtime behavioral trust scoring for MCP servers. Check reliability before calling unknown tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA least-privilege enforcement proxy for MCP servers. It sits between MCP clients and upstream servers, enforcing tool policies, hiding denied tools, requiring human approval for risky actions, and providing a structured audit trail.MIT
- AlicenseNot gradedqualityDmaintenanceA zero-infrastructure, local proxy that wraps any stdio MCP server to add audit logging, policy enforcement with regex guards, and per-session/per-day budgets.MIT
- AlicenseNot gradedqualityCmaintenanceEnforces deterministic security policies as an inline firewall for MCP server tool calls, with AST-based validation, cryptographic audit logging, and CLI-based evaluation and verification.MIT
- AlicenseNot gradedqualityCmaintenanceA transparent MCP stdio proxy that sits between an agent client and a real MCP server, intercepting tools/call requests to enforce runtime guardrails derived from the same YAML rules used for CI regression testing. It blocks forbidden, out-of-whitelist, looping, or non-idempotent tool invocations with JSON-RPC errors and writes every decision to an audit log.1MIT