How Nodeterm Handles Gemini’s ToolPermission Notifications

Nodeterm maps Gemini’s ToolPermission notifications to a blocked agent state, pausing execution until the user explicitly grants approval through the UI.

When building terminal-based AI agent interfaces, handling permission prompts gracefully is critical for both security and user experience. In the eneskirca/nodeterm repository, the developers implemented a robust normalization layer that interprets Gemini CLI’s permission requests and translates them into a universal state machine. This article examines the exact mechanism Nodeterm uses to handle Gemini’s ToolPermission notifications, from hook reception to UI rendering.

Understanding Gemini’s ToolPermission Hook Events

Gemini communicates permission requirements through a specific hook event taxonomy. When the Gemini CLI requires user approval to execute a tool, it emits a hook event with hook_event_name set to "Notification" and a notification_type of "ToolPermission".

Unlike Claude’s multi-variant permission system, Gemini consolidates all permission requests into this single notification type. This design simplifies the parsing logic but requires Nodeterm to treat every instance as a blocking operation that demands immediate user attention.

Normalizing ToolPermission Events in normalize.ts

The core logic for handling these notifications resides in src/shared/agents/normalize.ts. Here, the normalizeGemini function inspects incoming hook payloads and applies a rigid state mapping that converts Gemini-specific events into Nodeterm’s standardized agent state format.

Hook Reception and Parsing

When a payload arrives from the Gemini CLI, the normalizer first checks if the hook_event_name equals "Notification". If true, it inspects the notification_type field to determine the appropriate state mapping. According to the source code at line 394, the implementation specifically filters for the exact string "ToolPermission":

// src/shared/agents/normalize.ts – Gemini normalization
if (env.hook_event_name === 'Notification') {
  // Gemini sends a single permission request type.
  if (p.notification_type === 'ToolPermission') {
    // Map to a blocked state so the UI knows the agent is awaiting approval.
    return { state: 'blocked', ...commonFields };
  }
}

This conditional branch acts as a strict gate. Only payloads matching both the hook event name and notification type trigger the blocked state transition.

State Mapping to "blocked"

Upon detecting a ToolPermission notification, the function returns a NormalizedAgentEvent object with its state field explicitly set to "blocked". This normalized state serves as a universal signal across Nodeterm’s architecture, indicating that the agent cannot proceed without external intervention.

The "blocked" state abstracts away Gemini-specific implementation details, allowing the UI layer to handle permission prompts consistently regardless of which underlying AI agent generated the request.

UI Representation and User Interaction

Once the normalized event propagates to the renderer, Nodeterm’s terminal interface reacts by displaying a visual blocker. The component responsible for this behavior resides in src/renderer/terminal/agent-restart.ts.

When the renderer detects an agent state of "blocked", it renders a red badge on the Gemini node and pauses execution:

// src/renderer/terminal/agent-restart.ts – UI reacts to blocked state
if (agentState === 'blocked') {
  // Show a permission dialog; on user approval, send the appropriate command.
  showPermissionPrompt(nodeId, () => sendApprovalCommand(nodeId));
}

The showPermissionPrompt function displays the specific tool permission request to the user. When the user clicks the approval prompt, Nodeterm dispatches the appropriate command back to the Gemini CLI, which clears the blocked state and resumes agent execution.

Key Implementation Files

Several files work in concert to ensure reliable handling of Gemini’s permission system:

Summary

  • Nodeterm intercepts Gemini’s ToolPermission notifications in src/shared/agents/normalize.ts by checking for hook_event_name === 'Notification' and notification_type === 'ToolPermission'.
  • The normalizer converts these events into a universal "blocked" state via the normalizeGemini function.
  • The renderer displays a red blocked badge and pauses execution until the user explicitly approves the tool usage through the UI.
  • This architecture mirrors Claude’s permission handling but is deliberately simplified to accommodate Gemini’s single notification type.

Frequently Asked Questions

How does Nodeterm distinguish between different types of Gemini notifications?

Nodeterm checks the notification_type field within the hook payload. Currently, Nodeterm only implements specific handling for the "ToolPermission" value, treating all other notification types as non-blocking events. This is implemented in the conditional logic at line 394 of src/shared/agents/normalize.ts.

What happens if the user denies a ToolPermission request?

When the user denies permission through the UI prompt in src/renderer/terminal/agent-restart.ts, Nodeterm sends a denial command back to the Gemini CLI. The agent remains in the "blocked" state until the user either approves the request or terminates the session, preventing unauthorized tool execution.

Is the blocked state unique to Gemini, or do other agents use it?

The "blocked" state is a universal abstraction used across all supported agents in Nodeterm. While the repository implements specific parsing logic for Gemini’s ToolPermission notifications, the resulting state matches Claude’s permission prompt handling and other agents, ensuring consistent UI behavior regardless of the underlying AI provider.

Where can I find the test coverage for ToolPermission handling?

The test suite in src/renderer/terminal/agent-restart.test.ts validates that Gemini nodes correctly transition to the blocked state when receiving ToolPermission notifications. These tests verify both the state mapping logic and the UI components responsible for rendering permission prompts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →