Skip to main content

tool-call-guard (Python)

CI PyPI license

Deny-by-default policy gate for AI agent tool calls. The Python half of tool-call-guard — same JSON policy model and audit schema as the npm package, so one security review covers both stacks.

from tool_call_guard import Guard, ToolCallDenied

guard = Guard(
    {
        "defaultAction": "deny",              # anything unlisted is blocked
        "tools": {
            "search_*": {},                   # allowlist a group
            "send_email": {
                "validate": lambda a: a["to"].endswith("@mycompany.com")
                or "external recipients need approval",
                "maxCallsPerMinute": 5,
            },
            "deploy": {"action": "approve"},  # human-in-the-loop
            "shell_exec": {"action": "deny"},
        },
    },
    approve=lambda req: ask_operator(req),    # sync here; async via acheck()
)

@guard.protect("send_email")
def send_email(args):
    ...

send_email({"to": "attacker@evil.com"})       # raises ToolCallDenied

Install

pip install tool-call-guard

The core has zero dependencies and supports Python 3.9+. Provider SDKs are optional extras and currently require Python 3.10+ through their upstream packages. Validators accept plain callables (return True/False/reason-string, or raise) or pydantic-style model classes (anything with model_validate).

Provider adapters

OpenAI Agents SDK

pip install "tool-call-guard[openai]"

Attach the policy adapter to an OpenAI function tool's input guardrails. A denied call never reaches the function; by default the adapter returns a safe rejection message to the model.

from agents import function_tool
from tool_call_guard import Guard
from tool_call_guard.integrations.openai_agents import create_tool_input_guardrail

guard = Guard({
    "tools": {
        "search": {},
        "shell": {"action": "deny"},
    }
})

@function_tool(tool_input_guardrails=[create_tool_input_guardrail(guard)])
def search(query: str) -> str:
    """Search internal documents."""
    return search_documents(query)

Set denied_behavior="raise_exception" to trip the run instead of returning model-visible rejection content. The default rejection text is generic; use the message option when the model should receive a curated reason. Invalid JSON arguments fail closed and are never copied into the adapter response.

Anthropic Claude Agent SDK

pip install "tool-call-guard[anthropic]"

Register the adapter as a PreToolUse hook:

from claude_agent_sdk import ClaudeAgentOptions
from tool_call_guard import Guard
from tool_call_guard.integrations.claude_agent_sdk import create_hook_matcher

guard = Guard({
    "tools": {
        "Read": {},
        "mcp__docs__*": {},
        "Bash": {"action": "deny"},
    }
})

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [create_hook_matcher(guard)]},
)

Allowed calls return no permission decision, so the SDK's native permission checks still run. Denied calls return a structured PreToolUse denial with generic text unless you set the message option. With mode="dry-run", the hook records would_allow without changing the SDK's permission flow.

What the policy gives you

  • Deny-by-default — unlisted tools are blocked; the allowlist is the policy.
  • Wildcard rules — "fs_*" budgets and gates a whole group; exact names beat patterns.
  • Argument validation — runs before quota, so malformed calls never consume budget.
  • Quotas — maxCalls per guard lifetime, maxCallsPerMinute sliding window (injectable clock).
  • Approval hooks — action: "approve" calls your approver; no approver configured means deny, not allow.
  • Dry-run mode — everything proceeds, but the audit trail records what enforcement would have done. Observe a policy in production before turning it on. Approvers are never invoked during a rehearsal.
  • Audit trail — in-memory ring buffer plus optional sinks; jsonl_audit(path) writes one JSON line per decision, same schema as the JS package.

API sketch

guard = Guard(policy, mode="enforce"|"dry-run", approve=..., on_audit=...,
              audit_args=True, max_audit_events=1000, now=time.time)

guard.check(tool, args)   -> Decision      # sync; sync approvers only
await guard.acheck(tool, args)             # async; sync or async approvers
guard.wrap(name, fn)                       # sync fn -> sync wrapper, async -> async
@guard.protect(name)                       # decorator form
guard.wrap_tools({name: fn, ...})
guard.audit_log                            # ring buffer, newest last
guard.reset()

Decision: allowed, action, reason, tool, rule, and in dry-run would_allow + dry_run. Denied wrapped calls raise ToolCallDenied (with .decision).

Policy keys are camelCase (portable JSON, shared with the JS package); snake_case aliases (max_calls, …) are accepted in Python.

See the repository root for the full policy reference and the threat model this addresses.

License

MIT © Binaya Dhakal

Metadata

Release files for tool-call-guard 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tool-call-guard 0.2.0
File Size Uploaded
tool_call_guard-0.2.0.tar.gz 14.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tool-call-guard 0.2.0
File Interpreter ABI Platform
tool_call_guard-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.6 kB

Release files / tool_call_guard-0.2.0.tar.gz

Download URL tool_call_guard-0.2.0.tar.gz
Size 14.7 kB
Tags Source
SHA-256 checksum
How to use checksums
0733a0d6f66d185e17b9d81b6e410589aea7b588e226312243c44bb9b4040c79
BLAKE2b-256 checksum
How to use checksums
443681cab2e607c3253e1697b98ed25b62d3104fcac38866f08ce2eb0081ab6c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release files / tool_call_guard-0.2.0-py3-none-any.whl

Download URL tool_call_guard-0.2.0-py3-none-any.whl
Size 11.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d98fd8575757809b9c6b83312b9ddc5ea4ec1b5d0deb6874fc25bed165ab08eb
BLAKE2b-256 checksum
How to use checksums
61e34ef3583cf7aa40611914e28bd6a7a12882acd0abdfbf60e7176e1669e4c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.7

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page