Skip to main content

Overview

Hooks let you run custom shell scripts at key moments during an OpenHands session. They are configured per-repository via a .openhands/hooks.json file and work across Cloud, CLI, and local GUI setups. The hooks format is compatible with Claude Code hooks, so you can reuse hook scripts across both tools. Common use cases include:
  • Blocking dangerous commands before execution (e.g., preventing rm -rf /)
  • Enforcing quality gates before the agent finishes (e.g., requiring linting or tests to pass)
  • Logging and auditing tool usage for compliance
  • Injecting context into user prompts (e.g., appending git status)

Hook Types

HookWhen It RunsCan Block?
PreToolUseBefore the agent executes a toolYes
PostToolUseAfter a tool finishes executingNo
UserPromptSubmitBefore a user message is processedYes
StopWhen the agent tries to finishYes
SessionStartWhen a conversation beginsNo
SessionEndWhen a conversation endsNo
Blocking means the hook can prevent the operation from proceeding. For example, a Stop hook can force the agent to keep working if linting checks haven’t passed yet. Note that hooks with "async": true run in the background and can never block, regardless of event type.

Quick Start

1

Create the Hooks Directory

In your repository root, create the .openhands directory if it doesn’t already exist:
mkdir -p .openhands/hooks
2

Write a Hook Script

Create a shell script for your hook. For example, a Stop hook that requires linting to pass before the agent can finish:
.openhands/hooks/lint_check.sh
#!/bin/bash
# Stop hook: Don't let the agent stop if linting fails

cd "$OPENHANDS_PROJECT_DIR"

# Run your linter
if ! npm run lint 2>&1; then
    echo '{"decision": "deny", "reason": "Linting failed. Please fix the issues before finishing."}'
    exit 2
fi

exit 0
Make the script executable:
chmod +x .openhands/hooks/lint_check.sh
3

Create hooks.json

Create .openhands/hooks.json to register your hooks:
.openhands/hooks.json
{
  "stop": [
    {
      "matcher": "*",
      "hooks": [
        {
          "command": ".openhands/hooks/lint_check.sh",
          "timeout": 120
        }
      ]
    }
  ]
}
4

Commit to Your Repository

git add .openhands/hooks.json .openhands/hooks/
git commit -m "Add OpenHands hooks"
The next time OpenHands works on your repository, the hooks will be active automatically.

Configuration Reference

hooks.json Format

The .openhands/hooks.json file maps hook event types to matchers and commands using snake_case keys:
{
  "pre_tool_use": [
    {
      "matcher": "terminal",
      "hooks": [
        { "command": ".openhands/hooks/block_dangerous.sh", "timeout": 10 }
      ]
    }
  ],
  "stop": [
    {
      "matcher": "*",
      "hooks": [
        { "command": ".openhands/hooks/require_tests.sh", "timeout": 120 }
      ]
    }
  ]
}

Claude Code Compatibility

The hooks format is compatible with Claude Code hooks. PascalCase event keys (e.g., PreToolUse) and the {"hooks": {...}} wrapper are both supported, so you can share hook scripts between the two tools. The main differences are the file location (.openhands/hooks.json vs .claude/settings.json) and tool names (e.g., terminal vs Bash).

Hook Definition Fields

FieldTypeDefaultDescription
typestring"command"Hook type (currently only "command" is supported)
commandstring(required)Shell command or path to script to execute
timeoutinteger60Maximum execution time in seconds
asyncbooleanfalseRun in background without waiting for result

Matcher Patterns

The matcher field determines which tools trigger the hook (only relevant for PreToolUse and PostToolUse):
PatternDescriptionExample
*Matches all tools"matcher": "*"
Exact nameMatches a specific tool"matcher": "terminal"
RegexAuto-detected or wrapped in /"matcher": "/terminal|browser/"
For hooks that aren’t tool-specific (Stop, UserPromptSubmit, SessionStart, SessionEnd), set the matcher to "*" or omit it.

How Hook Scripts Work

Input

Hook scripts receive a JSON payload on stdin with details about the event:
{
  "event_type": "PreToolUse",
  "tool_name": "terminal",
  "tool_input": { "command": "rm -rf /tmp/data" },
  "session_id": "abc-123",
  "working_dir": "/workspace"
}
Additional fields may be present depending on the hook event type (e.g., tool_response for PostToolUse, message for UserPromptSubmit).
The following environment variables are also set:
VariableDescription
OPENHANDS_EVENT_TYPEThe hook event type (e.g., PreToolUse)
OPENHANDS_TOOL_NAMEThe tool being used (for tool hooks)
OPENHANDS_PROJECT_DIRThe project working directory
OPENHANDS_SESSION_IDThe current session ID

Output

Hook scripts communicate results through exit codes and optional JSON on stdout: Exit codes:
  • 0 - Success. The operation proceeds normally.
  • 2 - Block. The operation is denied.
  • Any other code - Error. The operation proceeds, but the error is logged.
JSON output (optional):
{
  "decision": "deny",
  "reason": "rm -rf commands are blocked for safety",
  "additionalContext": "Extra context to pass to the agent"
}
FieldDescription
decision"allow" or "deny" - overrides the exit code
reasonHuman-readable explanation shown in the UI
additionalContextAdditional context injected into the agent’s prompt

Real-World Example

The OpenHands Agent SDK repository uses a Stop hook that runs pre-commit checks, targeted pytest suites, and GitHub CI status verification before allowing the agent to finish: This hook setup prevents the agent from finishing if pre-commit, tests, or CI are failing.

More Examples

Block Dangerous Commands (PreToolUse)

Prevent the agent from running destructive shell commands:
.openhands/hooks/block_dangerous.sh
#!/bin/bash
# Read JSON input from stdin
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // ""')

if [[ "$command" =~ "rm -rf" ]]; then
    echo '{"decision": "deny", "reason": "rm -rf commands are blocked for safety"}'
    exit 2
fi

exit 0
.openhands/hooks.json
{
  "pre_tool_use": [
    {
      "matcher": "terminal",
      "hooks": [{ "command": ".openhands/hooks/block_dangerous.sh", "timeout": 10 }]
    }
  ]
}

Enforce Linting Before Stop

Don’t let the agent finish until linting passes - this is what prevents linting errors from being committed:
.openhands/hooks/lint_on_stop.sh
#!/bin/bash
cd "$OPENHANDS_PROJECT_DIR"

# Run pre-commit or your linter
if ! pre-commit run --all-files 2>&1; then
    echo '{"decision": "deny", "reason": "Linting failed. Fix the issues before finishing."}'
    exit 2
fi

exit 0
.openhands/hooks.json
{
  "stop": [
    {
      "matcher": "*",
      "hooks": [{ "command": ".openhands/hooks/lint_on_stop.sh", "timeout": 120 }]
    }
  ]
}

Log All Tool Usage (PostToolUse)

Log every tool the agent uses for auditing:
.openhands/hooks/log_tools.sh
#!/bin/bash
echo "[$(date)] Tool: $OPENHANDS_TOOL_NAME" >> /tmp/tool_usage.log
exit 0
.openhands/hooks.json
{
  "post_tool_use": [
    {
      "matcher": "*",
      "hooks": [{ "command": ".openhands/hooks/log_tools.sh", "timeout": 5 }]
    }
  ]
}

Inject Git Context (UserPromptSubmit)

Automatically include git status when the user asks about code changes:
.openhands/hooks/inject_git_context.sh
#!/bin/bash
input=$(cat)

if echo "$input" | grep -qiE "(changes|diff|git|commit|modified)"; then
    if git rev-parse --git-dir > /dev/null 2>&1; then
        status=$(git status --short 2>/dev/null | head -10)
        if [ -n "$status" ]; then
            escaped=$(echo "$status" | sed 's/"/\\"/g' | tr '\n' ' ')
            echo "{\"additionalContext\": \"Current git status: $escaped\"}"
        fi
    fi
fi

exit 0

Combine Multiple Hooks

You can configure multiple hook types and multiple hooks per event:
.openhands/hooks.json
{
  "pre_tool_use": [
    {
      "matcher": "terminal",
      "hooks": [
        { "command": ".openhands/hooks/block_dangerous.sh", "timeout": 10 }
      ]
    }
  ],
  "post_tool_use": [
    {
      "matcher": "*",
      "hooks": [
        { "command": ".openhands/hooks/log_tools.sh", "timeout": 5, "async": true }
      ]
    }
  ],
  "stop": [
    {
      "matcher": "*",
      "hooks": [
        { "command": ".openhands/hooks/lint_on_stop.sh", "timeout": 120 }
      ]
    }
  ]
}

Viewing Active Hooks

Use the /skills command in the CLI to see loaded skills, hooks, and MCPs for the current session.

Tips

  • Keep hooks fast. Hooks run synchronously (unless marked async) and add latency to agent actions. Use reasonable timeouts.
  • Use jq for JSON parsing. Hook scripts receive JSON input on stdin. The jq tool is available in the sandbox for parsing fields like tool_input.command.
  • Use environment variables for simple hooks. For PostToolUse hooks that only need the tool name, $OPENHANDS_TOOL_NAME avoids the need for JSON parsing.
  • Test hooks locally. You can test hook scripts by piping JSON to them:
    echo '{"event_type":"Stop"}' | bash .openhands/hooks/lint_on_stop.sh
    echo "Exit code: $?"
    
  • Async hooks can’t block. Hooks with "async": true run in the background and cannot block operations. Use async for logging or telemetry, not for enforcement.

See Also