Skip to content

Add OpenHands stop hook for pre-commit checks#21

Open
malhotra5 wants to merge 1 commit into
mainfrom
add-openhands-precommit-hook
Open

Add OpenHands stop hook for pre-commit checks#21
malhotra5 wants to merge 1 commit into
mainfrom
add-openhands-precommit-hook

Conversation

@malhotra5
Copy link
Copy Markdown
Member

Summary

Adds an OpenHands stop hook that runs pre-commit checks before allowing the agent to finish. This ensures code quality by blocking agent completion when pre-commit checks fail.

Changes

  • .openhands/hooks.json - Hook configuration that registers the on_stop.sh hook to run when the agent attempts to stop/finish
  • .openhands/hooks/on_stop.sh - Shell script that:
    • Runs uv run pre-commit run --all-files
    • Blocks stop (exit code 2) if pre-commit fails
    • Returns detailed error output as JSON for the agent to act on
    • Allows stop if all checks pass

How it works

When an agent tries to finish its task, the hook intercepts the stop action and:

  1. Runs pre-commit run --all-files using uv (falls back to direct pre-commit if uv unavailable)
  2. If pre-commit passes → returns {"decision": "allow"} and allows the agent to complete
  3. If pre-commit fails → returns {"decision": "deny", "additionalContext": "<error details>"} with exit code 2, blocking the agent and providing feedback

Future enhancements

This is the first of potentially more hooks. Additional capabilities we may add later (inspired by the SDK repo):

  • Smart test execution based on changed files
  • CI status monitoring before completion

Add .openhands/hooks directory with on_stop.sh hook that runs pre-commit
before allowing the agent to finish. This ensures code quality by blocking
agent completion when pre-commit checks fail.

Files added:
- .openhands/hooks.json - Hook configuration
- .openhands/hooks/on_stop.sh - Pre-commit hook script

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
__init__.py10100% 
app.py803951%30, 33, 36, 42, 44, 47, 50–53, 56–57, 60, 67–68, 71–72, 76, 84–85, 88, 95–96, 98, 101–102, 105, 110–118, 120–122
auth.py77692%59, 92, 149, 200, 208–209
config.py310100% 
constants.py120100% 
db.py442640%37–39, 48–49, 51–52, 54, 62, 69, 79, 82–83, 87–88, 96, 104, 109, 114, 117–123
dispatcher.py1353474%53, 73, 85, 87–88, 157, 161, 184, 203–205, 233–235, 238–240, 275–276, 300–307, 328–329, 339–340, 347–348, 350
exceptions.py40100% 
execution.py22212742%45–47, 56, 89–92, 100–102, 110, 115–119, 133, 135, 137–140, 142–147, 149, 151–158, 160–161, 163, 178, 183, 194, 200–202, 213, 219–221, 268, 276, 280, 282–283, 288–289, 294, 342, 344, 362, 365–368, 390–393, 395, 403–404, 407, 413, 476–482, 485–486, 488, 490–492, 495, 498, 501–504, 506, 509–510, 513–515, 519, 522, 526–529, 531, 539–540, 544–546, 548–554, 558, 560, 569–571, 573–575
logger.py531866%25–26, 36, 46–47, 49–55, 68, 88, 90–93
models.py650100% 
router.py1035645%71–72, 92, 94, 97, 99, 113, 126, 128–129, 131–132, 135–137, 148–150, 168–171, 190, 193, 196, 203, 205, 234, 239–241, 244–246, 250–251, 256, 260–263, 265, 273, 275–276, 281–282, 285, 287, 289–290, 299, 320–322, 326
scheduler.py57984%124–125, 162–163, 178–179, 189–190, 192
schemas.py1441192%67–69, 71, 129, 153–154, 157, 162, 167, 173
uploads.py1075944%138–141, 149–151, 157–158, 161, 170–171, 174–175, 183–184, 186–189, 192–195, 197, 199–201, 203–206, 208–209, 211, 226, 232–233, 236, 239, 242, 245, 247, 260–261, 275, 278–280, 282–283, 285, 291–292, 305, 313–315, 319
watchdog.py1074756%63–64, 76–77, 82–84, 96–97, 241–242, 244, 246, 255, 257–259, 261, 268–270, 272–274, 276–277, 279, 294, 296, 301–304, 306–311, 313–319, 321
storage
   __init__.py50100% 
   factory.py110100% 
   file_store.py18572%11, 20, 25, 30, 54
   google_cloud.py751086%103–108, 142–143, 196, 198
   s3.py1151487%100, 102–103, 107, 109, 190, 213–215, 269–270, 275, 337–338
utils
   __init__.py40100% 
   api_key.py322425%40–41, 46–48, 50, 55, 60, 62–65, 67–68, 70–71, 73, 79, 81–82, 89, 91–92, 98
   cron.py45686%39, 45, 74, 80, 123, 140
   run.py751284%74–76, 172–174, 179–181, 228, 234–235
   sandbox.py1088224%32–37, 51–52, 57–60, 62–64, 66–72, 84, 86, 96–97, 99–101, 103–104, 107–108, 114, 120–122, 132–133, 138–144, 167–168, 170–174, 176–180, 217–218, 220, 222–225, 230–231, 234, 236–237, 243–245, 250–252, 257–258, 266–268, 270
   tarball_validation.py480100% 
   time.py30100% 
TOTAL178158567% 

@github-actions
Copy link
Copy Markdown

🚀 Deploy Preview PR Created/Updated

A deploy preview has been created/updated for this PR.

Deploy PR: https://github.com/OpenHands/deploy/pull/3598
Automation SHA: 2888f982497d855b6d900b4abf8d536be64ee491
Last updated: Mar 30, 2026, 01:36:04 PM ET

Once the deploy PR's CI passes, the automation service will be deployed to the feature environment.

@malhotra5 malhotra5 marked this pull request as ready for review March 30, 2026 17:36
Copy link
Copy Markdown

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Code is clean and pragmatic, but where's the proof it works?

The implementation is straightforward and doesn't over-engineer the problem. However, the PR description talks about how it works without showing that it actually works. See inline comments.

# --------------------------
>&2 echo "=== Running pre-commit run --all-files ==="
if command -v uv &> /dev/null; then
PRECOMMIT_OUTPUT=$(uv run pre-commit run --all-files 2>&1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion - Error Handling: What happens if pre-commit isn't installed at all (neither via uv nor standalone)?

The command will fail and PRECOMMIT_EXIT will be non-zero, causing the hook to block with a message about "pre-commit checks failed" - but the real issue is that pre-commit isn't available. The agent might waste time trying to "fix" non-existent issues.

Consider adding an explicit check:

if ! command -v uv &> /dev/null && ! command -v pre-commit &> /dev/null; then
    >&2 echo "⚠️  Neither 'uv' nor 'pre-commit' found in PATH"
    log_issue "## Pre-commit Not Found\n\nCannot run pre-commit checks. Please install pre-commit or uv."
fi

This makes the failure mode explicit rather than cryptic.

Comment thread .openhands/hooks.json
{
"type": "command",
"command": ".openhands/hooks/on_stop.sh",
"timeout": 300
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Acceptable - Timeout: 300 seconds (5 minutes) for pre-commit is generous but reasonable for large repos or slow CI environments.

If this becomes a bottleneck in practice, consider making it configurable or running --files on changed files only (though --all-files is the safer default for agent work).

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.

3 participants