[PR #6500] fix: Handle BlockedError in Plugin.trigger for graceful hook blocking #11936

Closed
opened 2026-02-16 18:16:52 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/6500

State: closed
Merged: No


Summary

Fix graceful handling of hook blocking (exit code 2) in Plugin.trigger(). Previously, hooks returning exit code 2 would crash the entire message flow instead of gracefully blocking.

Problem

When a Claude Code hook returns exit code 2 (which means "block this operation"), OpenCode's plugin system would crash the entire message flow instead of handling it gracefully.

Root Cause

  1. Plugin.trigger() had no try-catch - Any error thrown by hooks propagated up and killed the caller
  2. hooks-plugin threw generic Error - Exit code 2 threw new Error(stderr) which is indistinguishable from actual errors

How to Reproduce

  1. Create a UserPromptSubmit hook that returns exit code 2:

    #!/usr/bin/env python3
    import sys
    print("Blocked by policy", file=sys.stderr)
    sys.exit(2)
    
  2. Configure the hook in ~/.claude/settings.json:

    {
      "hooks": {
        "UserPromptSubmit": [{ "hooks": [{ "command": "python3 /path/to/hook.py" }] }]
      }
    }
    
  3. Send any message in OpenCode

Expected: Message blocked gracefully, stderr shown to user
Actual (before fix): Entire message flow crashes, error propagates up

Claude Code vs OpenCode Comparison

Aspect Claude Code OpenCode (before)
Exit Code 2 Graceful block, shows stderr throw Error → crash
Error handling Built-in containment No try-catch in trigger
Block semantics Control flow signal Hard failure

Solution

1. Added BlockedError class (hooks-plugin)

export class BlockedError extends Error {
  constructor(
    public readonly hookEvent: string,
    public readonly reason: string
  ) {
    super(reason)
    this.name = "BlockedError"
  }
}

2. Updated exit code 2 handling (hooks-plugin)

// Before
if (result.exitCode === 2) {
  throw new Error(result.stderr)
}

// After
if (result.exitCode === 2) {
  throw new BlockedError("PreToolUse", result.stderr)
}

3. Added try-catch in Plugin.trigger() (opencode core)

try {
  await fn(input, output)
} catch (e) {
  if (isBlockedError(e)) {
    log.info("hook blocked", { event: name, hookEvent: e.hookEvent, reason: e.reason })
    return { ...output, blocked: { hookEvent: e.hookEvent, reason: e.reason } }
  }
  throw e  // Re-throw non-BlockedError
}

Changes

  • packages/opencode/src/plugin/index.ts:

    • Added BlockedResult interface
    • Added isBlockedError() type guard (duck typing for dynamic plugin loading)
    • Wrapped hook execution in try-catch
    • Returns { ...output, blocked: {...} } when blocked
  • packages/opencode/test/plugin/plugin.test.ts:

    • Tests for BlockedError handling pattern

Testing

bun test test/plugin/plugin.test.ts
# 4 pass, 0 fail

🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode

**Original Pull Request:** https://github.com/anomalyco/opencode/pull/6500 **State:** closed **Merged:** No --- ## Summary Fix graceful handling of hook blocking (exit code 2) in `Plugin.trigger()`. Previously, hooks returning exit code 2 would crash the entire message flow instead of gracefully blocking. ## Problem When a Claude Code hook returns exit code 2 (which means "block this operation"), OpenCode's plugin system would crash the entire message flow instead of handling it gracefully. ### Root Cause 1. **`Plugin.trigger()` had no try-catch** - Any error thrown by hooks propagated up and killed the caller 2. **hooks-plugin threw generic `Error`** - Exit code 2 threw `new Error(stderr)` which is indistinguishable from actual errors ### How to Reproduce 1. Create a UserPromptSubmit hook that returns exit code 2: ```python #!/usr/bin/env python3 import sys print("Blocked by policy", file=sys.stderr) sys.exit(2) ``` 2. Configure the hook in `~/.claude/settings.json`: ```json { "hooks": { "UserPromptSubmit": [{ "hooks": [{ "command": "python3 /path/to/hook.py" }] }] } } ``` 3. Send any message in OpenCode **Expected**: Message blocked gracefully, stderr shown to user **Actual (before fix)**: Entire message flow crashes, error propagates up ### Claude Code vs OpenCode Comparison | Aspect | Claude Code | OpenCode (before) | |--------|-------------|-------------------| | Exit Code 2 | Graceful block, shows stderr | `throw Error` → crash | | Error handling | Built-in containment | No try-catch in trigger | | Block semantics | Control flow signal | Hard failure | ## Solution ### 1. Added `BlockedError` class (hooks-plugin) ```typescript export class BlockedError extends Error { constructor( public readonly hookEvent: string, public readonly reason: string ) { super(reason) this.name = "BlockedError" } } ``` ### 2. Updated exit code 2 handling (hooks-plugin) ```typescript // Before if (result.exitCode === 2) { throw new Error(result.stderr) } // After if (result.exitCode === 2) { throw new BlockedError("PreToolUse", result.stderr) } ``` ### 3. Added try-catch in `Plugin.trigger()` (opencode core) ```typescript try { await fn(input, output) } catch (e) { if (isBlockedError(e)) { log.info("hook blocked", { event: name, hookEvent: e.hookEvent, reason: e.reason }) return { ...output, blocked: { hookEvent: e.hookEvent, reason: e.reason } } } throw e // Re-throw non-BlockedError } ``` ## Changes - `packages/opencode/src/plugin/index.ts`: - Added `BlockedResult` interface - Added `isBlockedError()` type guard (duck typing for dynamic plugin loading) - Wrapped hook execution in try-catch - Returns `{ ...output, blocked: {...} }` when blocked - `packages/opencode/test/plugin/plugin.test.ts`: - Tests for BlockedError handling pattern ## Testing ```bash bun test test/plugin/plugin.test.ts # 4 pass, 0 fail ``` --- 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
yindo added the pull-request label 2026-02-16 18:16:52 -05:00
yindo closed this issue 2026-02-16 18:16:52 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#11936