Least-privilege permissions for AI agents and MCP tools. Record what your agent does, auto-generate a safe policy, and block dangerous tool calls before they happen.
Project description
AgentPerms
Least-privilege permissions for AI agents and MCP tools. Record what your agent does, auto-generate a safe policy, and block dangerous tool calls before they happen.
Your AI agent has sudo. AgentPerms takes it away.
MCP is becoming the standard way AI apps connect to tools and data — which means agents are getting access to your filesystem, your repos, your email, and your database faster than anyone can govern them. Scanners tell you something looks risky. Firewalls make you hand-write YAML. AgentPerms does the missing thing:
record → infer → lock → replay → enforce
Run your agent in dev, record every tool call, infer the minimum permissions it actually needed, generate a policy, prove it blocks attacks, and enforce it in CI and at runtime.
Install
pip install agentperms
Requires Python 3.10+. This installs the agentperms CLI.
Quick start (30 seconds, no setup)
AgentPerms ships with a deliberately over-privileged demo MCP server, so you can see a real policy decision without wiring up a client or touching the network:
# 1. Scan the demo config — flags a ~/.ssh mount and an unpinned npx server
agentperms scan --path examples/vulnerable-mcp-demo
# 2. Replay a pack of canned attacks against the example policy
agentperms replay --policy examples/policies/example.mcp.policy.yaml
The replay prints a table and a verdict:
8/8 attacks blocked.
Every attack in the pack — SSH-key exfiltration, .env reads, rm -rf /, unapproved email, force-push, repo deletion, destructive SQL — is denied or routed to human approval before it would ever reach a server.
The full flow on your own agent
agentperms scan # find MCP configs, flag risky tools & exposures
agentperms lock # pin every tool's identity (detect tool poisoning)
agentperms record --client cursor # route the client through the recording proxy
# ... use your agent normally for a while ...
agentperms infer # traces -> mcp.policy.yaml (least privilege)
agentperms replay # prove the policy blocks a pack of attacks
agentperms enforce --client cursor # route the client through the blocking proxy
agentperms report # agentperms-report.html
agentperms init # scaffold .github/workflows/agentperms.yml
This produces mcp.policy.yaml, mcp.lock, agentperms-report.html, and a CI workflow.
When you're done recording or want to roll back enforcement, restore the original client config:
agentperms record --client cursor --stop
agentperms enforce --client cursor --stop
Command reference
| Command | What it does | Key options |
|---|---|---|
scan |
Discover MCP configs and flag risky tools, exposures, and unpinned servers | --client <name> (default all), --path <dir>, --tools (also launch servers to enumerate tool names) |
lock |
Pin every tool's name/description/schema; warn on silent changes (tool poisoning) | --check (fail on drift), --out mcp.lock, --path <dir> |
record |
Route a client's MCP servers through the recording proxy | --client cursor, --path <cfg>, --stop (restore) |
infer |
Infer a least-privilege policy from recorded traces | [traces...] (default traces/*.jsonl), --out mcp.policy.yaml |
replay |
Replay canned attacks against a policy and report blocks | --policy mcp.policy.yaml |
enforce |
Route a client's MCP servers through the blocking proxy | --client cursor, --policy mcp.policy.yaml, --path <cfg>, --stop |
report |
Render the HTML risk report from scan + traces + policy + replay | --policy, --out agentperms-report.html, --path |
init |
Scaffold the GitHub Actions workflow (scan + lock --check + replay) |
--out .github/workflows/agentperms.yml |
Supported clients (--client)
scan defaults to all and auto-discovers across every known client. record/enforce default to cursor.
| Name | Config discovered |
|---|---|
claude |
~/Library/Application Support/Claude/claude_desktop_config.json |
cursor |
~/.cursor/mcp.json, ./.cursor/mcp.json |
vscode |
~/.vscode/mcp.json, ./.vscode/mcp.json, ./.vscode/settings.json |
windsurf |
~/.codeium/windsurf/mcp_config.json |
gemini |
~/.gemini/settings.json, ./.gemini/settings.json |
Don't see your client? Point any command at a config explicitly with --path.
The killer command
agentperms infer
Your agent only used read-only GitHub calls and local
./srcaccess. I generated a least-privilege policy. The agent does not need shell, home directory, secrets, Gmail send, or database write access.
infer reads your recorded traces and emits the minimum policy that still lets the agent do what it actually did: used tools become allowed_tools, the directories it touched collapse into the smallest covering set of allowed_paths, and known-dangerous categories (shell, repo deletion, email send, DB writes) are seeded into denied_tools / human-approval.
How enforcement works
AgentPerms is a transparent stdio proxy. record/enforce rewrite your MCP client's config so each server launches through agentperms _proxy instead of directly:
Agent → AgentPerms proxy → MCP server
│
├─ record: log every tools/call, then forward
└─ enforce: allow / deny / require-approval before forwarding
A rewritten server entry looks like this (the original command is preserved after --, and a *.agentperms.bak backup is written so --stop can restore it):
// before
{ "command": "python3", "args": ["server.py"] }
// after `agentperms enforce`
{
"command": "/usr/bin/python3",
"args": ["-m", "agentperms", "_proxy",
"--mode", "enforce", "--server", "demo",
"--policy", "/abs/path/mcp.policy.yaml",
"--", "python3", "server.py"]
}
Denied calls never reach the server — the client gets a clean JSON-RPC error. Approval-gated calls prompt on your terminal. The exact same decision function powers both live enforcement and offline replay, so what you test is what you get.
The policy file
mcp.policy.yaml is a small, readable contract. This is the bundled example (examples/policies/example.mcp.policy.yaml):
version: 1
servers:
github:
allowed_tools: [list_repos, read_file, create_issue]
denied_tools: [delete_repo, write_secret, force_push]
filesystem:
allowed_paths: [./src, ./docs]
denied_paths: [~/.ssh, ~/.env, /etc]
denied_patterns: ["*.pem", "*.key"]
approvals:
require_human_approval: [gmail.send_email, github.merge_pr, postgres.execute_write, shell.exec]
redaction: { secrets: true, emails: true, api_keys: true }
Per-server fields: allowed_tools, denied_tools, allowed_paths, denied_paths, denied_patterns.
Top-level: approvals.require_human_approval (entries are server.tool or bare tool) and redaction (scrubs secrets/emails/API keys from traces and reports).
How a decision is made
For each tools/call, the engine returns allow / deny / require-approval using first-match-wins precedence:
- On the human-approval list → require approval
- In
denied_tools→ deny - A path argument hits
denied_paths/denied_patterns→ deny allowed_toolsis set and the tool isn't in it → deny (default-deny)allowed_pathsis set and a path argument falls outside it → deny- Otherwise → allow
An empty policy (no servers) allows everything. Once any server is constrained, unknown servers default-deny.
The attack pack
replay runs your policy against a built-in set of real-world attack shapes — entirely offline, no server required:
| Attack | Call | Why it's dangerous |
|---|---|---|
| SSH key exfil | filesystem.read ~/.ssh/id_rsa |
Steals a private key |
| Env secret read | filesystem.read ./.env |
Leaks credentials |
| Private cert read | filesystem.read ./certs/server.pem |
Reads a private key file |
| Shell exec | shell.exec rm -rf / |
Arbitrary command execution |
| Unapproved email | gmail.send_email … |
Sends mail with no human in the loop |
| Force push | github.force_push … |
Rewrites history |
| Repo deletion | github.delete_repo … |
Destroys a repository |
| Destructive SQL | postgres.execute_write DROP TABLE users |
Drops data |
A call counts as blocked if the policy denies it or routes it to human approval.
CI integration
agentperms init scaffolds a GitHub Actions workflow that fails the build on drift or weak policy:
agentperms init # writes .github/workflows/agentperms.yml
It runs, on every push/PR:
agentperms scan --path . # surface risky configs in the repo
agentperms lock --check # fail if any tool's identity changed (tool poisoning)
agentperms replay # fail if the committed policy stops blocking attacks
Commit mcp.policy.yaml and mcp.lock, and your agent's permissions become a reviewable, enforceable part of your codebase.
Status
v0.1 — supports Claude Desktop, Cursor, VS Code/Copilot, Windsurf, and Gemini CLI configs, plus local stdio MCP servers. Roadmap: HTTP/SSE transport, a Node wrapper, and a live dashboard.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentperms-0.1.1.tar.gz.
File metadata
- Download URL: agentperms-0.1.1.tar.gz
- Upload date:
- Size: 37.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46006006e5ccc616b11537a76943af929e60fcf3c82d8994b34170390fdbd418
|
|
| MD5 |
fce6a2ccabe1c55a8c5604ca87f1e5d1
|
|
| BLAKE2b-256 |
072583daecafb590e1e28d445a1b42537339bb55d7a566a12f49f8f51d4f24c7
|
Provenance
The following attestation bundles were made for agentperms-0.1.1.tar.gz:
Publisher:
release.yml on hasanmehmood/agentperms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agentperms-0.1.1.tar.gz -
Subject digest:
46006006e5ccc616b11537a76943af929e60fcf3c82d8994b34170390fdbd418 - Sigstore transparency entry: 1897597870
- Sigstore integration time:
-
Permalink:
hasanmehmood/agentperms@8e7cf2bb5f8122fa79de9cc38555337849492293 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hasanmehmood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8e7cf2bb5f8122fa79de9cc38555337849492293 -
Trigger Event:
push
-
Statement type:
File details
Details for the file agentperms-0.1.1-py3-none-any.whl.
File metadata
- Download URL: agentperms-0.1.1-py3-none-any.whl
- Upload date:
- Size: 36.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05a704a0874356a24e1a5021dff94c608b218acf637a44772cd55e04456a74ba
|
|
| MD5 |
ed01316f796a4997d7da480bacc6e158
|
|
| BLAKE2b-256 |
e1a909ceb33e7ca2085b3bcb23cacc843b47c861924ca3ef64ef67d93b52d7e5
|
Provenance
The following attestation bundles were made for agentperms-0.1.1-py3-none-any.whl:
Publisher:
release.yml on hasanmehmood/agentperms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agentperms-0.1.1-py3-none-any.whl -
Subject digest:
05a704a0874356a24e1a5021dff94c608b218acf637a44772cd55e04456a74ba - Sigstore transparency entry: 1897597980
- Sigstore integration time:
-
Permalink:
hasanmehmood/agentperms@8e7cf2bb5f8122fa79de9cc38555337849492293 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hasanmehmood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8e7cf2bb5f8122fa79de9cc38555337849492293 -
Trigger Event:
push
-
Statement type: