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

> Discover how normalize.ts in Nodeterm unifies AI agent hook events from Claude, Gemini, Copilot, and more into a single schema for consistent state management across AI agents.

- Repository: [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm)
- Tags: internals
- Published: 2026-08-23

---

**The [`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/eneskirca/nodeterm/blob/main/agent-hook-listener.ts) remain agnostic about the specific AI backend, consuming only the standardized `NormalizedAgentEvent` interface.

## Related Files and Testing

The normalization layer relies on several supporting files:

- **[`src/shared/agents/config.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/config.ts)** – Defines the `AgentId` type and capability lists used by the normalizers to determine which features each agent supports.
- **[`src/shared/agents/normalize.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.test.ts)** – Comprehensive unit tests verifying that each agent's normalizer correctly maps raw payloads to the expected `NormalizedAgentEvent` structure.
- **[`src/shared/agents/normalize.grok.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.grok.test.ts)** – Specific test coverage for Grok's unique notification filtering logic.
- **[`src/shared/agents/agent-hook-listener.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/agent-hook-listener.ts)** – Consumes normalized events and updates the UI state accordingly.

## Summary

- **[`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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`](https://github.com/eneskirca/nodeterm/blob/main/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.