Skip to content

Conversation

@SandyTao520
Copy link
Collaborator

@SandyTao520 SandyTao520 commented Dec 23, 2025

TLDR

Implemented a strict 'Two-Stage Validation Pattern' for hook responses (BeforeAgent, AfterAgent, BeforeTool, AfterTool, BeforeModel). This distinguishes between Stage 1: Policy/Security Decisions (decision: 'block') and Stage 2: Workflow Lifecycle Decisions (continue: false).

Dive Deeper

Previously, the distinction between blocking an action for security reasons and stopping a workflow for logic reasons was ambiguous. This PR unifies the behavior:

  • Stage 1 (Decision): Checks decision: 'block'. If blocked, it returns a PERMISSION_DENIED error (for tools) or a Policy Block error (for agents). This signals to the LLM that the action was forbidden.
  • Stage 2 (Continue): Checks continue: false. If false, it gracefully terminates the execution with an EXECUTION_FAILED (or 'Execution stopped') status, often with a 'stop reason'. This is for natural workflow endpoints.
  • BeforeModel: Updated to support 'synthetic responses' for blocked calls, allowing the hook to provide a canned refusal message instead of a network call.
  • Tests: Added comprehensive tests for all stages in client.test.ts, geminiChat.test.ts, and coreToolHookTriggers.test.ts.

Reviewer Test Plan

  1. Configure a BeforeTool hook that returns decision: 'block', reason: 'Unsafe'. Verify that the tool call fails with PERMISSION_DENIED and the model receives 'Policy Block: Unsafe'.
  2. Configure a BeforeTool hook that returns continue: false, stopReason: 'Done'. Verify that the tool call fails with EXECUTION_FAILED and the model receives 'Execution stopped: Done'.
  3. Run npm test packages/core/src/core/client.test.ts
  4. Run npm test packages/core/src/core/geminiChat.test.ts
  5. Run npm test packages/core/src/core/coreToolHookTriggers.test.ts

Testing Matrix

🍏 🪟 🐧
npm run
npx
Docker
Podman - -
Seatbelt - -

Linked issues / bugs

#14718

@SandyTao520 SandyTao520 requested a review from a team as a code owner December 23, 2025 23:27
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @SandyTao520, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant refinement to the system's hook validation process by establishing a clear two-stage pattern. This change aims to provide more precise control and feedback mechanisms for both security-related policy enforcement and general workflow management. By distinguishing between blocking an action due to policy and stopping an execution for logical reasons, the system can now communicate more effectively with the LLM and handle various intervention scenarios with greater clarity and robustness.

Highlights

  • Two-Stage Hook Validation: Implemented a strict 'Two-Stage Validation Pattern' for hook responses across BeforeAgent, AfterAgent, BeforeTool, AfterTool, and BeforeModel hooks. This clarifies the distinction between security/policy decisions and workflow lifecycle decisions.
  • Policy/Security Decisions (Stage 1): When a hook returns decision: 'block', it now triggers a Stage 1 block, resulting in a PERMISSION_DENIED error for tools or a 'Policy Block' error for agents, signaling a forbidden action to the LLM.
  • Workflow Lifecycle Decisions (Stage 2): When a hook returns continue: false, it triggers a Stage 2 stop, gracefully terminating execution with an EXECUTION_FAILED status or 'Execution stopped' message, indicating a natural workflow endpoint.
  • BeforeModel Synthetic Responses: The BeforeModel hook has been updated to support 'synthetic responses' for blocked calls, allowing the hook to provide a canned refusal message instead of proceeding with a network call.
  • Enhanced Error Handling: Error messages and types have been refined to clearly indicate whether an action was blocked due to a policy violation (Policy Block, PERMISSION_DENIED) or a workflow termination (Execution stopped, EXECUTION_FAILED).
  • Comprehensive Testing: New and updated tests have been added to client.test.ts, geminiChat.test.ts, and a new file coreToolHookTriggers.test.ts to ensure the correct behavior of the two-stage validation across various hook types.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a two-stage validation pattern for hooks, separating security/policy decisions from workflow control. However, it critically introduces several high-severity prompt injection vulnerabilities: hook output is directly injected into LLM content, enabling malicious hooks to control LLM behavior and bypass security policies. Furthermore, the AfterTool hook implementation critically omits the policy-blocking validation stage, which could silently ignore security policies after tool execution.

Comment on lines 329 to 338
if (afterOutput?.shouldStopExecution()) {
const reason = afterOutput.getEffectiveReason();
return {
llmContent: `Agent execution stopped by hook: ${reason}`,
returnDisplay: `Agent execution stopped by hook: ${reason}`,
llmContent: `Execution stopped: ${reason}`,
returnDisplay: `Execution stopped: ${reason}`,
error: {
type: ToolErrorType.EXECUTION_FAILED,
message: `Agent execution stopped: ${reason}`,
message: `Execution stopped: ${reason}`,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

This AfterTool hook implementation has two critical issues. Firstly, the shouldStopExecution path is vulnerable to prompt injection because the reason from getEffectiveReason() is directly injected into llmContent, allowing malicious hooks to control the LLM. Secondly, it critically omits the Stage 1 validation for policy/security decisions (isBlockingDecision). This means a decision: 'block' from an AfterTool hook would be silently ignored, undermining the two-stage validation and potentially bypassing security policies.

Comment on lines +653 to +661
if (hookOutput?.isBlockingDecision()) {
const blockReason = `Policy Violation: ${hookOutput.getEffectiveReason()}`;
const continueRequest = [{ text: blockReason }];
yield* this.sendMessageStream(
continueRequest,
signal,
prompt_id,
boundedTurns - 1,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

A prompt injection vulnerability exists in the AfterAgent hook processing. The reason provided by a hook, which can be controlled by a third-party extension, is directly concatenated into a new prompt sent to the LLM via sendMessageStream. A malicious hook could provide a crafted reason to manipulate the LLM, potentially leading to policy bypasses or unintended behavior. The getEffectiveReason() output is not sanitized or treated as untrusted data before being used as part of the next prompt.

Comment on lines 273 to 281
if (blockingError?.blocked) {
return {
llmContent: `Tool execution blocked: ${blockingError.reason}`,
returnDisplay: `Tool execution blocked: ${blockingError.reason}`,
llmContent: `Policy Block: ${blockingError.reason}`,
returnDisplay: `Policy Block: ${blockingError.reason}`,
error: {
type: ToolErrorType.EXECUTION_FAILED,
type: ToolErrorType.PERMISSION_DENIED,
message: blockingError.reason,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

A prompt injection vulnerability exists in the BeforeTool hook processing. The blockingError.reason from a hook, which can be controlled by a third-party, is directly injected into the llmContent of the tool result. This content is then sent to the LLM, allowing a malicious hook to manipulate the LLM's behavior.

Comment on lines 285 to 294
if (beforeOutput?.shouldStopExecution()) {
const reason = beforeOutput.getEffectiveReason();
return {
llmContent: `Agent execution stopped by hook: ${reason}`,
returnDisplay: `Agent execution stopped by hook: ${reason}`,
llmContent: `Execution stopped: ${reason}`,
returnDisplay: `Execution stopped: ${reason}`,
error: {
type: ToolErrorType.EXECUTION_FAILED,
message: `Agent execution stopped: ${reason}`,
message: `Execution stopped: ${reason}`,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

Similar to the blocking case, the shouldStopExecution path in the BeforeTool hook is also vulnerable to prompt injection. The reason from getEffectiveReason() is injected into llmContent and sent to the LLM, creating a vector for malicious prompt injection.

@github-actions
Copy link

Size Change: +1.04 kB (0%)

Total Size: 22 MB

Filename Size Change
./bundle/gemini.js 22 MB +1.04 kB (0%)
ℹ️ View Unchanged
Filename Size
./bundle/sandbox-macos-permissive-closed.sb 1.03 kB
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-closed.sb 3.29 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB

compressed-size-action

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.

1 participant