Skip to content

feat(plugins): let pre_tool_call hooks block tool calls#5385

Closed
gianfrancopiana wants to merge 1 commit into
NousResearch:mainfrom
gianfrancopiana:codex/plugin-tool-policy-hooks
Closed

feat(plugins): let pre_tool_call hooks block tool calls#5385
gianfrancopiana wants to merge 1 commit into
NousResearch:mainfrom
gianfrancopiana:codex/plugin-tool-policy-hooks

Conversation

@gianfrancopiana

Copy link
Copy Markdown
Contributor

What does this PR do?

This adds a simple plugin policy hook for tool calls.

A pre_tool_call hook can now block a tool call by returning:
{"action":"block","message":"..."}

This works for both:

  • tools routed through model_tools
  • agent-level tools handled directly in run_agent

If a tool is blocked, Hermes now returns a clean JSON error and skips execution side effects like:

  • running the tool
  • creating checkpoints
  • resetting read-loop tracking
  • marking a blocked tool as if it had run

Related Issue

No tracked issue for this specific change.

Note: PR #4610 works on the same problem. This PR covers the same basic hook-blocking feature, but also applies it to direct agent-level tools and prevents side effects before blocked calls.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • Added a helper in hermes_cli/plugins.py to read the supported pre_tool_call block result
  • Updated model_tools.handle_function_call() to stop blocked calls cleanly
  • Updated run_agent.py so the same blocking behavior also applies to agent-level tools
  • Made sure blocked calls do not trigger pre-execution side effects
  • Added focused tests for plugin hooks, model_tools, and run_agent paths

How to Test

  1. Run:
    pytest -q tests/test_plugins.py tests/test_model_tools.py tests/test_run_agent.py

  2. Confirm all tests pass.

  3. Optional: register a test pre_tool_call hook that returns:
    {"action":"block","message":"blocked by policy"}
    and confirm the tool returns a JSON error without running.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Validated locally:

  • pytest -q tests/test_plugins.py tests/test_model_tools.py tests/test_run_agent.py
  • Result: 264 passed

Local full-suite note:

  • In clean worktrees, pytest tests/ -q -x fails on both main and this branch with the same unrelated failure in tests/gateway/test_matrix_voice.py, so I am not claiming a clean local full-suite pass from this machine. CI should be the full-branch signal for this PR.

AI assistance

This PR was implemented with AI assistance using GPT-5.4 xhigh.

@gianfrancopiana gianfrancopiana force-pushed the codex/plugin-tool-policy-hooks branch from 47f1a81 to 3c345c9 Compare April 6, 2026 02:12
teknium1 pushed a commit that referenced this pull request Apr 14, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR #5385 (gianfrancopiana) and PR #4610 (oredsecurity).
teknium1 pushed a commit that referenced this pull request Apr 14, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR #5385 (gianfrancopiana) and PR #4610 (oredsecurity).
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #9377. Your implementation was the basis for the salvage — the dual-path blocking design (handle_function_call + _invoke_tool), the skip_pre_tool_call_hook flag, and the get_pre_tool_call_block_message helper were all adapted from your work. Your authorship is preserved in the git commit. Thanks for the thorough contribution!

@teknium1 teknium1 closed this Apr 14, 2026
hermes-agent-dhabibi pushed a commit to hermes-agent-dhabibi/hermes-agent that referenced this pull request Apr 14, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
@gianfrancopiana gianfrancopiana deleted the codex/plugin-tool-policy-hooks branch April 14, 2026 17:13
ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.

Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).

Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.

Salvaged from PR NousResearch#5385 (gianfrancopiana) and PR NousResearch#4610 (oredsecurity).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants