-
Notifications
You must be signed in to change notification settings - Fork 101
feat(sdk): complete hooks implementation with additional context and stop hook #1547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ak684
merged 7 commits into
main
from
ak684/featsdk-complete-hooks-implementation-with-additional-context-and-stop-hook-13
Jan 6, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
bda858e
feat(sdk): complete hooks implementation with additional context and …
ak684 55c4838
refactor: reorganize hooks examples into folder structure
ak684 175b0bd
fix: break long lines in hooks example to pass linting
ak684 815a264
Merge branch 'main' into ak684/featsdk-complete-hooks-implementation-…
xingyaoww 62831a0
fix: wire up original_callback in hook processor for event forwarding
ak684 046c60a
refactor: remove jq dependency from hook example
ak684 1c9db5c
Merge branch 'main' into ak684/featsdk-complete-hooks-implementation-…
xingyaoww File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| """OpenHands Agent SDK — Hooks Example | ||
|
|
||
| Demonstrates the OpenHands hooks system. | ||
| Hooks are shell scripts that run at key lifecycle events: | ||
|
|
||
| - PreToolUse: Block dangerous commands before execution | ||
| - PostToolUse: Log tool usage after execution | ||
| - UserPromptSubmit: Inject context into user messages | ||
| - Stop: Enforce task completion criteria | ||
|
|
||
| The hook scripts are in the scripts/ directory alongside this file. | ||
| """ | ||
|
|
||
| import os | ||
| import signal | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| from pydantic import SecretStr | ||
|
|
||
| from openhands.sdk import LLM, Conversation | ||
| from openhands.sdk.hooks import HookConfig | ||
| from openhands.tools.preset.default import get_default_agent | ||
|
|
||
|
|
||
| signal.signal(signal.SIGINT, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) | ||
|
|
||
| SCRIPT_DIR = Path(__file__).parent / "hook_scripts" | ||
|
|
||
| # Configure LLM | ||
| api_key = os.getenv("LLM_API_KEY") | ||
| assert api_key is not None, "LLM_API_KEY environment variable is not set." | ||
| model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") | ||
| base_url = os.getenv("LLM_BASE_URL") | ||
|
|
||
| llm = LLM( | ||
| usage_id="agent", | ||
| model=model, | ||
| base_url=base_url, | ||
| api_key=SecretStr(api_key), | ||
| ) | ||
|
|
||
| # Create temporary workspace with git repo | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| workspace = Path(tmpdir) | ||
| os.system(f"cd {workspace} && git init -q && echo 'test' > file.txt") | ||
|
|
||
| log_file = workspace / "tool_usage.log" | ||
| summary_file = workspace / "summary.txt" | ||
|
|
||
| # Configure ALL hook types in one config | ||
| hook_config = HookConfig.from_dict( | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "terminal", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": str(SCRIPT_DIR / "block_dangerous.sh"), | ||
| "timeout": 10, | ||
| } | ||
| ], | ||
| } | ||
| ], | ||
| "PostToolUse": [ | ||
| { | ||
| "matcher": "*", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": f"LOG_FILE={log_file} " | ||
| f"{SCRIPT_DIR / 'log_tools.sh'}", | ||
| "timeout": 5, | ||
| } | ||
| ], | ||
| } | ||
| ], | ||
| "UserPromptSubmit": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": str(SCRIPT_DIR / "inject_git_context.sh"), | ||
| } | ||
| ], | ||
| } | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": f"SUMMARY_FILE={summary_file} " | ||
| f"{SCRIPT_DIR / 'require_summary.sh'}", | ||
| } | ||
| ], | ||
| } | ||
| ], | ||
| } | ||
| } | ||
| ) | ||
|
|
||
| agent = get_default_agent(llm=llm) | ||
| conversation = Conversation( | ||
| agent=agent, | ||
| workspace=str(workspace), | ||
| hook_config=hook_config, | ||
| ) | ||
|
|
||
| # Demo 1: Safe command (PostToolUse logs it) | ||
| print("=" * 60) | ||
| print("Demo 1: Safe command - logged by PostToolUse") | ||
| print("=" * 60) | ||
| conversation.send_message("Run: echo 'Hello from hooks!'") | ||
| conversation.run() | ||
|
|
||
| if log_file.exists(): | ||
| print(f"\n[Log: {log_file.read_text().strip()}]") | ||
|
|
||
| # Demo 2: Dangerous command (PreToolUse blocks it) | ||
| print("\n" + "=" * 60) | ||
| print("Demo 2: Dangerous command - blocked by PreToolUse") | ||
| print("=" * 60) | ||
| conversation.send_message("Run: rm -rf /tmp/test") | ||
| conversation.run() | ||
|
|
||
| # Demo 3: Context injection + Stop hook enforcement | ||
| print("\n" + "=" * 60) | ||
| print("Demo 3: Context injection + Stop hook") | ||
| print("=" * 60) | ||
| print("UserPromptSubmit injects git status; Stop requires summary.txt\n") | ||
| conversation.send_message( | ||
| "Check what files have changes, then create summary.txt describing the repo." | ||
| ) | ||
| conversation.run() | ||
|
|
||
| if summary_file.exists(): | ||
| print(f"\n[summary.txt: {summary_file.read_text()[:80]}...]") | ||
|
|
||
| print("\n" + "=" * 60) | ||
| print("Example Complete!") | ||
| print("=" * 60) | ||
|
|
||
| cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost | ||
| print(f"\nEXAMPLE_COST: {cost}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Hooks Examples | ||
|
|
||
| This folder demonstrates the OpenHands hooks system. | ||
|
|
||
| ## Example | ||
|
|
||
| - **33_hooks.py** - Complete hooks demo showing all four hook types | ||
|
|
||
| ## Scripts | ||
|
|
||
| The `hook_scripts/` directory contains reusable hook script examples: | ||
|
|
||
| - `block_dangerous.sh` - Blocks rm -rf commands (PreToolUse) | ||
| - `log_tools.sh` - Logs tool usage to a file (PostToolUse) | ||
| - `inject_git_context.sh` - Injects git status into prompts (UserPromptSubmit) | ||
| - `require_summary.sh` - Requires summary.txt before stopping (Stop) | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| # Set your LLM credentials | ||
| export LLM_API_KEY="your-key" | ||
| export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # optional | ||
| export LLM_BASE_URL="https://your-endpoint" # optional | ||
|
|
||
| # Run example | ||
| python 33_hooks.py | ||
| ``` | ||
|
|
||
| ## Hook Types | ||
|
|
||
| | Hook | When it runs | Can block? | | ||
| |------|--------------|------------| | ||
| | PreToolUse | Before tool execution | Yes (exit 2) | | ||
| | PostToolUse | After tool execution | No | | ||
| | UserPromptSubmit | Before processing user message | Yes (exit 2) | | ||
| | Stop | When agent tries to finish | Yes (exit 2) | | ||
| | SessionStart | When conversation starts | No | | ||
| | SessionEnd | When conversation ends | No | |
13 changes: 13 additions & 0 deletions
13
examples/01_standalone_sdk/33_hooks/hook_scripts/block_dangerous.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| #!/bin/bash | ||
| # PreToolUse hook: Block dangerous rm -rf commands | ||
| # Uses grep on raw JSON input (no jq needed) | ||
|
|
||
| input=$(cat) | ||
|
|
||
| # Block rm -rf commands by checking if the input contains the pattern | ||
| if echo "$input" | grep -q "rm -rf"; then | ||
| echo '{"decision": "deny", "reason": "rm -rf commands are blocked for safety"}' | ||
| exit 2 # Exit code 2 = block the operation | ||
| fi | ||
|
|
||
| exit 0 # Exit code 0 = allow the operation |
18 changes: 18 additions & 0 deletions
18
examples/01_standalone_sdk/33_hooks/hook_scripts/inject_git_context.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #!/bin/bash | ||
| # UserPromptSubmit hook: Inject git status when user asks about code changes | ||
|
|
||
| input=$(cat) | ||
|
|
||
| # Check if user is asking about changes, diff, or git | ||
| if echo "$input" | grep -qiE "(changes|diff|git|commit|modified)"; then | ||
| # Get git status if in a git repo | ||
| if git rev-parse --git-dir > /dev/null 2>&1; then | ||
| status=$(git status --short 2>/dev/null | head -10) | ||
| if [ -n "$status" ]; then | ||
| # Escape for JSON | ||
| escaped=$(echo "$status" | sed 's/"/\\"/g' | tr '\n' ' ') | ||
| echo "{\"additionalContext\": \"Current git status: $escaped\"}" | ||
| fi | ||
| fi | ||
| fi | ||
| exit 0 |
9 changes: 9 additions & 0 deletions
9
examples/01_standalone_sdk/33_hooks/hook_scripts/log_tools.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/bin/bash | ||
| # PostToolUse hook: Log all tool usage | ||
| # Uses OPENHANDS_TOOL_NAME env var (no jq/python needed!) | ||
|
|
||
| # LOG_FILE should be set by the calling script | ||
| LOG_FILE="${LOG_FILE:-/tmp/tool_usage.log}" | ||
|
|
||
| echo "[$(date)] Tool used: $OPENHANDS_TOOL_NAME" >> "$LOG_FILE" | ||
| exit 0 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.