HOL Guard Tool Policy

Gate command-bearing tools through HOL Guard before they execute

HOL Guard checks every shell command your agent proposes and only lets safe ones run.

Diagram

allow

block

🤖 Agent

🔧 Tool Call

🛡️ HOL Guard

✅ Tool Runs

🚫 Blocked Response

HOL Guard is an external, optional security engine. Install it separately, then wrap your command-bearing tools with one @wrap_tool_call policy — every unsafe command is blocked before it runs.

Quick Start

  1. Install HOL Guard

    HOL Guard is a separate CLI. Install it once:

    bash
    pip install hol-guard
  2. Add the guard policy to your agent
    python
    import json
    import shutil
    import subprocess
    
    from praisonaiagents import Agent, tool
    from praisonaiagents.hooks import wrap_tool_call, ToolRequest, ToolResponse
    
    COMMAND_BEARING_TOOLS = {"run_shell"}
    GUARD_TIMEOUT_SECONDS = 10
    
    
    @tool
    def run_shell(command: str) -> str:
        """Run a shell command and return its output."""
        result = subprocess.run(command, shell=True, capture_output=True, text=True)
        return result.stdout or result.stderr
    
    
    def _inspect_with_hol_guard(command: str) -> bool:
        if not shutil.which("hol-guard"):
            return False
        try:
            completed = subprocess.run(
                ["hol-guard", "command", "test", command, "--json"],
                capture_output=True, text=True, timeout=GUARD_TIMEOUT_SECONDS,
            )
        except (subprocess.TimeoutExpired, OSError):
            return False
        if completed.returncode != 0:
            return False
        try:
            verdict = json.loads(completed.stdout)
        except (json.JSONDecodeError, ValueError):
            return False
        if not isinstance(verdict, dict):
            return False
        decision = str(verdict.get("decision", "")).lower()
        return decision in ("allow", "benign")
    
    
    @wrap_tool_call
    def hol_guard_tool_policy(request: ToolRequest, call_next):
        if request.tool_name not in COMMAND_BEARING_TOOLS:
            return call_next(request)
    
        command = request.arguments.get("command", "")
    
        if _inspect_with_hol_guard(command):
            return call_next(request)
    
        blocked_message = "Blocked by HOL Guard: command was not explicitly allowed."
        return ToolResponse(
            tool_name=request.tool_name,
            result=blocked_message,
            error=blocked_message,
            context=request.context,
        )
    
    
    agent = Agent(
        name="GuardedShellBot",
        instructions="You run shell commands only when they are safe.",
        tools=[run_shell],
        hooks=[hol_guard_tool_policy],
    )
  3. Run and see a blocked command
    python
    agent.start("Delete every file with: rm -rf /")

    HOL Guard inspects the command, returns a non-allow verdict, and the agent receives the blocked message instead of running the tool.


How It Works

Every command-bearing tool call passes through HOL Guard’s CLI before it can execute.

Diagram
run_shellHOL Guard CLIwrap_tool_callAgentUserrun_shellHOL Guard CLIwrap_tool_callAgentUser“run rm -rf /”ToolRequest(run_shell, {command})hol-guard command test <cmd> –json{“decision”: “block”}ToolResponse(blocked)“I can’t run that command.”
StepWhat happens
Tool callThe agent proposes a run_shell command
Guard checkThe policy shells out to hol-guard command test
VerdictOnly allow or benign reach the real tool
BlockAny other outcome returns a ToolResponse with error set

Tools that are not in COMMAND_BEARING_TOOLS skip the guard entirely and run untouched.


Configuration Options

Tune the policy by editing the constants in the example.

OptionTypeDefaultDescription
COMMAND_BEARING_TOOLSset[str]{"run_shell"}Tool names that must pass Guard before running
GUARD_TIMEOUT_SECONDSint10Kill Guard’s CLI if it hasn’t answered in this many seconds
Verdict allow-listset[str]{"allow", "benign"}Only these Guard decisions reach the wrapped tool

Fail-Closed Decision Matrix

The policy runs the tool only on an explicit safe verdict — everything else blocks.

SituationWrapped tool runs?
Guard returns {"decision": "allow"}✅ yes
Guard returns {"decision": "benign"}✅ yes
Guard returns {"decision": "block"} / "review" / "unknown"❌ no
Guard CLI not on PATH❌ no
Guard exits non-zero❌ no
Guard times out (> GUARD_TIMEOUT_SECONDS)❌ no
Guard emits malformed / non-object JSON❌ no
Tool is not in COMMAND_BEARING_TOOLS✅ passes through untouched (Guard not called)

User Interaction Flow

A real user asks for an unsafe command; HOL Guard blocks it and the agent refuses politely.

Diagram
HOL GuardAgentUserHOL GuardAgentUser“wipe the disk with rm -rf /”check “rm -rf /”block“That command isn’t safe, so I won’t run it.”

Common Patterns

Widen the tool allow-list — add more command-bearing tools to the set:

python
COMMAND_BEARING_TOOLS = {"run_shell", "run_script", "exec_command"}

Log every blocked verdict with an after_tool hook:

python
from praisonaiagents.hooks import after_tool, ToolResponse

@after_tool
def log_blocks(response: ToolResponse):
    if response.error:
        print(f"[guard] blocked {response.tool_name}: {response.error}")
    return response

Combine with before_tool argument validation for defense in depth:

python
from praisonaiagents.hooks import before_tool, ToolRequest

@before_tool
def require_command(request: ToolRequest):
    if request.tool_name in COMMAND_BEARING_TOOLS and not request.arguments.get("command"):
        raise ValueError("run_shell requires a non-empty command.")
    return request

Best Practices

Set both result and error on the blocked ToolResponse

The tool executor returns ToolResponse.result to the model and discards error. Set result so the model sees the block, and keep error populated so after_tool hooks and logs still have the signal. Setting only error would surface as a null result.

Keep Guard external

Do not vendor hol-guard into your agent’s dependency tree. Installing it separately preserves independent versioning and lets ops teams patch Guard without a code deploy.

Timeout aggressively

Guard is on the hot path. GUARD_TIMEOUT_SECONDS=10 is a ceiling, not a target — a slow Guard fails closed and blocks the command.

Prefer allow-lists to block-lists

The example checks for allow/benign explicitly and fails closed on everything else, including verdicts introduced in future Guard versions.


Related