Stop Hook Exit Code 2 Failure In Plugin Architecture
Understanding Stop Hook Failures in Plugin Architectures
Have you encountered issues with stop hooks in your plugin-based architecture? Specifically, are you struggling with scenarios where returning exit code 2 fails to trigger the expected continuation? This article delves into a perplexing problem encountered in Claude Code, where stop hooks, designed to signal forced continuation via exit code 2, fail to resume agent execution when implemented within the plugin architecture. Instead of the desired behavior, Claude Code displays ⏺ Stop hook prevented continuation and abruptly halts, leaving users frustrated and workflows interrupted. If you're grappling with this issue, you're in the right place. We'll explore the underlying mechanisms, dissect the bug, provide steps to reproduce it, and offer a comprehensive analysis to help you understand and potentially resolve the problem. The core of the issue lies in how Claude Code interprets exit code 2 differently when the hook is loaded from a plugin compared to direct installation in the .claude/hooks/ directory. This discrepancy suggests a potential oversight in the handling of plugin-based hooks, impacting the reliability and predictability of agent workflows. Let's unravel this mystery and find a solution together.
Background: Stop Hook Continuation Protocol
To fully grasp the issue, it's crucial to understand the stop hook continuation protocol. This protocol operates on a two-layer communication system, ensuring seamless interaction between the agent and the hooks.
1. JSON Output Structure
The first layer involves a structured JSON output, which acts as a signaling mechanism. This output communicates the hook's decision and the rationale behind it. Here's a typical JSON structure:
{
"decision": "block",
"reason": "Instructions for continuing execution..."
}
In this structure, the decision field indicates whether the hook wants to block the stoppage, and the reason field provides instructions for the agent to continue its execution. This structured format allows for clear and unambiguous communication of the hook's intent.
2. Exit Codes
The second layer of the protocol relies on exit codes, which serve as a crucial signal to Claude Code about the hook's disposition. Different exit codes convey distinct instructions:
- Exit 0: This code signals that the agent should stop normally. The standard output (STDOUT) from the hook is displayed to the user, providing a natural conclusion to the agent's execution.
- Exit 1: This code indicates that the agent should stop due to an error. The standard error (STDERR) from the hook is displayed to the user, highlighting the issue that led to the stoppage.
- Exit 2: This is the critical code for our discussion. It signals that the stop hook intends to block the stoppage and force continuation. The standard error (STDERR) from the hook, containing the
reasonfield with continuation instructions, should be passed to Claude.
When a stop hook exits with code 2 and provides continuation instructions in the reason field, Claude Code is expected to perform the following actions:
- Prevent the agent from stopping, ensuring the workflow isn't prematurely terminated.
- Pass the instructions from the
reasonfield to Claude, providing the agent with the necessary guidance for the next steps. - Resume agent execution with those new instructions, allowing the workflow to proceed smoothly.
However, as we'll explore, the actual behavior deviates from this expected flow in certain scenarios, leading to the bug we're investigating.
The Bug: Divergence from Expected Behavior
Now, let's pinpoint the core of the issue. The bug manifests as a divergence from the expected behavior when a stop hook returns exit code 2 within the plugin architecture.
Expected behavior: Exit code 2 → Agent continues with instructions.
This is the ideal scenario, where the agent seamlessly resumes execution based on the instructions provided by the hook.
Actual behavior: Exit code 2 → ⏺ Stop hook prevented continuation → Agent stops.
This is where the problem lies. Instead of continuing, Claude Code displays the message ⏺ Stop hook prevented continuation and halts the agent's execution, effectively negating the purpose of the stop hook and disrupting the workflow.
This discrepancy highlights a critical flaw in the system's handling of exit code 2 within the plugin architecture. It suggests that the mechanism for forcing continuation, which should be triggered by exit code 2, is not functioning as intended when hooks are implemented as plugins.
Context: The Shift to Plugin Marketplace
Understanding the context in which this bug emerged is crucial for tracing its origins. This regression appeared after migrating hooks from direct .claude/hooks/ installation to the plugin marketplace system. This shift, while intended to enhance modularity and ease of use, inadvertently introduced a discrepancy in how stop hook exit codes are interpreted.
Working configuration:
- Hooks directly in
.claude/hooks/ - Stop hook script exits with code 2
- Agent resumes with instructions ✓
In the original setup, where hooks were directly placed in the .claude/hooks/ directory, the system functioned as expected. When a stop hook script exited with code 2, the agent correctly resumed execution with the provided instructions.
Broken configuration:
- Hooks installed via plugin (e.g.,
plugins/my-plugin/hooks/) - Same stop hook script exits with code 2
- Agent shows "Stop hook prevented continuation" and halts ✗
However, the introduction of the plugin marketplace system changed this behavior. When the same stop hook script is packaged as a plugin and installed, exiting with code 2 no longer triggers the expected continuation. Instead, the agent displays the error message and halts, effectively breaking the workflow.
This comparison clearly indicates that the issue is not with the hook implementation itself, but rather with how Claude Code handles hooks within the plugin architecture. The migration to the plugin marketplace seems to have introduced a change in the interpretation of exit code 2, leading to the observed bug.
Steps to Reproduce: Witnessing the Bug in Action
To fully grasp the issue, let's walk through the steps to reproduce the bug. This hands-on approach will allow you to witness the problem firsthand and solidify your understanding.
-
Create a plugin that provides a stop hook:
Start by creating a plugin structure with a stop hook implementation. The directory structure might look like this:
plugins/ └── my-plugin/ └── hooks/ └── Stop/ └── my_handler.rbThis structure defines a plugin named
my-pluginwith a stop hook implemented in themy_handler.rbfile. -
Implement the hook to return exit code 2 with continuation instructions:
Next, implement the hook script to return exit code 2 along with the necessary continuation instructions. Here's an example using Ruby:
#!/usr/bin/env ruby require 'json' # Read hook input input = JSON.parse(STDIN.read) # Decide to force continuation output = { 'decision' => 'block', 'reason' => 'Please continue by fixing the error above' } # Exit code 2 = force continuation $stderr.puts JSON.generate(output) exit 2This script reads the hook input, constructs a JSON output with
decisionset toblockand areasonfor continuation, writes the JSON to STDERR, and exits with code 2. -
Install the plugin and configure the hook:
Install the plugin into your Claude Code environment and configure the stop hook to be triggered during agent execution. This step involves the specific procedures for your Claude Code setup, which may vary depending on the version and configuration.
-
Trigger the stop hook during agent execution:
Initiate an agent execution that will trigger the stop hook. This might involve a specific input or scenario that activates the hook based on its defined logic.
-
Observe that execution halts instead of continuing:
This is the moment of truth. Instead of the agent resuming execution with the instructions provided in the
reasonfield, you'll observe that execution halts, and Claude Code displays the⏺ Stop hook prevented continuationmessage. This confirms the presence of the bug.
By following these steps, you can reliably reproduce the bug and gain a deeper understanding of its manifestation.
Expected vs. Actual: A Tale of Two Flows
To further clarify the issue, let's contrast the expected flow of execution with the actual flow when the bug occurs. This comparison will highlight the point of divergence and provide a clearer picture of the problem.
Expected flow:
Agent attempts to stop
↓
Stop hook executes
↓
Hook exits with code 2
↓
Claude receives instructions via STDERR
↓
Agent resumes execution ✓
This is the ideal scenario, where the agent's attempt to stop triggers the stop hook, which executes and returns exit code 2. Claude Code correctly interprets this signal, receives the continuation instructions via STDERR, and seamlessly resumes agent execution.
Actual flow:
Agent attempts to stop
↓
Stop hook executes
↓
Hook exits with code 2
↓
"⏺ Stop hook prevented continuation"
↓
Execution halts ✗
In contrast, the actual flow deviates significantly. The agent's attempt to stop still triggers the stop hook, and the hook still exits with code 2. However, instead of resuming execution, Claude Code displays the ⏺ Stop hook prevented continuation message and halts execution. This premature termination disrupts the workflow and prevents the agent from continuing as intended.
The key difference lies in the interpretation of exit code 2. In the expected flow, it signals continuation, while in the actual flow, it leads to a halt. This discrepancy underscores the core of the bug and highlights the need for a fix.
Technical Analysis: Unraveling the Mechanism
Now, let's delve into a more technical analysis to understand why this bug is occurring. The hook correctly performs the following actions:
- Sets
decision: 'block'in JSON output: The hook accurately signals its intent to block the stoppage by setting thedecisionfield toblockin the JSON output. - Provides instructions in the
reasonfield: The hook also correctly includes the necessary continuation instructions in thereasonfield of the JSON output. - Exits with code 2: The hook appropriately exits with code 2, signaling its intention to force continuation.
- Writes to STDERR: The hook correctly writes the JSON output to STDERR, ensuring that Claude Code receives the instructions.
Despite these correct actions, Claude Code appears to treat exit code 2 differently when the hook is loaded from a plugin compared to when it's directly loaded from .claude/hooks/. This suggests that the issue lies in the handling of plugin-based hooks, rather than in the hook implementation itself.
Possible causes for this discrepancy include:
- Different code paths for plugin-based hooks: Claude Code might be using different code paths to handle hooks loaded from plugins, potentially leading to a misinterpretation of exit codes.
- Sandboxing or isolation issues: The plugin architecture might be introducing sandboxing or isolation mechanisms that interfere with the proper handling of exit codes.
- Configuration or environment differences: There might be subtle differences in the configuration or environment between plugin-based hooks and directly installed hooks that affect the interpretation of exit codes.
Further investigation is needed to pinpoint the exact cause, but the analysis points towards a problem in how Claude Code manages plugin-based hooks.
Questions for Maintainers: Seeking Clarity and Solutions
To facilitate the resolution of this bug, here are some key questions for the maintainers of Claude Code:
- Does the plugin architecture change how stop hook exit codes are interpreted? This is a crucial question to understand whether the plugin architecture intentionally or unintentionally alters the handling of exit codes.
- Are there additional requirements for hooks in plugins to signal forced continuation? If there are specific requirements for plugin-based hooks to signal continuation, they need to be clearly documented and communicated.
- Is the exit code 2 → continuation protocol documented and officially supported? Clarifying the official support for this protocol is essential for ensuring its reliability and proper implementation.
Addressing these questions will provide valuable insights into the bug and guide the development of a solution.
Impact: Disrupting Reflexive Agent Workflows
The impact of this bug is significant, particularly for reflexive agent workflows. These workflows rely on stop hooks to provide corrective feedback and continue execution, enabling agents to learn and adapt during their operation. The bug disrupts this process, forcing users to choose between:
- Plugin modularity (broken continuation): Users can leverage the benefits of plugin modularity, but at the cost of broken continuation, hindering the agent's ability to self-correct.
- Direct hook installation (working continuation, but less modular): Alternatively, users can opt for direct hook installation, which provides working continuation but sacrifices the advantages of modularity and maintainability offered by plugins.
This dilemma creates a significant trade-off, limiting the flexibility and effectiveness of Claude Code in scenarios that require reflexive agent behavior.
Environment: Bug's Habitat
To aid in the debugging process, here's the environment in which the bug was observed:
- Claude Code: Latest version
- Platform: macOS (Darwin 24.6.0)
- Hook type: Stop / SubagentStop
- Installation method: Plugin marketplace
This information provides context for the bug's occurrence and can help narrow down potential causes.
Related Evidence: A History of the Issue
Further evidence of this issue can be found in the original discussion on GitHub:
Original discussion: https://github.com/gabriel-dehan/claude_hooks/issues/11
The discussion highlights that the same hook code that worked pre-plugin-refactor now fails when packaged as a plugin. This further supports the conclusion that the issue lies in how Claude Code handles plugin-based hooks rather than the hook implementation itself.
In conclusion, the failure of stop hooks to continue when returning exit code 2 from a plugin-based architecture is a significant bug that disrupts reflexive agent workflows. Understanding the stop hook continuation protocol, the context of the plugin marketplace migration, and the technical analysis of the issue are crucial steps towards finding a solution. By addressing the questions raised and considering the impact on users, the maintainers of Claude Code can effectively resolve this bug and restore the intended functionality of stop hooks within the plugin ecosystem.
For more information on plugin architectures and their implementation, consider exploring resources from trusted sources like https://plugins.jetbrains.com/docs/intellij/plugin-architecture.html.