Skip to main content

View source on GitHub

A self-contained example showing how to use PreToolUse hooks in a plugin to blacklist dangerous shell commands. When the agent tries to execute a risky command, the hook blocks it with helpful (and slightly snarky) feedback. This example demonstrates the blacklist approach: block known dangerous patterns while allowing everything else to proceed normally.

What’s in the Box

The safety-guardian/ plugin bundles:
  • Hooks (hooks/hooks.json) - PreToolUse hook that intercepts terminal commands
  • Skill (skills/safety-guardian/SKILL.md) - Documentation about what’s protected
  • Plugin manifest (.claude-plugin/plugin.json) - Standard Claude Code plugin format

How It Works

Protected Patterns

The hook blocks: All other commands work normally - only these specific dangerous patterns are blocked.
The rm -rf and chmod 777 rules only fire on system directories (/etc, /usr, /var, /home, /bin, /lib, /root, /dev, …). Ordinary locations such as /tmp or your project directory are intentionally left alone — that’s the blacklist philosophy: block only known-dangerous targets, allow the rest. (So rm -rf /tmp is not blocked; use the curl … | bash demo below to see a block.)

Try It

Use the companion load-plugin example:
Replace ref: main with your branch name if testing before merge: --ref add-hooks-examples
The OpenHands agent has its own safety training and may refuse or rewrite an obviously-dangerous command (e.g. turn curl … | bash into a download-then-inspect) before the hook ever runs. That’s expected — the PreToolUse hook is a deterministic backstop that enforces the policy no matter what the agent decides. If the agent declines, re-run the demo (framing it as “run verbatim to test the guard” usually gets the command through), then confirm the block in the conversation events: a hook block is a PreToolUse HookExecutionEvent with exit_code: 2 / blocked: true and a reason.

The Hook

The magic happens in hooks/hooks.json:
safety-guardian/hooks/hooks.json
How it works:
  1. PreToolUse - Runs before the terminal tool executes
  2. matcher: "terminal" - Only applies to shell commands (not file edits, etc.)
  3. type: "command" - The command is a shell script run by the hook runner (via /bin/sh -c). Keep it POSIX-compatible and inline — see the note below on why these examples don’t reference external .sh files.
  4. Exit codes:
    • 0 = Allow the command
    • 2 = Block the command (with reason in JSON output)
    • Other = Log error, but allow (non-blocking)
The inline script:
  • Reads the tool invocation JSON from stdin (input=$(cat))
  • Uses grep -qE to check for dangerous patterns
  • Prints {"decision": "deny", "reason": "..."} to stdout if blocked
  • Returns exit code 2 to enforce the block
The hook runner executes command through /bin/sh -c, so wrapping the body in bash -c '...' makes any apostrophe in a message (I've, that's) terminate the quote and break the script. We also can’t point command at a bundled hooks/scripts/*.sh: when this runs as a plugin, hooks execute with the working directory set to the agent’s workspace (not the plugin directory) and there is no plugin-root path variable, so a relative script path won’t resolve. Inlining a plain POSIX-sh script avoids both traps.

Blacklist vs. Whitelist

This example uses a blacklist approach:
  • ✅ Pro: Most commands work normally
  • ✅ Pro: Easier to get started
  • ❌ Con: Can’t catch every dangerous pattern
  • ❌ Con: Clever variations might slip through
For high-security scenarios, see the companion command-whitelist example that shows the whitelist approach (only allow explicitly approved commands).

Hook Types

Hooks can intercept different lifecycle events:

Plugin Structure

This follows the Claude Code plugin format, compatible with:
  • OpenHands Cloud plugin launcher
  • Claude Desktop plugin marketplace
  • Any system supporting the .claude-plugin spec

OpenHands Hooks Guide

Full hook documentation

Plugin System

How plugins work

load-plugin

Programmatic plugin loading

launch-plugin-badge

No-code plugin launcher

command-whitelist

Whitelist approach (opposite strategy)

Real-World Use Cases

  • Onboarding agents - Prevent trainees from dangerous operations
  • Shared environments - Protect against accidental damage
  • Compliance - Enforce security policies automatically
  • Education - Teach safe command practices
  • Testing - Prevent test scripts from harming the host

Extending the Example

Want to add your own patterns? Edit hooks/hooks.json and add another if block:
The inline bash makes it easy to iterate without rebuilding images or restarting servers.