Policy-as-code for AI agent permissions.
Define bounded permissions for AI agents in YAML. Enforce at runtime. Prevent scope creep.
AI agents need bounded permissions. Current solutions are either:
- Sandboxes (E2B, Daytona) — heavy, require infrastructure
- Custom code — rebuilt per project, error-prone
- Nothing — agents run unrestricted
agent-guard is a lightweight CLI + library that lets you declare exactly what an agent can do, then enforce it on every tool call.
pip install git+https://github.com/yunaremaia/agent-guard.gitNote on installation: this project is not published on PyPI, so it installs from the git repository rather than from the index.
Note: the
agent-guardname on PyPI is taken by an unrelated project by a different author (a monitoring library for Crew AI). This distribution isagentperm-py; theagent-guardcommand and theagent_guardimport are unchanged.
agent-guard init my-agent --output .agent-guard.yaml# .agent-guard.yaml - Minimal working example
name: minimal-agent
default_action: deny
rules:
- action: allow
tool: fs.read
resource: "./**/*"This policy:
- Denies everything by default
- Allows reading files in the current directory
- Blocks all other operations
# Test allowed operation
agent-guard check .agent-guard.yaml --tool fs.read --resource ./src/main.py
# ✅ True — allowed
# risk: low
# Test denied operation
agent-guard check .agent-guard.yaml --tool shell --resource "rm -rf /"
# ❌ False — denied by default_action
# risk: mediumfrom agent_guard import Policy, Guard, ToolCall
policy = Policy.from_yaml("""
name: my-agent
default_action: deny
max_tool_calls: 50
rules:
- action: allow
tool: fs.read
resource: "./**/*"
""")
guard = Guard(policy)
# Evaluate tool call
verdict = guard.check(ToolCall(tool="fs.read", resource="./src/main.py"))
print(verdict.allowed) # True
# Inspect execution metrics and policy limits
stats = guard.stats()
print(stats.tool_call_count) # 1
print(stats.max_tool_calls) # 50
# Reset counters between test cases or sessions (supports chaining)
guard.reset()
print(guard.stats().tool_call_count) # 0agent-guard provides first-class async support for use in async agent frameworks
(LangChain, AutoGen, CrewAI, or any asyncio-based loop).
import asyncio
from agent_guard import Policy, ToolCall, check_async
policy = Policy.from_file(".agent-guard.yaml")
async def main():
decision = await check_async(policy, ToolCall(tool="fs.read", resource="./src/main.py"))
print(decision.allowed) # True
print(decision.reason) # "allowed by rule: Read project files"
print(decision.risk) # RiskLevel.LOW
asyncio.run(main())from agent_guard import check_batch_async
async def main():
calls = [
ToolCall(tool="fs.read", resource="./src/main.py"),
ToolCall(tool="shell", resource="rm -rf /"),
]
decisions = await check_batch_async(policy, calls)
for call, decision in zip(calls, decisions):
print(f"{call.tool}: {'✅' if decision.allowed else '❌'} {decision.reason}")
asyncio.run(main())Pass raise_on_deny=True to raise a PolicyViolation exception instead of
returning a denied PolicyDecision:
from agent_guard import check_async, PolicyViolation
async def agent_step(policy, tool_call):
try:
decision = await check_async(policy, tool_call, raise_on_deny=True)
# Proceed — decision.allowed is guaranteed True here
execute_tool(tool_call)
except PolicyViolation as exc:
print(f"Blocked: {exc.decision.reason}")If you need stateful tracking (execution counts, tool-call limits), use the
Guard instance methods directly:
from agent_guard import Guard
guard = Guard(policy)
verdict = await guard.check_async(ToolCall(tool="fs.read", resource="./x.py"))
print(verdict.allowed) # True
print(guard.stats().tool_call_count) # 1
verdicts = await guard.check_batch_async([
ToolCall(tool="fs.read", resource="./a.py"),
ToolCall(tool="fs.read", resource="./b.py"),
])
print(guard.stats().tool_call_count) # 3import asyncio
from agent_guard import Policy, Guard, ToolCall
async def run_agent():
policy = Policy.from_file(".agent-guard.yaml")
guard = Guard(policy)
tool_calls = [
ToolCall(tool="fs.read", resource="./data.csv"),
ToolCall(tool="http", resource="https://api.github.com/repos"),
ToolCall(tool="shell", resource="rm -rf /"),
]
for call in tool_calls:
verdict = await guard.check_async(call)
if verdict.allowed:
print(f"✅ Executing {call.tool} on {call.resource}")
else:
print(f"❌ Blocked: {verdict.reason} (risk: {verdict.risk.value})")
asyncio.run(run_agent())Pre-made templates for common scenarios. Copy, customize, and use.
The same templates are checked in under examples/policies/.
Start with one and audit it before adapting it to your agent:
cp examples/policies/read-only.yaml .agent-guard.yaml
agent-guard audit .agent-guard.yamlPerfect for code review agents that should only read files and run read-only git commands.
# code-reviewer.yaml
name: code-reviewer
default_action: deny
max_tool_calls: 100
# Block all destructive operations
blocked_tools:
- destructive_*
- admin_*
- shell:rm*
- shell:del*
rules:
# Read project files
- action: allow
tool: fs.read
resource: "./**/*"
description: "Read any project file"
# Read-only git commands
- action: allow
tool: shell
resource: "git log *"
description: "View git history"
- action: allow
tool: shell
resource: "git show *"
description: "Show git commits"
- action: allow
tool: shell
resource: "git diff *"
description: "View git diffs"
- action: allow
tool: shell
resource: "git status"
description: "Check git status"
# Block write operations
- action: deny
tool: shell
resource: "git push *"
description: "Prevent pushing changes"
- action: deny
tool: fs.write
resource: "./**/*"
description: "Prevent file modifications"Use case: CI/CD code review agents that analyze but don't modify code.
Allows web browsing but restricts to specific domains.
# research-agent.yaml
name: research-agent
default_action: deny
# Domain allowlist
allowed_domains:
- github.com
- stackoverflow.com
- docs.python.org
- arxiv.org
# Block all other domains
blocked_domains:
- "*"
rules:
# Allow reading project files
- action: allow
tool: fs.read
resource: "./**/*"
# Allow web requests to approved domains
- action: allow
tool: http.get
resource: "https://github.com/**"
description: "Browse GitHub"
- action: allow
tool: http.get
resource: "https://stackoverflow.com/**"
description: "Search StackOverflow"
- action: allow
tool: http.get
resource: "https://docs.python.org/**"
description: "Read Python docs"
- action: allow
tool: http.get
resource: "https://arxiv.org/**"
description: "Browse research papers"
# Block all other web requests
- action: deny
tool: http.*
resource: "*"
description: "Block unapproved domains"
# Allow local file writes (for saving research)
- action: allow
tool: fs.write
resource: "./research/**"
description: "Save research notes"Use case: Research agents that browse the web but stay within approved domains.
Restricts agent to a specific directory tree.
# sandbox.yaml
name: filesystem-sandbox
default_action: deny
rules:
# Allow operations within sandbox directory only
- action: allow
tool: fs.read
resource: "./sandbox/**"
description: "Read within sandbox"
- action: allow
tool: fs.write
resource: "./sandbox/**"
description: "Write within sandbox"
- action: allow
tool: fs.delete
resource: "./sandbox/**"
description: "Delete within sandbox"
# Block all operations outside sandbox
- action: deny
tool: fs.*
resource: "../**"
description: "Prevent parent directory access"
- action: deny
tool: fs.*
resource: "/**"
description: "Prevent system directory access"
# Allow shell commands only within sandbox
- action: allow
tool: shell
resource: "cd ./sandbox *"
description: "Change to sandbox directory"
- action: deny
tool: shell
resource: "cd .. *"
description: "Prevent escaping sandbox"Use case: Untrusted agents that should only operate within a isolated directory.
For deployment agents with strict operational boundaries.
# deploy-agent.yaml
name: ci-deploy-agent
default_action: deny
max_tool_calls: 30
rules:
# Allow reading deployment configs
- action: allow
tool: fs.read
resource: "./deploy/**"
description: "Read deployment configs"
- action: allow
tool: fs.read
resource: "./.github/**"
description: "Read CI/CD configs"
# Allow specific deployment commands
- action: allow
tool: shell
resource: "kubectl apply *"
description: "Apply Kubernetes manifests"
- action: allow
tool: shell
resource: "docker build *"
description: "Build Docker images"
- action: allow
tool: shell
resource: "docker push *"
description: "Push Docker images"
# Block dangerous operations
- action: deny
tool: shell
resource: "kubectl delete *"
description: "Prevent resource deletion"
- action: deny
tool: shell
resource: "docker rm *"
description: "Prevent container removal"
- action: deny
tool: shell
resource: "rm *"
description: "Prevent file deletion"
# Block production access unless explicitly allowed
- action: deny
tool: shell
resource: "*--namespace=production*"
description: "Block production namespace"Use case: Automated deployment agents with strict operational boundaries.
Different policies for different agent roles.
# multi-agent.yaml
name: multi-agent-system
default_action: deny
# Agent-specific rules
agents:
# Research agent - can browse web
research:
rules:
- action: allow
tool: http.get
resource: "*"
- action: allow
tool: fs.read
resource: "./**/*"
- action: deny
tool: fs.write
resource: "./**/*"
# Coding agent - can write code but not browse web
coder:
rules:
- action: deny
tool: http.*
resource: "*"
- action: allow
tool: fs.read
resource: "./src/**"
- action: allow
tool: fs.write
resource: "./src/**"
- action: allow
tool: shell
resource: "pytest *"
# Reviewer agent - read-only
reviewer:
rules:
- action: allow
tool: fs.read
resource: "./**/*"
- action: allow
tool: shell
resource: "git log *"
- action: deny
tool: fs.write
resource: "./**/*"
- action: deny
tool: shell
resource: "git push *"Use case: Multi-agent systems where each agent has different permission levels.
┌─────────────────────────────────────────────────────────────┐
│ Tool Call Request │
│ (e.g., fs.read ./src/main.py) │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Load Policy (.agent-guard.yaml) │
│ │
│ name: my-agent │
│ default_action: deny │
│ rules: │
│ - action: allow │
│ tool: fs.read │
│ resource: "./**/*" │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Evaluate Rules (First Match Wins) │
│ │
│ Rule 1: fs.read ./**/* → MATCH ✅ │
│ Rule 2: shell rm * → no match │
│ Rule 3: default_action: deny → fallback │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Generate Verdict │
│ │
│ allowed: true │
│ reason: "Allowed by rule: Read project files" │
│ risk: low │
│ matched_rule: 1 │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Return Result │
│ │
│ Exit code: 0 (allowed) or 1 (denied) │
│ Output: JSON or human-readable table │
└─────────────────────────────────────────────────────────────┘
Flow:
- Agent makes a tool call request
agent-guardloads the policy file- Evaluates rules in order (first match wins)
- Returns verdict (allowed/denied) with reason
- Agent proceeds or blocks based on verdict
A: The default_action determines the outcome. If default_action: deny, unmatched operations are blocked. If default_action: allow, they're permitted. We recommend deny for security.
A: Yes! Use * to match multiple tools. For example:
fs.*matchesfs.read,fs.write,fs.deleteshell:git*matchesshell:git log,shell:git push, etc.http.*matcheshttp.get,http.post, etc.
A: Resource patterns support:
- Glob patterns:
./**/*matches all files recursively - Prefix matching:
git *matches any string starting with "git " - Exact matching:
./src/main.pymatches only that file - Regex: Use
regex:prefix for complex patterns
Examples:
resource: "./**/*" # All files in current directory
resource: "./src/*.py" # Python files in src/
resource: "git *" # Any git command
resource: "https://*.com" # Any .com domainA: Yes! Rules are evaluated in order, first match wins. Put specific rules first:
rules:
# Allow safe git commands
- action: allow
tool: shell
resource: "git log *"
- action: allow
tool: shell
resource: "git status"
# Block dangerous git commands
- action: deny
tool: shell
resource: "git push *"
- action: deny
tool: shell
resource: "git reset --hard *"A: Use the check command:
agent-guard check .agent-guard.yaml --tool fs.read --resource ./src/main.pyThis tells you if the operation would be allowed, which rule matched, and the risk level.
A: Yes! Use max_tool_calls:
name: limited-agent
max_tool_calls: 50 # Agent can make at most 50 tool calls
rules:
- action: allow
tool: fs.read
resource: "./**/*"After 50 calls, all subsequent calls are denied.
The limit is fail-closed, so 0 and "unset" are different things:
| Value | Meaning |
|---|---|
max_tool_calls: 50 |
At most 50 calls; call 51 and beyond are denied. |
max_tool_calls: 0 |
Zero calls permitted — every call is denied from the first one. |
max_tool_calls: null / omitted |
No limit configured. |
An explicit 0 is enforced as a limit of zero rather than being treated as
"unset", because agent-guard is a deny-oriented guard: silently widening a
policy because someone wrote 0 is the dangerous direction, so the ambiguous
case fails closed. The same applies to max_executions.
A: Each verdict includes a risk level:
- LOW: Read-only operations (fs.read, git log)
- MEDIUM: Write operations (fs.write, shell commands)
- HIGH: Destructive operations (fs.delete, rm)
- CRITICAL: System-level operations (sudo, system paths)
Risk is auto-calculated based on the tool and resource.
A: Absolutely! Exit codes are CI-friendly:
0= allowed1= denied
Example GitHub Actions step:
- name: Check agent permissions
run: |
agent-guard check .agent-guard.yaml \
--tool ${{ steps.agent.outputs.tool }} \
--resource ${{ steps.agent.outputs.resource }}A: Use the audit command:
agent-guard audit .agent-guard.yaml --format tableThis shows all effective rules, what they allow/deny, and any gaps or overlaps.
A: Yes! Create separate policy files:
# Research agent
agent-guard check research-agent.yaml --tool http.get --resource "https://example.com"
# Coding agent
agent-guard check coding-agent.yaml --tool fs.write --resource "./src/main.py"Or use the multi-agent template (see Policy Templates section).
- YAML policies — version-controlled, reviewable
- Rule ordering — first match wins (like firewall rules)
- Tool wildcards —
fs.*matchesfs.read,fs.write - Resource patterns — glob + regex support
- Domain controls — allowlist/blocklist for network tools
- Execution limits — max tool calls per session
- Risk scoring — LOW/MEDIUM/HIGH/CRITICAL per verdict
- Multiple outputs — JSON for automation, table for humans
- Exit codes — 0 = allowed, 1 = denied (CI-friendly)
| Command | Purpose |
|---|---|
check |
Test a tool call against policy |
explain |
Show which rule matched and why |
audit |
Review effective ruleset |
init |
Generate starter policy |
Import classes directly from the package:
from agent_guard import (
Guard, Policy, Rule, ToolCall, Action, RiskLevel,
PolicyDecision, PolicyViolation, check_async, check_batch_async,
)
# 1. Load or construct a Policy
policy = Policy.from_file(".agent-guard.yaml")
# Alternatively, from a YAML string:
# policy = Policy.from_yaml(yaml_string)
# Or construct programmatically:
# policy = Policy(
# name="custom-agent",
# description="Custom security policy",
# default_action=Action.DENY,
# rules=[Rule(action=Action.ALLOW, tool="fs.read", resource="./**/*")],
# max_tool_calls=50,
# )
# 2. Instantiate Guard
guard = Guard(policy)
# 3. Check / evaluate tool calls
call = ToolCall(tool="fs.read", resource="./src/main.py", arguments={"mode": "r"})
verdict = guard.check(call)
if verdict.allowed:
print(f"Allowed: {verdict.reason} (Risk: {verdict.risk.value})")
else:
print(f"Blocked: {verdict.reason} (Risk: {verdict.risk.value})")Policy:Policy.from_file(path: str | Path) -> Policy: Load policy from YAML file.Policy.from_yaml(text: str) -> Policy: Parse policy from YAML string.Policy.from_file_async(path: str | Path) -> Policy: Async version offrom_file.- Attributes:
name,description,default_action(Action),rules(list[Rule]),max_tool_calls(int | None),max_executions(int | None),blocked_tools(list[str]),allowed_domains(list[str]),blocked_domains(list[str]).
Guard:Guard(policy: Policy): Initializes runtime guard with execution and call counters.guard.check(call: ToolCall) -> Verdict: Evaluates tool call against limits, blocked tools, domain rules, and ordered policy rules.guard.check_async(call: ToolCall) -> Verdict: Async version ofcheck(). Runs the evaluation in a thread viaasyncio.to_thread.guard.check_batch_async(calls: list[ToolCall]) -> list[Verdict]: Evaluates multiple tool calls concurrently.guard.stats() -> GuardStats: Returns current execution metrics.guard.reset() -> Guard: Resets counters (supports chaining).
ToolCall:ToolCall(tool: str, resource: str = "", arguments: dict[str, Any] = ...): Represents an agent action.
Verdict:allowed: bool: Whether the action is permitted.rule: Rule | None: Matched rule if applicable, or None if fell through to default/limits.reason: str: Human-readable justification.risk: RiskLevel: Risk category (RiskLevel.LOW,RiskLevel.MEDIUM,RiskLevel.HIGH,RiskLevel.CRITICAL).
PolicyDecision:allowed: bool: Whether the action is permitted.denied: bool: Convenience property —Truewhen the call was denied.reason: str: Human-readable justification.risk: RiskLevel: Risk category.rule: Rule | None: Matched rule if applicable.PolicyDecision.from_verdict(verdict: Verdict) -> PolicyDecision: Create from aVerdict.
PolicyViolation(Exception):- Raised when a tool call is denied with
raise_on_deny=True. decision: PolicyDecision: The decision that triggered the violation.
- Raised when a tool call is denied with
Rule:Rule(action: Action, resource: str, tool: str, description: str = ""): Rule definition with glob/regex matching.
check_async(policy, tool_call, context=None, *, raise_on_deny=False) -> PolicyDecision: Evaluate a single tool call asynchronously.check_batch_async(policy, tool_calls, context=None, *, raise_on_deny=False) -> list[PolicyDecision]: Evaluate multiple tool calls concurrently.
Agent Guard policies are written in standard YAML. Below are all available fields:
| Field | Type | Default | Description |
|---|---|---|---|
name |
string |
(required) | Identifier for the policy. |
description |
string |
"" |
Description of the agent's role and boundaries. |
default_action |
string (allow | deny) |
"deny" |
Fallback action when no rules match. |
max_tool_calls |
integer | null |
null |
Maximum tool calls allowed per session before tripping a block. 0 means zero calls are permitted — the policy becomes a deny-all. Only null or an absent field means "no limit". |
max_executions |
integer | null |
null |
Maximum policy execution count. 0 means zero executions are permitted. Only null or an absent field means "no limit". |
blocked_tools |
list[string] |
[] |
Glob patterns for tools that are unconditionally blocked (e.g. destructive_*). |
allowed_domains |
list[string] |
[] |
Glob patterns for permitted domains on network tools (browser, http, fetch, request). If specified, any non-matching domain is blocked. Hosts are matched case-insensitively, and a resource that cannot be reduced to a host (metadata.internal.corp, //metadata.internal.corp, HTTPS://METADATA.INTERNAL.CORP, https://user@metadata.internal.corp/) is matched against these patterns too — a domain policy fails closed rather than skipping the check. |
blocked_domains |
list[string] |
[] |
Glob patterns for domains that are explicitly blocked. Matched case-insensitively against the host of every network-tool resource, however the URL is spelled. |
rules |
list[object] |
[] |
Ordered list of evaluation rules evaluated top-to-bottom. First match wins. |
| Field | Type | Default | Description |
|---|---|---|---|
action |
string (allow | deny) |
(required) | Verdict returned when this rule matches. |
tool |
string |
"*" |
Tool name pattern. Supports wildcards (e.g., fs.*, shell). |
resource |
string |
"*" |
Target resource path, glob (./**/*.py), command prefix (git *), or regex. |
description |
string |
"" |
Human-readable explanation shown in audit and check logs. |
Enables reading repository files and running read-only git queries, while preventing any file writes or remote repository pushes:
name: code-reviewer
description: "Restricted policy for autonomous code review"
default_action: deny
max_tool_calls: 50
blocked_tools:
- destructive_*
- admin_*
rules:
# Allow reading project code
- action: allow
tool: fs.read
resource: "./**/*"
description: "Read project files"
# Allow read-only git commands
- action: allow
tool: shell
resource: "git diff *"
description: "Inspect code diffs"
- action: allow
tool: shell
resource: "git log *"
description: "Inspect commit history"
- action: allow
tool: shell
resource: "git status"
description: "Check working tree status"
# Deny git mutations or pushes
- action: deny
tool: shell
resource: "git push *"
description: "Prevent pushing commits"A zero-mutation policy suitable for security scanning or static code analysis:
name: read-only-audit
description: "Strict read-only policy for static analysis and inspection"
default_action: deny
max_tool_calls: 100
blocked_tools:
- shell
- fs.write
- fs.delete
- exec_*
rules:
# Allow directory listing and inspection
- action: allow
tool: fs.read
resource: "./**/*"
description: "Read repository contents"
# Allow querying internal security docs or allowlisted APIs
- action: allow
tool: http
resource: "https://api.github.com/*"
description: "Query GitHub API"Allows executing deployment tools only against specific target environments with strict domain restrictions:
name: deploy-gate
description: "Production and staging deployment gate policy"
default_action: deny
max_tool_calls: 20
allowed_domains:
- "*.internal.company.com"
- "api.github.com"
blocked_domains:
- "*.public-cloud-sandbox.net"
rules:
# Allow running staging deployments
- action: allow
tool: deploy
resource: "staging-*"
description: "Allow automated staging deployments"
# Deny direct production deployment tool calls
- action: deny
tool: deploy
resource: "production-*"
description: "Block unapproved production deployment"
# Allow notifications/status pings
- action: allow
tool: http
resource: "https://*.internal.company.com/webhook/*"
description: "Send status notifications"Run agent-guard audit against your policy YAML file:
agent-guard audit .agent-guard.yaml --format tableYou can also specify --format json or --format yaml for automated validation and CI pipelines.
Use agent-guard check or agent-guard explain with test tool calls:
# Test whether a file write is prevented:
agent-guard check .agent-guard.yaml --tool fs.write --resource /etc/shadow
# Inspect rule resolution and rationale:
agent-guard explain .agent-guard.yaml --tool shell --resource "rm -rf /"- Tool names: Glob patterns matching tool names (e.g.
fs.*matchesfs.readandfs.write). - Resources: Supports path globs (
./**/*.py), prefixes (git *), and regex syntax for advanced resource constraints.
Rules are evaluated sequentially from top to bottom. The first matching rule determines the verdict (allow or deny). If no rule matches, the policy falls back to default_action.
- CI/CD pipelines — restrict agent to read-only operations
- Code review agents — allow
git logbut blockgit push - Browser agents — restrict to specific domains
- Multi-agent systems — different policies per agent
- Compliance — audit trail of all enforcement decisions
| Tool | Self-hosted | Declarative | Lightweight | Domain-aware |
|---|---|---|---|---|
| agent-guard | ✅ | ✅ | ✅ | ✅ |
| E2B | ❌ | ❌ | ❌ | ❌ |
| Daytona | ✅ | ❌ | ❌ | ❌ |
| OpenAI sandbox | ❌ | ❌ | ❌ | ❌ |
| Custom code | ✅ | ❌ | ✅ | ❌ |
If this tool is useful to you, a star helps other people find it.
agent-guard is MIT licensed and maintained in the open. Keeping policy-as-code for AI agent permissions free to install means keeping the rules engine, the CLI, and the runtime checks shipping on every release. If it saves you time, you can support continued development through GitHub Sponsors or the Solana treasury below.
Funding details are declared in .github/FUNDING.yml, which is
what GitHub reads to render the Sponsor button on this repository.
- GitHub Sponsors: @yunaremaia
- Solana:
Eeztv1nCYUt1fwGWpzKC948gaWfjejYCAuLtUMgzDWbW
Use the Solana address only for intended donations. Anyone can generate a similar
address, so verify the address against .github/FUNDING.yml before sending funds.
- driftcheck — detect version drift between docs and toolchain files
- agentcost — track and attribute LLM spend per agent
- agent-workspace — isolated workspaces per AI agent session
- tool-call-retry — retry failed tool calls with backoff
Part of a family of focused, single-purpose developer tools — each one does one thing and does it well.
MIT © Yunare Maia