Menu
magesh.ai agent v1.0 (views are my own)
kill-chain resources about · viewing: hook_guardrails · 00:00:00
← agent.navigate: resources / defensive controls
12 min read · 3 architectures · 5 patterns · 7 references

Hook-Based Guardrails

Hooks can intercept selected agent actions before they run and record or review outcomes afterward. This guide separates executable policy from model feedback, with small reference scripts and explicit limits. Permissions and sandboxing remain the underlying enforcement boundaries.

category:
Defensive Controls · builders · security-teams
PREREQUISITE This article is part of the Agentic AI Kill Chain — read it first for the full threat model →

Enforce at the Tool Boundary

A model can propose an action; the application decides whether to execute it. A pre-execution hook can check structured inputs against policy. A post-execution hook observes an action that has already happened. Its feedback cannot undo a write or a disclosure.

Hook support varies by event and host. Shell handlers, typed callbacks, remote handlers and model-based reviews have different failure modes. An LLM evaluator is probabilistic; a deterministic rule is reproducible but can still implement the wrong policy.

Three Architectures

Claude Code

Command handlers receive event JSON on stdin. For PreToolUse, exit 2 denies the call; exit 0 leaves normal permission checks in place. Event-specific JSON can express a decision. Not every event supports blocking.

Configure matchers for the actual tool names. Protect hook files and settings from agent edits; repository presence alone does not establish that a hook is trusted or enabled. Hook reference.

Agent SDK

Typed callbacks let the application evaluate tool use against identity, task and resource policy. Check supported events, decision fields and error behavior for the installed SDK language and version.

Test allowed, denied, malformed and timed-out requests through the actual host. A standalone handler test does not validate its integration. SDK documentation.

Kiro

Kiro supports shell actions and agent prompts. For a Pre Tool Use shell action, a nonzero exit blocks invocation. Printing “BLOCKED” with a successful exit only supplies text; it is not a denial.

Create the trigger and tool matcher in the installed IDE or CLI configuration and verify that it fires. Configuration formats differ by version. Kiro action contract (documentation reviewed September 18, 2026).

Delegation and Permissions

Claude Code’s SubagentStart event can supply context; it does not grant or restrict the child’s permissions. Use supported agent permission settings and execution-time authorization. SubagentStop can influence completion, but is not automatically a redaction boundary for returned data.

At each delegation, preserve the requesting identity, task, allowed resources and permitted actions. Validate tool calls under that scope, and check outputs before sharing them with another agent or an external recipient.

Five Patterns

1. Restrict direct writes to an explicit source tree

The Python 3.9+ example accepts only Edit/Write events targeting the operator-selected workspace’s existing src directory. It rejects hidden paths, selected instruction/configuration files, traversal outside that tree, and resolved symlink escapes. There is no blanket .example or .template exception.

This checks paths, not file contents. It does not stop secrets being written to an ordinary source file, writes through other tools, or a filesystem race after the check. Enforce filesystem permissions and sandboxing separately. Keep this script outside agent-writable paths.

protect-files.py
#!/usr/bin/env python3
"""Illustrative Claude Code PreToolUse path policy, not a filesystem sandbox.

Operator sets AGENT_WORKSPACE to an absolute, existing project directory.
Only Edit/Write beneath its existing src directory may pass this hook.
Other tools, races after checking, and secret content need separate controls.
Exit 0 leaves the host's normal permission checks in place; it does not approve.
"""
import json
import os
from pathlib import Path
import sys


def deny(reason):
    print(f"Blocked: {reason}", file=sys.stderr)
    raise SystemExit(2)


try:
    event = json.load(sys.stdin)
    if not isinstance(event, dict) or event.get("hook_event_name") != "PreToolUse":
        deny("expected a PreToolUse event")
    if event.get("tool_name") not in ("Edit", "Write"):
        deny("this handler only supports Edit and Write")
    tool_input = event.get("tool_input")
    raw = tool_input.get("file_path") if isinstance(tool_input, dict) else None
    if not isinstance(raw, str) or not raw.strip():
        deny("missing file_path")
    root = Path(os.environ["AGENT_WORKSPACE"])
    if not root.is_absolute():
        deny("AGENT_WORKSPACE must be absolute")
    root = root.resolve(strict=True)
    allowed = (root / "src").resolve(strict=True)
    if not allowed.is_dir() or allowed == root or not allowed.is_relative_to(root):
        deny("src must be a directory inside the workspace")
    candidate = Path(raw)
    if not candidate.is_absolute():
        candidate = root / candidate
    target = candidate.resolve()
    if target == allowed or not target.is_relative_to(allowed):
        deny("writes are restricted to the src tree")
    parts = target.relative_to(allowed).parts
    if any(part.startswith(".") for part in parts):
        deny("hidden paths are excluded, including .env and .example directories")
    if target.name in ("CLAUDE.md", "AGENTS.md", "wrangler.toml", "GoogleService-Info.plist"):
        deny("instruction or configuration file")
    if target.suffix == ".xcconfig":
        deny("configuration file")
except (ValueError, TypeError, KeyError, OSError, RuntimeError):
    deny("invalid input or unresolved policy configuration")
raise SystemExit(0)

Download protect-files.py

2. Deny shell execution when the task does not need it

The previous regex denylist could be bypassed by alternate flags, additional targets and equivalent commands. This replacement denies every Bash call. If shell access is necessary, use a separate constrained runner and an explicit approval policy rather than adding “safe” substrings.

block-shell.sh
#!/bin/sh
# Attach to Claude Code PreToolUse with matcher Bash.
# Every shell command is denied. This script never executes its input.
printf '%s\n' 'Blocked: shell execution is disabled for this session.' >&2
exit 2

Download block-shell.sh. Other execution tools and subprocess interfaces need their own policy.

3. Review outputs after edits

Secret scanners and configuration checks can report suspicious changes. Use the host’s supported context-output format to deliver feedback; a warning does not guarantee the model will correct it. Require a passing check before committing or releasing where the risk warrants it. This is an integration pattern, not a supplied scanner implementation.

4. Deny mutations in a Kiro audit configuration

Configure this shell action on Pre Tool Use for each mutation-capable tool in a dedicated read-only audit configuration. It always denies the matched calls; it does not infer an “audit mode” from model text. Include shell, network mutations and delegated tools in the permission design.

kiro-audit-gate.sh — shell action only
#!/bin/sh
# Configure as a Kiro Pre Tool Use shell action for mutation tools.
# This action is always read-only; attach it only to an audit configuration.
printf '%s\n' 'Blocked: mutations are disabled in this audit configuration.' >&2
exit 1

Download the Kiro shell action. Its nonzero exit is tested as a script; the IDE/CLI trigger and matcher must be tested in your installed version.

5. Restore task context after compaction

Reintroducing task and policy reminders can help continuity. It does not create immutable instructions or eliminate prompt injection. Keep authorization in the host, and test whether resumed sessions retain the intended permissions. This pattern has no supplied compaction integration.

What These Examples Do Not Establish

  • A hook cannot enforce a tool path it never intercepts. Inventory aliases, MCP tools, delegation and alternative execution routes.
  • Pre-tool checks do not inspect a final text response. Restrict sensitive reads and enforce recipient/output policy separately.
  • Keep hooks, environment variables and settings under operator control. A compromised policy configuration can invalidate the checks.
  • Path resolution followed by execution can race with filesystem changes. Use an OS-enforced boundary for adversarial workloads.
  • Test multi-step behavior and host error/timeout handling. A successful standalone test is not evidence that the host fails closed.

Wire, Test and Verify

Install the downloaded scripts in a trusted location outside the agent’s write scope. Replace the absolute paths below with your own. The project must already contain src. Python must be installed. Do not copy the example paths literally.

.claude/settings.json — replace the absolute paths
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "AGENT_WORKSPACE='/absolute/path/to/project' python3 '/trusted/hooks/protect-files.py'"
        }]
      },
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "/bin/sh '/trusted/hooks/block-shell.sh'"
        }]
      }
    ]
  }
}

Verify an ordinary source edit succeeds, protected and out-of-scope paths fail, and every shell call is denied. Also test symlinks, malformed input, missing configuration and host failures. These scripts never execute the command strings used in their checks. Record the host and SDK versions alongside integration results.

Download the three handlers and the regression suite into one directory, then run python3 test_hook_examples.py -v. The suite uses synthetic paths and inert command strings.

The downloadable examples are checked with standalone regression tests. They have not been exercised inside each supported CLI, SDK or IDE. Use adversarial testing and observable outcomes to validate the full deployment.

This work represents the author's independent research and personal views. It is not related to or endorsed by the author's employer.