Purpose of normalize.ts in Nodeterm: Unifying AI Agent Hook Events

The src/shared/agents/normalize.ts file in the Nodeterm repository serves as the central architecture layer that converts disparate raw hook payloads from Claude, Codex, Gemini, Copilot, Opencode, and Grok into a unified NormalizedAgentEvent schema, enabling consistent state management across all supported AI agents.

Managing multiple AI agents within a single terminal application requires handling incompatible webhook formats and event naming conventions. The normalize.ts module eliminates this fragmentation by exposing a single entry point—normalizeFor—that translates proprietary agent payloads into a standardized internal representation consumed by the UI and status store.

The Problem: Disparate Agent Hook Formats

Each AI agent integrated into Nodeterm emits hooks with unique field names, event types, and payload structures. Claude might emit UserPromptSubmit events, while Grok uses notification with different casing and metadata. Without normalization, every consumer in the codebase would need agent-specific parsing logic.

The normalize.ts file solves this by defining a unified event model through the NormalizedAgentEvent interface (lines 7-83) and the RawHookEnvelope helper (lines 85-91). This abstraction allows downstream components to reason about agent activity using consistent fields like sessionId, state, and task, regardless of which AI backend generated the event.

Per-Agent Normalization Logic

The module contains dedicated normalization functions for each supported agent, mapping proprietary events to high-level states (working, waiting, blocked, done).

Claude Normalization

The normalizeClaude function (lines 42-88) handles events like UserPromptSubmit, Stop, and PermissionRequest. It extracts the session_id from the raw payload and maps interaction states to the standardized schema while calculating durationMs for performance tracking.

// Example: Normalizing a Claude hook envelope
import { normalizeFor } from '@/shared/agents/normalize';

const rawClaude = {
  nodeId: 'nt-123',
  agentId: 'claude',
  payload: {
    hook_event_name: 'UserPromptSubmit',
    session_id: 's1',
    prompt: 'Explain the code',
  },
};

const event = normalizeFor('claude', rawClaude);
// event => {
//   nodeId: 'nt-123',
//   agentId: 'claude',
//   sessionId: 's1',
//   kind: 'state',
//   state: 'working',
//   task: 'Explain the code',
//   newTurn: true,
// }

Codex, Gemini, and Copilot

Similarly, normalizeCodex (lines 101-136), normalizeGemini (lines 188-229), and normalizeCopilot (lines 254-302) each implement agent-specific parsing logic. These functions handle tool use confirmations, permission requests, and completion events while normalizing metadata fields such as pendingId and toolUseId into the shared format.

Opencode and Grok Edge Cases

The normalizeOpencode function (lines 322-371) manages async sub-agent launches through the isAsyncSubagentLaunch helper (lines 138-140). Meanwhile, normalizeGrok (lines 384-439) utilizes grokRawFields (lines 664-679) to extract common fields from Grok's notification-heavy payload structure.

// Example: Handling a Grok notification
import { normalizeFor } from '@/shared/agents/normalize';

const rawGrok = {
  nodeId: 'nt-456',
  agentId: 'grok',
  payload: {
    hookEventName: 'notification',
    notification_type: 'permission_prompt',
    message: 'Tool permission requested',
    level: 'info',
  },
};

const event = normalizeFor('grok', rawGrok);
// event => null (notification suppressed per Grok rules)

The Dispatcher: normalizeFor Entry Point

Rather than calling individual normalizers directly, the rest of the codebase uses the normalizeFor function (lines 711-718) as a dispatcher. This function accepts an agentId and RawHookEnvelope, then routes to the appropriate agent-specific normalizer.

// Example: Using the dispatcher for any agent
function handleHook(env: RawHookEnvelope) {
  const normalized = normalizeFor(env.agentId, env);
  if (!normalized) return;

  // Store the normalized event in the agent‑status store
  // storeAgentEvent(normalized);
}

This design pattern ensures that components like agent-hook-listener.ts remain agnostic about the specific AI backend, consuming only the standardized NormalizedAgentEvent interface.

The normalization layer relies on several supporting files:

Summary

  • src/shared/agents/normalize.ts acts as the translation layer between proprietary AI agent hooks and Nodeterm's internal event system.
  • Six dedicated normalizers—normalizeClaude, normalizeCodex, normalizeGemini, normalizeCopilot, normalizeOpencode, and normalizeGrok—handle agent-specific parsing logic.
  • normalizeFor provides a single entry point that routes raw hooks to the correct normalizer based on agentId.
  • The NormalizedAgentEvent interface unifies disparate payload formats into consistent fields including sessionId, state, task, and durationMs.
  • Helper functions like isAsyncSubagentLaunch and grokRawFields manage edge cases for specific agent behaviors.

Frequently Asked Questions

What is the difference between RawHookEnvelope and NormalizedAgentEvent?

The RawHookEnvelope interface (lines 85-91) represents the incoming webhook structure containing nodeId, agentId, and an opaque payload specific to each AI provider. NormalizedAgentEvent (lines 7-83) is the standardized output format containing unified fields like sessionId, state, and metadata that the rest of the application consumes.

How does normalize.ts handle unsupported agent IDs?

The normalizeFor dispatcher (lines 711-718) selects normalizers based on the agentId field using a switch statement or mapping object. If an unknown agent ID is passed, the function returns null or undefined, preventing the application from crashing and allowing the hook listener to silently drop unsupported events.

Why does the Grok normalizer return null for certain notifications?

According to the source code in normalizeGrok (lines 384-439), certain Grok notification types—such as informational messages or permission prompts—are intentionally suppressed and return null. This filtering prevents noise in the agent status store, ensuring only actionable state changes (working, blocked, etc.) propagate through the system.

Which AI agents are supported by the normalize.ts module?

As implemented in eneskirca/nodeterm, the module supports six AI agents: Claude, Codex, Gemini, Copilot, Opencode, and Grok. Each has a dedicated normalization function that handles its specific hook event naming conventions and payload structures.

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 →