claude-hud

Architecture

How Claude HUD works under the hood — from data ingestion to terminal rendering.

Prerequisites: Familiarity with Claude HUD overview and HUD elements.


High-Level Data Flow

Claude Code invokes the HUD plugin as a child process roughly every 300 ms. Each invocation is a fresh Node.js process that reads data, renders output, and exits. There is no long-running daemon or persistent state in memory.

The pipeline follows five stages:

  1. Stdin ingestion — Claude Code pipes a JSON payload containing model info, context window metrics, and a transcript file path.
  2. Transcript parsing — The plugin reads the session transcript (a JSONL file on disk) to extract tool calls, agent tasks, and todo items.
  3. Config and environment collectionCLAUDE.md files, MCP servers, hooks, rules, git status, and OAuth usage data are gathered from the filesystem and APIs.
  4. Rendering — Each HUD element is rendered into ANSI-colored text lines.
  5. Output — Lines are printed to stdout, where Claude Code displays them as the statusline.

Data flow diagram showing the five-stage pipeline: stdin JSON from Claude Code flows into the plugin, which parses the transcript JSONL file, collects config and environment data, renders HUD elements, and outputs ANSI lines to stdout


Data Sources

Claude HUD combines three categories of input data. Understanding these sources is essential for debugging unexpected display values or extending the plugin with new indicators.

Stdin JSON

Claude Code writes a JSON object to the plugin’s stdin on every invocation. This is the primary source of accurate, real-time session data.

interface StdinData {
  transcript_path?: string;          // Path to session transcript JSONL
  cwd?: string;                      // Current working directory
  model?: {
    id?: string;                     // Raw model ID (e.g., "anthropic.claude-sonnet-4-20250514")
    display_name?: string;           // Human-readable name (e.g., "Sonnet")
  };
  context_window?: {
    context_window_size?: number;    // Maximum context size in tokens
    current_usage?: {
      input_tokens?: number;
      cache_creation_input_tokens?: number;
      cache_read_input_tokens?: number;
    };
    used_percentage?: number;        // Native percentage (v2.1.6+)
  };
}

The used_percentage field was introduced in Claude Code v2.1.6. When available, the HUD uses it directly instead of computing tokens manually — this ensures the displayed percentage matches what Claude Code reports in /context.

Transcript JSONL

The transcript file is a newline-delimited JSON log of every message in the current session. The HUD parses it line by line using a streaming readline interface, extracting:

Block Type Data Extracted HUD Element
tool_use Tool name, target file, start time Tools line
tool_result Completion status, error flag Tools line (status update)
Task tool_use Agent type, model, description Agents line
TodoWrite tool_use Full todo list Todos line
TaskCreate / TaskUpdate Individual task status changes Todos line

A tool is considered “running” when a tool_use block has no matching tool_result block (matched by ID). This is how the HUD shows spinner indicators for in-progress operations.

Configuration Files

The plugin scans multiple locations to count configuration items displayed in the environment line:

Source What It Counts
~/.claude/CLAUDE.md CLAUDE.md files (user scope)
{cwd}/CLAUDE.md, {cwd}/CLAUDE.local.md CLAUDE.md files (project scope)
~/.claude/settings.json MCP servers, hooks
{cwd}/.mcp.json Project MCP servers
{cwd}/.claude/settings.json Project MCP servers, hooks
~/.claude/rules/*.md Rule files (recursive)

Disabled MCP servers (listed in disabledMcpServers or disabledMcpjsonServers) are subtracted from the count. The plugin also avoids double-counting when the project .claude directory overlaps with the user-scope directory.


Entry Point and Orchestration

The main() function in src/index.ts orchestrates the entire pipeline. It uses dependency injection for testability — every external dependency is passed through a MainDeps object that can be overridden in tests.

export async function main(overrides: Partial<MainDeps> = {}): Promise<void> {
  const deps: MainDeps = {
    readStdin, parseTranscript, countConfigs,
    getGitStatus, getUsage, loadConfig,
    parseExtraCmdArg, runExtraCmd, render,
    now: () => Date.now(),
    log: console.log,
    ...overrides,
  };

  const stdin = await deps.readStdin();
  if (!stdin) {
    deps.log('[claude-hud] Initializing...');
    return;
  }
  // ... orchestration continues
}

When no stdin data is available (e.g., during first invocation or when running interactively), the plugin outputs an initialization message and exits gracefully.

The orchestration sequence:

  1. Read and parse stdin JSON
  2. Parse the transcript file from the path in stdin
  3. Count configuration items from the filesystem
  4. Load user configuration (config.json)
  5. Optionally fetch git status and OAuth usage data
  6. Build a RenderContext object combining all collected data
  7. Pass the context to the render pipeline

Rendering Pipeline

The render system is organized as a set of independent line renderers coordinated by src/render/index.ts. Each renderer receives the full RenderContext and returns either a formatted string or null (when there is nothing to display).

Layout Modes

The plugin supports two layout modes controlled by the lineLayout configuration:

Element Renderers

Element Renderer File Always Shown
Project src/render/lines/project-line.ts Yes
Context src/render/lines/identity-line.ts Yes
Usage src/render/lines/usage-line.ts When data available
Environment src/render/lines/environment-line.ts When thresholds met
Tools src/render/tools-line.ts When tools used
Agents src/render/agents-line.ts When agents active
Todos src/render/todos-line.ts When todos exist

Terminal Width Handling

The render pipeline detects terminal width from process.stdout.columns (falling back to the COLUMNS environment variable). Lines that exceed the terminal width are wrapped at separator boundaries (| or ) rather than mid-word. If a single segment still exceeds the width, it is truncated with an ellipsis.

The width calculation accounts for ANSI escape sequences (which occupy zero visual width), emoji glyphs (which occupy two columns), and Unicode combining characters.

Color System

Colors are applied through ANSI escape codes via helper functions in src/render/colors.ts. Context usage thresholds determine the bar color:

Range Color Meaning
0–70% Green Healthy — plenty of context remaining
70–85% Yellow Warning — consider wrapping up or compacting
85%+ Red Critical — shows token breakdown for diagnosis

All color names are configurable through the colors field in config.json, supporting: red, green, yellow, magenta, cyan, brightBlue, and brightMagenta.


Usage API Integration

The HUD optionally fetches plan usage data from the Anthropic OAuth API (api.anthropic.com/api/oauth/usage). Because the plugin runs as a new process every ~300 ms, a file-based caching layer prevents excessive API calls.

Cache Strategy

~/.claude/plugins/claude-hud/.usage-cache.json
Scenario Cache TTL
Successful response 60 seconds (configurable)
Failed request 15 seconds (configurable)
Rate limited (429) Exponential backoff: 60s, 120s, 240s, up to 5 min

A file-based lock (.usage-cache.lock) prevents concurrent API calls when multiple HUD instances run simultaneously. If the lock is held, the process waits up to 2 seconds for fresh cache data before falling back to stale data.

Credential Resolution

On macOS, the plugin reads OAuth tokens from the system Keychain using /usr/bin/security. On other platforms (or when Keychain is unavailable), it falls back to ~/.claude/.credentials.json. The credential lookup includes:

  1. Try macOS Keychain with profile-specific service name
  2. Try macOS Keychain with legacy service name
  3. Fall back to file-based credentials
  4. If all fail, usage display is silently disabled

A backoff mechanism prevents repeated Keychain prompts — after a failure, the plugin skips Keychain access for 60 seconds.


File Structure

src/
├── index.ts              # Entry point, orchestrates the pipeline
├── stdin.ts              # Reads and parses stdin JSON
├── transcript.ts         # Parses transcript JSONL for tools/agents/todos
├── config.ts             # Loads and validates user configuration
├── config-reader.ts      # Counts CLAUDE.md, MCP, hooks, rules
├── git.ts                # Git branch, dirty state, ahead/behind
├── usage-api.ts          # OAuth usage API with file-based caching
├── types.ts              # Shared TypeScript interfaces
├── constants.ts          # Shared constants
├── render/
│   ├── index.ts          # Render coordinator (layout, wrapping, output)
│   ├── session-line.ts   # Compact mode session line
│   ├── tools-line.ts     # Tool activity with spinners
│   ├── agents-line.ts    # Agent status with elapsed time
│   ├── todos-line.ts     # Todo progress
│   ├── colors.ts         # ANSI color helpers
│   └── lines/
│       ├── identity-line.ts     # Model and context bar
│       ├── project-line.ts      # Project name and git info
│       ├── usage-line.ts        # Plan usage bars
│       └── environment-line.ts  # Config counts

Extension Points

Claude HUD is designed to be extended with new data sources and display elements.

Adding a New HUD Line

  1. Create a new renderer in src/render/ that exports a function accepting RenderContext and returning string | null.
  2. Add any new data fields to the RenderContext interface in src/types.ts.
  3. If the data comes from the transcript, add extraction logic in src/transcript.ts.
  4. Register the renderer in src/render/index.ts.
  5. Add the new element to the HudElement type in src/config.ts and include it in DEFAULT_ELEMENT_ORDER.

Adding a New Data Source

  1. Create a new module in src/ with an async function that returns the data.
  2. Add the function to the MainDeps interface in src/index.ts.
  3. Call it from main() and add the result to RenderContext.
  4. Reference the data in the appropriate renderer.

Modifying Thresholds

Context color thresholds are defined in the session line renderer (src/render/session-line.ts). The percentage checks that determine color coding can be adjusted there. Usage display thresholds are configurable via config.json without code changes.