Files
claude-hud/docs/adr/003-shell-vs-typescript.md
Jarrod Watts e39933fac2 docs: Add architecture decision records for v2
ADR 001: State Management - Custom hooks + useReducer
- Extract state logic from app.tsx into domain-specific hooks
- useReducer for predictable state transitions
- Each hook independently testable

ADR 002: Data Flow - Event-driven with minimal polling
- Primary: Hook events via FIFO (real-time)
- Secondary: Single consolidated poll (git/mcp only)
- Eliminate redundant polling sources that cause flickering

ADR 003: Shell vs TypeScript - Minimal shell, logic in TS
- Hooks must be shell scripts (Claude Code requirement)
- Keep shell scripts minimal (extract data, write FIFO)
- Complex logic moves to testable TypeScript

ADR 004: Session Handling - Track session ID, graceful reset
- Session ID in all events for change detection
- Reset state on session change (/new, /exit, /resume)
- Exponential backoff reconnection strategy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:10:26 +11:00

5.4 KiB

ADR 003: Shell Scripts vs TypeScript

Status

Accepted

Context

Current implementation uses shell scripts for:

  1. session-start.sh - Spawns HUD, creates FIFO, manages split pane
  2. capture-event.sh - Processes hook events, writes to FIFO
  3. cleanup.sh - Kills process, removes FIFO
  4. verify-install.sh - Checks installation

Questions:

  • Should we keep shell scripts or rewrite in TypeScript/Node?
  • What logic belongs where?

Decision

Keep shell scripts for hooks (required), but make them minimal. Move logic to TypeScript.

Why Shell Scripts Are Required for Hooks

Claude Code hooks must be shell scripts. From the plugin spec:

  • command field specifies the shell command to run
  • Environment variables passed to the script
  • JSON input via stdin, output via stdout

There is no option to use Node.js directly for hooks.

The Minimal Shell Script Pattern

Shell scripts should do exactly two things:

  1. Parse/extract essential data from hook input
  2. Write to FIFO or perform one simple action
#!/bin/bash
# capture-event.sh - MINIMAL

set -uo pipefail

# Extract just what we need
event_name="$HOOK_EVENT_NAME"
session_id="$SESSION_ID"
timestamp=$(date +%s)

# Read stdin (JSON from Claude Code)
input=$(cat)

# Write to FIFO (non-blocking)
if [[ -p "$FIFO_PATH" ]]; then
  echo "{\"event\":\"$event_name\",\"session\":\"$session_id\",\"ts\":$timestamp,\"data\":$input}" > "$FIFO_PATH" &
fi

What Moves to TypeScript

Currently in Shell Move to TypeScript? Reason
FIFO creation Yes Better error handling, cross-platform
Split pane creation Keep in shell AppleScript/osascript required
Event processing Yes Complex logic should be testable
JSON parsing Minimal in shell jq for extraction, full parsing in TS
Process management Keep in shell PID files, signals are shell-native
Data enrichment Yes TypeScript has better tooling

New Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Shell Scripts (Hooks)                     │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ capture-event.sh - Extract data, write raw to FIFO   │  │
│  │ session-start.sh - Create FIFO, spawn HUD process    │  │
│  │ cleanup.sh - Signal process, remove FIFO             │  │
│  └──────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                    TypeScript (TUI)                          │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ EventReader - Read FIFO, parse JSON, validate        │  │
│  │ EventProcessor - Enrich events, compute derived data │  │
│  │ State Hooks - Update UI state from processed events  │  │
│  │ Components - Render UI                               │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

TypeScript Event Processing

// lib/event-processor.ts
interface RawEvent {
  event: string;
  session: string;
  ts: number;
  data: unknown;
}

interface ProcessedEvent {
  type: 'tool_start' | 'tool_end' | 'agent_stop' | 'session_change' | ...;
  timestamp: Date;
  sessionId: string;
  payload: ToolEvent | AgentEvent | SessionEvent | ...;
}

function processEvent(raw: RawEvent): ProcessedEvent {
  // Validation, type narrowing, enrichment
  // All testable in TypeScript
}

Consequences

Positive

  • Testability: TypeScript logic is fully unit testable
  • Type safety: Catch errors at compile time
  • Maintainability: Complex logic in familiar language
  • Cross-platform: TypeScript works everywhere Node runs
  • Debugging: Better stack traces, source maps

Negative

  • Two languages: Shell for hooks, TypeScript for logic
  • Coordination: Shell must output format TypeScript expects
  • Still need shell knowledge: Hooks can't be avoided

Shell Script Guidelines

  1. No complex logic - If you need if/then/else more than once, reconsider
  2. No error recovery - Let it fail fast, TypeScript handles gracefully
  3. Non-blocking writes - Always use & for FIFO writes
  4. Timeouts for commands - Use timeout 1s command for any external call
  5. Minimal dependencies - Only rely on POSIX + jq

Implementation Notes

  1. Simplify capture-event.sh to raw data extraction only
  2. Create lib/event-processor.ts for event enrichment
  3. Add comprehensive tests for event processing
  4. Keep session-start.sh for platform-specific terminal handling