# claude-hud Administrator & Developer Guide

`claude-hud` is a small (72 files, ~270k characters), pure-TypeScript Claude
Code plugin with **zero runtime dependencies**. It is built into a single
ESM bundle (`dist/index.js`) by the TypeScript compiler, ships through the
Claude Code plugin marketplace, and runs as the statusline subprocess on
every Claude Code prompt. There is no service, no API, no daemon, and no
network call. This guide covers the architecture, every config knob, the
deployment shape, the operational characteristics, performance behaviour,
the security profile (transcript exposure is the headline concern), the
code organisation for contributors, and the integration points (Claude Code
stdin schema, git, OS).

It is the companion to the User Guide; usage / recipes / FAQ live there.

## 1. Architecture Overview

`claude-hud` is a synchronous-feeling but internally async pipeline that
runs once per Claude Code prompt and exits. The driver is `main()` in
`src/index.ts` (the full file is ~155 lines). Everything else is a leaf
function or a small module with a single responsibility.

### High-level data flow

```text
                 ┌──────────────────┐
Claude Code  ──► │ stdin (JSON 1x)  │
                 └────────┬─────────┘
                          │
                          ▼
              ┌─────────────────────────┐
              │ src/index.ts: main()    │
              │  1. readStdin()         │
              │  2. parseTranscript()   │
              │  3. countConfigs()      │
              │  4. loadConfig()        │
              │  5. getGitStatus()      │
              │  6. getUsageFromStdin() │
              │  7. parseExtraCmdArg()  │
              │  8. runExtraCmd()       │
              │  9. getClaudeCodeVer()  │
              │ 10. getMemoryUsage()    │
              └────────┬────────────────┘
                       │ RenderContext
                       ▼
            ┌────────────────────────┐
            │ src/render/index.ts    │
            │  render(ctx)           │
            │   ├── renderExpanded() │
            │   │     - project      │
            │   │     - context      │
            │   │     - usage        │
            │   │     - memory       │
            │   │     - environment  │
            │   │     - tools        │
            │   │     - agents       │
            │   │     - todos        │
            │   ├── renderCompact()  │
            │   │     - sessionLine  │
            │   ├── ANSI-aware wrap  │
            │   └── console.log()×N  │
            └────────────────────────┘
```

The pipeline is **fully testable** through dependency injection — `main()`
accepts a `Partial<MainDeps>` overrides bag, and every leaf function the
driver calls is also in `MainDeps`. The `tests/` directory exercises this
extensively (15 test files, including `tests/index.test.js` which mocks
`readStdin`, `parseTranscript`, etc.).

### Component inventory

| Component | Path | Role |
|---|---|---|
| Driver | `src/index.ts` | `main()` orchestrates all leaf calls into `RenderContext`. Also exports `formatSessionDuration()`. |
| Stdin reader | `src/stdin.ts` | Drains JSON from stdin with a 250ms first-byte timeout, 30ms idle timeout, 256KB max. Also: `getTotalTokens`, `getContextPercent`, `getBufferedPercent`, `getModelName`, `formatModelName`, `getProviderLabel`, `getUsageFromStdin`. |
| Transcript parser | `src/transcript.ts` | Streams the JSONL transcript line-by-line, builds tool/agent/todo state, deduplicates via maps, keeps the last 20 tools / 10 agents. Cached by mtime+size at `transcript-cache/<sha256>.json`. |
| Config loader | `src/config.ts` | Defines `HudConfig`, `DEFAULT_CONFIG`, `mergeConfig()`. Validates every field; clamps numbers; migrates legacy `layout` strings. |
| Config-counts probe | `src/config-reader.ts` | `countConfigs()` — counts CLAUDE.md / rules / MCPs / hooks across user and project scopes with deduplication. mtime-keyed cache at `config-cache/<sha256>.json`. |
| Plugin-dir resolver | `src/claude-config-dir.ts` | `getClaudeConfigDir()`, `getClaudeConfigJsonPath()`, `getHudPluginDir()` — single source of truth for `~/.claude/` resolution and `CLAUDE_CONFIG_DIR` honouring. |
| Git probe | `src/git.ts` | `getGitStatus()` — fork-execs `git rev-parse`, `git status --porcelain`, `git diff --numstat HEAD`, `git rev-list --left-right --count`, `git remote get-url origin`. 1–2s timeouts. |
| Cost calculator | `src/cost.ts` | `resolveSessionCost()` — prefers native `cost.total_cost_usd`, falls back to a regex-table model match (Opus 4 / Sonnet 4 / Sonnet 3.7 / Sonnet 3.5 / Haiku 3.5) and per-token estimate. Bedrock suppresses cost. |
| Memory probe | `src/memory.ts` | `getMemoryUsage()` — `/usr/bin/vm_stat` on macOS (`parseVmStat`), `os.totalmem`/`os.freemem` elsewhere. |
| Speed tracker | `src/speed-tracker.ts` | `getOutputSpeed()` — file-cached output-token counter with a 2-second sliding window. |
| Version probe | `src/version.ts` | `getClaudeCodeVersion()` — resolves `claude` binary on PATH, runs `--version` (or `cmd.exe /c` on Windows .cmd/.bat), caches result keyed by binary path + mtime. |
| Extra-cmd | `src/extra-cmd.ts` | `parseExtraCmdArg()` + `runExtraCmd()` — sanitised user-provided shell command that returns `{label}`. |
| Constants | `src/constants.ts` | `AUTOCOMPACT_BUFFER_PERCENT = 0.165`. |
| Debug | `src/debug.ts` | `createDebug(namespace)` — stderr-only, gated on `DEBUG` env var. |
| i18n | `src/i18n/` (4 files) | `setLanguage`, `getLanguage`, `t(key)`. `en` and `zh` locales; English fallback for missing keys. |
| Renderer (orchestrator) | `src/render/index.ts` | `render(ctx)` — dispatches on `lineLayout`, runs ANSI-aware grapheme-aware width calculation and line wrapping. |
| Header lines | `src/render/lines/*` (8 files) | `renderProjectLine`, `renderIdentityLine` (context), `renderUsageLine`, `renderMemoryLine`, `renderEnvironmentLine`, `renderSessionTokensLine`, `renderGitFilesLine`, `renderCostEstimate`. |
| Compact session line | `src/render/session-line.ts` | The kitchen-sink single-line renderer for `lineLayout: "compact"`. |
| Activity lines | `src/render/{tools,agents,todos}-line.ts` | Three small renderers, each ≤80 lines. |
| Colors | `src/render/colors.ts` | ANSI escape constants, hex-to-truecolor, named-preset table, threshold-aware colour pickers (`getContextColor`, `getQuotaColor`), bar generators (`coloredBar`, `quotaBar`). |
| Terminal width | `src/utils/terminal.ts` | `UNKNOWN_TERMINAL_WIDTH = 40`, `getAdaptiveBarWidth()`. |
| Tests | `tests/*.test.js` (14 files) | `node --test` against the compiled `dist/`. |

### Technology stack

| Layer | Choice |
|---|---|
| Language | TypeScript (target ESNext, ESM modules) |
| Build | `tsc` only — no bundler, no Babel, no esbuild |
| Test runner | Node's built-in `node:test` (`node --test`) |
| Coverage | `c8` (V8 source-map-aware, dev-only) |
| Runtime | Node.js ≥18 (no native modules, all `node:*` builtins) |
| Distribution | Claude Code plugin marketplace (`/plugin install ...`) |
| License | MIT |

### Data flow for the main "render once" workflow

1. Claude Code calls the configured `statusLine.command`.
2. The command runs `node ~/.claude/plugins/claude-hud/dist/index.js` with
   the JSON of the current session piped to stdin.
3. `readStdin()` reads, parses, and returns a `StdinData` object (or
   `null` if stdin is a TTY or empty within the timeouts).
4. `parseTranscript(stdin.transcript_path)` either reads from the
   on-disk JSONL transcript or returns the cached parse result if
   transcript mtime + size haven't changed.
5. `countConfigs(stdin.cwd)` reads or returns from cache.
6. `loadConfig()` reads the user's `config.json` or returns defaults.
7. `getGitStatus(stdin.cwd)` runs git subprocesses if `gitStatus.enabled`.
8. `getUsageFromStdin(stdin)` extracts the rate-limit windows.
9. `parseExtraCmdArg()` reads `--extra-cmd` from `process.argv`; if
   present, `runExtraCmd()` executes it.
10. `formatSessionDuration()` formats start-time delta.
11. `getClaudeCodeVersion()` runs only if
    `display.showClaudeCodeVersion`.
12. `getMemoryUsage()` runs only if `display.showMemoryUsage` AND
    `lineLayout === "expanded"`.
13. The full `RenderContext` is handed to `render(ctx)`, which iterates
    enabled elements in `elementOrder`, computes line widths with grapheme
    awareness, wraps to terminal width, and `console.log()`s each line.
14. The process exits.

There is no shared state across invocations except the on-disk caches.

> [!NOTE]
> The driver is wrapped in a single `try / catch`. Any uncaught error
> falls through to `[claude-hud] Error: <message>` on stdout. The plugin
> is designed to never block Claude Code's prompt cycle, even if every
> probe fails.

### Cache topology

| Cache | Location | Key | TTL / Invalidation |
|---|---|---|---|
| Transcript parse | `<plugin>/transcript-cache/<sha256-of-resolved-transcript-path>.json` | transcript path + mtime + size | Auto (mtime/size change) |
| Config counts | `<plugin>/config-cache/<sha256-of-{cwd,claudeConfigDir}>.json` | cwd + Claude config dir + per-file sentinels (CLAUDE.md, rules dir, settings.json, settings.local.json, .mcp.json, etc.) | Auto (any sentinel mtime/size change) |
| Claude Code version | `<plugin>/.claude-code-version-cache.json` | binary realpath + mtime | Auto (binary mtime change) — known bug: in-place updates that preserve mtime are not detected (issue #429) |
| Output speed | `<plugin>/.speed-cache.json` | (singleton) | Sliding 2-second window |

## 2. System Requirements

### Runtime requirements

| Component | Required version | Why |
|---|---|---|
| Node.js | ≥18.0.0 | Declared in `package.json` engines field. ESM, `Intl.Segmenter`, `node:test` all rely on 18+. |
| Claude Code | Any modern release; 2.0+ recommended; 2.1.6+ for native context % | `claude-hud` adapts to whatever stdin shape Claude Code emits, but newer fields like `context_window.used_percentage` and `cost.total_cost_usd` are only honoured when present. |
| Operating system | macOS / Linux / Windows | All three are supported. macOS-specific code paths exist (`vm_stat`, init message reminding users to restart Claude Code). Windows-specific code paths exist (`PATHEXT`, `cmd.exe /c` for `.cmd`/`.bat` Claude binaries, drive-root path normalisation). |
| Terminal | UTF-8, ANSI colour | The HUD prints unicode bars, emojis, OSC-8 hyperlinks, and 256-color/truecolor escape codes. |

### Build-time requirements (for contributors)

| Component | Version | Why |
|---|---|---|
| TypeScript | ^6.0.2 | `tsc` is the only compiler. No bundler. |
| `@types/node` | ^25.5.2 | Type definitions for `node:*`. |
| `c8` | ^11.0.0 | Coverage during `npm run test:coverage`. |

`devDependencies` live in `package.json`; there are zero runtime
`dependencies`. The published plugin contains the compiled `dist/` plus
`src/`, `commands/`, and `.claude-plugin/`.

### Disk footprint

The plugin itself is well under 1 MB compiled (the source is ~270 KB, the
emitted JS is similar). Caches add up over time but each cache file is
small (≤ a few KB):

- `transcript-cache/` — one file per distinct transcript path (one per
  session). Pruning is opportunistic — files become unreadable and are
  silently rebuilt when transcript mtime/size mismatch.
- `config-cache/` — one file per distinct `(cwd, claudeConfigDir)` pair.

> [!IMPORTANT]
> There is no automatic cache cleanup. If you switch between many
> projects, the `config-cache/` and `transcript-cache/` directories will
> grow unboundedly. This is generally fine — files are tiny — but
> `rm -rf ~/.claude/plugins/claude-hud/{config,transcript}-cache` is a
> safe no-op recovery if you ever want to start fresh.

## 3. Configuration Reference

This section is the **complete** reference. Every key, every default,
every type, every clamp.

### Configuration files

| File | Format | Location | Purpose |
|---|---|---|---|
| `config.json` | JSON | `<plugin>/config.json` (where `<plugin>` is `~/.claude/plugins/claude-hud/` by default, or `$CLAUDE_CONFIG_DIR/plugins/claude-hud/`) | User-facing config: layout, display flags, colours, thresholds. Read by `loadConfig()`. |
| `~/.claude/settings.json` | JSON | User home (or `$CLAUDE_CONFIG_DIR/settings.json`) | Where Claude Code itself stores `statusLine.command`. `claude-hud` does not read this except via `countConfigs()` to count MCPs/hooks/outputStyle. |
| `~/.claude.json` | JSON | User home | Claude Code's user MCP servers + `disabledMcpServers`. Read by `countConfigs()`. |
| `<cwd>/.mcp.json` | JSON | Per-project | Project-level MCP server registrations. Read by `countConfigs()`. |
| `<cwd>/.claude/settings.{json,local.json}` | JSON | Per-project | Project Claude Code settings, including hooks and `disabledMcpjsonServers`. Read by `countConfigs()`. |
| `~/.claude/CLAUDE.md`, `<cwd>/CLAUDE.md`, `<cwd>/CLAUDE.local.md`, `~/.claude/rules/**/*.md`, `<cwd>/.claude/rules/**/*.md` | Markdown | Various | Counted by `countConfigs()`; never parsed for content. |

### Environment variables

`claude-hud` reads only a small set of env vars; all others are ignored.

| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| `CLAUDE_CONFIG_DIR` | path | `~/.claude` | No | Override the Claude config directory. Affects every cache and the plugin install location. Resolves `~` prefixes. |
| `COLUMNS` | int | unset | No | Last-resort terminal width when neither `process.stdout.columns` nor `process.stderr.columns` is available. Default fallback is `UNKNOWN_TERMINAL_WIDTH = 40`. |
| `COMSPEC` | path | `cmd.exe` | No (Windows only) | Used when invoking `claude --version` against a `.cmd` or `.bat` binary on Windows. |
| `PATHEXT` | semicolon-separated string | `.COM;.EXE;.BAT;.CMD` | No (Windows only) | Used when resolving `claude` on PATH on Windows. |
| `PATH` | colon/semicolon-separated paths | inherited | Yes (for `claude --version`) | Searched by `resolveClaudeBinaryFromPath()`. |
| `DEBUG` | string | unset | No | If it includes `claude-hud` (or equals `*`), namespaced debug output goes to stderr. |
| `LANG` | locale | system | No | Read only by tests to enforce English output. |

### CLI flags

The HUD has exactly one user-facing CLI flag.

| Flag | Type | Default | Description |
|---|---|---|---|
| `--extra-cmd <command>` or `--extra-cmd=<command>` | string | unset | A user-defined shell command. Output is parsed as JSON `{ "label": "..." }`; the label is sanitised, truncated to 50 chars, and rendered on the project line. 3-second timeout, 10 KB stdout cap. |

There are no other flags. The plugin does not respond to `--help`,
`--version`, or `-h`.

### `HudConfig` shape (every key)

#### Top level

| Key | Type | Default | Validator | Notes |
|---|---|---|---|---|
| `language` | `'en' \| 'zh'` | `'en'` | `validateLanguage` | Anything else → default. |
| `lineLayout` | `'compact' \| 'expanded'` | `'expanded'` | `validateLineLayout` | Anything else → default. |
| `showSeparators` | boolean | `false` | type check | Non-boolean → default. |
| `pathLevels` | `1 \| 2 \| 3` | `1` | `validatePathLevels` | Anything else → default. |
| `elementOrder` | `HudElement[]` | `['project', 'context', 'usage', 'memory', 'environment', 'tools', 'agents', 'todos']` | `validateElementOrder` | Drops unknown items, dedupes, falls back to default if empty. |

#### `gitStatus`

| Key | Type | Default | Validator |
|---|---|---|---|
| `enabled` | boolean | `true` | type check |
| `showDirty` | boolean | `true` | type check |
| `showAheadBehind` | boolean | `false` | type check |
| `showFileStats` | boolean | `false` | type check |
| `pushWarningThreshold` | number ≥0 (integer) | `0` | `validateCountThreshold` (clamps NaN/negative to 0, floors floats) |
| `pushCriticalThreshold` | number ≥0 (integer) | `0` | `validateCountThreshold` |

#### `display`

| Key | Type | Default | Validator |
|---|---|---|---|
| `showModel` | boolean | `true` | type check |
| `showProject` | boolean | `true` | type check |
| `showContextBar` | boolean | `true` | type check |
| `contextValue` | `'percent' \| 'tokens' \| 'remaining' \| 'both'` | `'percent'` | `validateContextValue` |
| `showConfigCounts` | boolean | `false` | type check |
| `showCost` | boolean | `false` | type check |
| `showDuration` | boolean | `false` | type check |
| `showSpeed` | boolean | `false` | type check |
| `showTokenBreakdown` | boolean | `true` | type check |
| `showUsage` | boolean | `true` | type check |
| `usageBarEnabled` | boolean | `true` | type check |
| `showTools` | boolean | `false` | type check |
| `showAgents` | boolean | `false` | type check |
| `showTodos` | boolean | `false` | type check |
| `showSessionName` | boolean | `false` | type check |
| `showClaudeCodeVersion` | boolean | `false` | type check |
| `showMemoryUsage` | boolean | `false` | type check (rendered only in expanded layout) |
| `showSessionTokens` | boolean | `false` | type check |
| `showOutputStyle` | boolean | `false` | type check |
| `autocompactBuffer` | `'enabled' \| 'disabled'` | `'enabled'` | `validateAutocompactBuffer` |
| `usageThreshold` | number 0-100 | `0` | `validateThreshold` (clamps to [0,100]) |
| `sevenDayThreshold` | number 0-100 | `80` | `validateThreshold` |
| `environmentThreshold` | number 0-100 | `0` | `validateThreshold` |
| `modelFormat` | `'full' \| 'compact' \| 'short'` | `'full'` | `validateModelFormat` |
| `modelOverride` | string ≤80 chars | `''` | type check + slice(0, 80) |
| `customLine` | string ≤80 chars | `''` | type check + slice(0, 80) |

#### `colors`

Each value is a `HudColorValue`: a `HudColorName` (`dim`, `red`, `green`,
`yellow`, `magenta`, `cyan`, `brightBlue`, `brightMagenta`), an integer
0-255, or `#rrggbb`. Anything else falls back to the default.

| Key | Default | Validator |
|---|---|---|
| `context` | `green` | `validateColorValue` |
| `usage` | `brightBlue` | `validateColorValue` |
| `warning` | `yellow` | `validateColorValue` |
| `usageWarning` | `brightMagenta` | `validateColorValue` |
| `critical` | `red` | `validateColorValue` |
| `model` | `cyan` | `validateColorValue` |
| `project` | `yellow` | `validateColorValue` |
| `git` | `magenta` | `validateColorValue` |
| `gitBranch` | `cyan` | `validateColorValue` |
| `label` | `dim` | `validateColorValue` |
| `custom` | `208` (Claude orange) | `validateColorValue` |

> [!IMPORTANT]
> `mergeConfig()` is **lenient by design**: every invalid value falls
> back to the default rather than raising. There is no way to tell
> "config saved but values were rejected" from the HUD itself — set
> `DEBUG=claude-hud` and run a smoke test if you suspect silent rejection.

### Legacy migrations

| Source | Target |
|---|---|
| `layout: "default"` (string) | `lineLayout: "compact"`, `showSeparators: false`, drop `layout` |
| `layout: "separators"` (string) | `lineLayout: "compact"`, `showSeparators: true`, drop `layout` |
| `layout: { lineLayout, showSeparators, pathLevels }` (object) | Lift each into top level if not already set, drop `layout` |

`migrateConfig()` is invoked unconditionally on load.

## 4. Deployment & Scaling

`claude-hud` runs as a per-prompt subprocess. It has no notion of
"deployment" in the cluster sense — but operators wiring it into shared
dev images, devcontainers, or CI smoke tests should know how the install
shape behaves.

### Production deployment shape

```text
Developer machine
└── ~/.claude/plugins/claude-hud/        (plugin install root)
    ├── dist/index.js                     (compiled entrypoint)
    ├── src/                              (TypeScript sources, shipped)
    ├── commands/configure.md             (slash command source)
    ├── commands/setup.md                 (slash command source)
    ├── .claude-plugin/                   (plugin manifest)
    ├── config.json                       (USER WRITES THIS)
    ├── config-cache/<sha256>.json        (auto)
    ├── transcript-cache/<sha256>.json    (auto)
    ├── .claude-code-version-cache.json   (auto)
    └── .speed-cache.json                 (auto)
```

The `package.json` `files` field intentionally publishes `dist/`, `src/`,
`commands/`, and `.claude-plugin/` so users can read sources directly from
their plugin install — useful both for transparency and for tweaking
`commands/setup.md` locally.

### Devcontainer / dotfiles automation

Because the marketplace install requires a Claude Code session,
fully unattended provisioning isn't practical. Closest patterns:

1. **Pre-bake the install** — run `/plugin marketplace add` and
   `/plugin install` once interactively, commit the resulting
   `~/.claude/plugins/claude-hud/` tree to your dotfiles, and restore it
   on boot. `/claude-hud:setup` still has to run interactively to write
   `~/.claude/settings.json`.
2. **Pre-bake `settings.json`** — write the `statusLine.command` entry
   yourself in your dotfiles/devcontainer Dockerfile, pointing at
   `~/.claude/plugins/claude-hud/dist/index.js`, and skip `/claude-hud:setup`
   entirely. The downside: you lose the dynamic version resolution that
   `/claude-hud:setup` writes.

> [!TIP]
> If you go the pre-baked-`settings.json` route, prepend
> `COLUMNS=$(tput cols) ` to the command — see issue #416. Without it
> you'll hit the `UNKNOWN_TERMINAL_WIDTH = 40` fallback every time
> Claude Code spawns the subprocess.

### Scaling behaviour

`claude-hud` runs once per Claude Code prompt. There's nothing to scale
in the traditional sense, but performance characteristics matter because
the subprocess executes synchronously inside Claude Code's prompt cycle:

- **Cold path** (no caches): one Node startup, one transcript JSONL
  read, several git subprocesses, optional `claude --version`, optional
  `vm_stat`. Typical latency: tens to a couple hundred milliseconds on
  modern hardware.
- **Warm path** (caches hit): all of the above short-circuit immediately;
  only the stdin read, the transcript mtime check, and the always-fresh
  rendering happen. Typical latency: a few milliseconds.

> [!NOTE]
> Because Claude Code spawns the statusline subprocess on every prompt,
> Node.js startup time (typically 30-80 ms) dominates total latency on
> small projects. There's no way to avoid this without changing Claude
> Code's plugin protocol — the entire plugin model is "spawn fresh".

### Multi-user systems

Each user has their own `~/.claude/plugins/claude-hud/`. Caches are
per-user. There's no cross-user state; nothing to coordinate.

### Disabling without uninstalling

Set `~/.claude/settings.json` to remove or comment out the `statusLine`
entry. The plugin remains on disk but never executes.

## 5. Operations

### Health check / smoke test

Run the canonical stdin smoke test from anywhere:

```bash
echo '{"model":{"display_name":"Opus"},"context_window":{"current_usage":{"input_tokens":45000},"context_window_size":200000},"transcript_path":"/tmp/test.jsonl"}' \
  | node ~/.claude/plugins/claude-hud/dist/index.js
```

Expected: a single colourised line (default-config compact would be the
session line). Non-zero exit code or missing output indicates a broken
install.

The plugin source ships the same smoke test as `npm run test:stdin` in
`package.json`.

### Debug telemetry

There is no metrics endpoint, no log file, no remote telemetry. The only
runtime observability is `DEBUG=claude-hud` (or `DEBUG=*`) on stderr.
Namespaces in use:

| Namespace | Source | Meaning |
|---|---|---|
| `[claude-hud:config]` | `src/config-reader.ts` | A `settings.json`/`.mcp.json` read or parse failed; the count silently fell to zero. |
| `[claude-hud:context]` | `src/render/session-line.ts`, `src/render/lines/identity.ts` | Active when `autocompactBuffer === "disabled"`; reports raw vs buffered percent. |
| `[claude-hud:extra-cmd]` | `src/extra-cmd.ts` | `--extra-cmd` parse / execution failures (timeout, JSON parse error, missing `label`). |

> [!NOTE]
> Stderr is **not** captured by Claude Code's statusline pipe — it goes
> to the parent terminal. That's deliberate (you can keep
> `DEBUG=claude-hud` on without breaking the HUD output) but means
> debug logs aren't preserved across sessions. Pipe stderr to a file in
> the `statusLine.command` if you need durable logs.

### Routine operations

| Task | How |
|---|---|
| Upgrade plugin | `/plugin marketplace update` then `/plugin install claude-hud` |
| Force version recheck | `rm ~/.claude/plugins/claude-hud/.claude-code-version-cache.json` |
| Force config recount | `rm -rf ~/.claude/plugins/claude-hud/config-cache` |
| Force transcript reparse | `rm -rf ~/.claude/plugins/claude-hud/transcript-cache` |
| Reset config | `rm ~/.claude/plugins/claude-hud/config.json` |
| Disable temporarily | Comment out `statusLine` in `~/.claude/settings.json` |
| Permanently uninstall | `/plugin uninstall claude-hud` then remove `statusLine` |

### Operational checklist

- [ ] Plugin installed via `/plugin install claude-hud`
- [ ] `/claude-hud:setup` completed (or `statusLine` entry written manually)
- [ ] `node ~/.claude/plugins/claude-hud/dist/index.js < smoke.json` returns a styled line
- [ ] `claude --version` returns a number when run from your shell
- [ ] Terminal supports UTF-8 and ANSI colour
- [ ] If running over a pipe (statusline subprocess), `COLUMNS=$(tput cols)` is prepended in `settings.json` (issue #416)
- [ ] On macOS, Claude Code has been fully restarted post-install (issue #375)

### Common operational signals

| Symptom | Most likely cause | Remediation |
|---|---|---|
| HUD blank | Missing `statusLine` entry | Re-run `/claude-hud:setup` |
| HUD wraps | Terminal width unknown | Prepend `COLUMNS=$(tput cols) ` (issue #416/#436) |
| `CC v...` shows old version | Version cache hit despite update | Touch the binary or delete the version cache (issue #429) |
| Cost line missing | Model not in pricing table | Wait for upstream fix (issue #452) |
| Compact looks like expanded | v0.0.12 regression | Pin v0.0.11 or use expanded (issues #408/#412) |
| `style:` doesn't appear | `outputStyle` not set in any settings.json | Set in user or project settings.json |

## 6. Performance

`claude-hud` is engineered to never noticeably slow Claude Code's prompt
cycle, even on large projects.

### Performance budget (per prompt)

| Phase | Cold | Warm | Notes |
|---|---|---|---|
| Node.js startup | ~30-80 ms | ~30-80 ms | Unavoidable; Claude Code spawns fresh. |
| `readStdin` | ≤30 ms idle timeout | same | First-byte timeout 250 ms (Claude Code is much faster in practice). |
| `parseTranscript` | linear in JSONL size; bounded by file mtime cache | ~1 ms | Streamed via `readline.createInterface`. |
| `countConfigs` | O(rules markdown files + a few stat()s) | ~1 ms (sentinel match) | mtime-keyed cache. |
| `loadConfig` | one JSON read + validate | same | Tiny file. |
| `getGitStatus` | up to 5 git fork-execs, each capped at 1-2 s | same | Returns `null` if cwd is missing. |
| `getClaudeCodeVersion` | one fork-exec capped at 2 s | ~1 ms | Cached. |
| `getMemoryUsage` (macOS) | one `vm_stat` fork-exec capped at 5 s | same | No cache. |
| Render | linear in line count, with grapheme + ANSI awareness | same | Bounded — element order ≤8, activity items ≤(2 running tools + 4 frequency + 3 agents + 1 todo). |

### Caching strategy

The four mtime-keyed caches (transcript, config, version, speed) are the
heart of warm-path performance. Cache **writes** are best-effort
(`try/catch` swallowed) and cache **reads** validate every key field
before trusting the entry.

```typescript
// From src/transcript.ts: cache invalidation logic
if (
  parsed.transcriptPath !== path.resolve(transcriptPath)
  || parsed.transcriptState?.mtimeMs !== state.mtimeMs
  || parsed.transcriptState?.size !== state.size
) {
  return null;
}
```

The config-counts cache is more elaborate — it sentinels every input
file (CLAUDE.md, settings.json, settings.local.json, .mcp.json,
~/.claude.json) AND the entire `rules/` directory tree. A sentinel is
`{mtimeMs, size}`; when **any** sentinel changes, the cache is rebuilt.

> [!TIP]
> The cache is stored under `~/.claude/plugins/claude-hud/config-cache/`,
> which lives under the same `~/.claude/` directory whose mtime would
> otherwise invalidate the cache itself. The implementation deliberately
> sentinels `~/.claude/CLAUDE.md` directly, not the parent directory, to
> avoid a self-invalidation loop.

### Width calculation

The renderer's grapheme-aware width logic (in `src/render/index.ts`)
handles:

- ANSI escape sequences (CSI + OSC) — width 0
- Combining marks, ZWJ, variation selectors — width 0
- Extended pictographic glyphs (most emoji) — width 2
- East Asian wide characters (CJK, Hangul, fullwidth forms) — width 2
- Everything else — width 1

It uses `Intl.Segmenter` when available (Node 16+), falling back to
`Array.from()` otherwise. The wrapper function (`wrapLineToWidth`)
prefers ` | ` and ` │ ` separators as wrap points and keeps the leading
`[model | provider]` block together so the badge isn't split.

### Resource limits

| Resource | Limit | Source |
|---|---|---|
| Stdin total bytes | 256 KB | `DEFAULT_MAX_STDIN_BYTES` |
| Stdin first-byte timeout | 250 ms | `DEFAULT_FIRST_BYTE_TIMEOUT_MS` |
| Stdin idle timeout | 30 ms | `DEFAULT_IDLE_TIMEOUT_MS` |
| Git subprocess timeout | 1 s (most), 2 s (numstat) | hardcoded in `getGitStatus()` |
| `claude --version` timeout | 2 s | hardcoded in `getClaudeCodeVersion()` |
| `vm_stat` timeout | 5 s | hardcoded in `readMacOSMemory()` |
| `--extra-cmd` timeout | 3 s | `TIMEOUT_MS` |
| `--extra-cmd` stdout cap | 10 KB | `MAX_BUFFER` |
| `--extra-cmd` label length | 50 chars | `MAX_LABEL_LENGTH` |
| Transcript tools shown | last 20 | `transcript.ts` |
| Transcript agents shown | last 10 | `transcript.ts` |
| Project label segments | 1, 2, or 3 | `pathLevels` |
| `modelOverride` length | 80 chars | `mergeConfig()` |
| `customLine` length | 80 chars | `mergeConfig()` |

> [!NOTE]
> The 256 KB stdin cap is more than enough for Claude Code's status
> payload (typically <10 KB). If Claude Code ever ships a much larger
> payload, the cap would need raising — currently `claude-hud` would
> refuse to parse and return null silently.

## 7. Security

`claude-hud` is a local-only tool, but it lives at the intersection of
Claude Code transcripts (which can contain anything you typed or the
model output, including credentials, source code, customer data, PII,
internal docs) and the user's shell. Treat it as **highly trusted local
software** and read this section before deploying it to anyone but
yourself.

### Threat model

| Threat | Severity | Mitigation |
|---|---|---|
| Transcript content leaking via the HUD | High | The HUD itself prints summarised metadata only — no transcript prose, no tool input/output, no model response. |
| Transcript content leaking via caches | High | Caches store only the parsed *summary* (tool names, agent counts, todo titles, token totals) — never the raw transcript text. Still: the cache files live alongside everything else under `~/.claude/`, which deserves filesystem permissions hardening. |
| Malicious `--extra-cmd` running as the user | High | `--extra-cmd` is treated as user input by design; the comment in `src/extra-cmd.ts` is explicit about this. The mitigation is "don't paste an `--extra-cmd` somebody else gave you". Output is sanitised (ANSI, control chars, bidi) before display. |
| Malicious project triggering arbitrary git commands | Low | Only `git rev-parse`, `git status`, `git diff --numstat`, `git rev-list`, `git remote get-url` are run, all with `--no-optional-locks` for the porcelain status. None of them execute hooks. |
| Terminal-escape injection from filenames or branch names | Medium | The renderer's wrapping/truncation preserves ANSI escapes the renderer itself produced, but does not sanitise external strings entering it (branch names, filenames, custom titles). A maliciously named branch could in theory contain ANSI escapes; in practice git rejects most of these. Treat untrusted repos with the usual care. |
| Stdin payload too large / malformed | Low | 256 KB cap, JSON parse wrapped in `try`. Failures return `null` and the HUD skips rendering. |
| Cache file tampering | Low | All caches are validated on read (type checks for every field). Unrecognised content returns `null` and the cache is rebuilt. The version cache could be poisoned to lie about the Claude Code version, but the consequence is purely cosmetic. |

> [!WARNING]
> The Claude Code transcript JSONL file (`~/.claude/transcripts/...`) is
> the most sensitive piece of data on the machine — it contains the
> verbatim text of every prompt, every tool call, every model response.
> `claude-hud` reads this file. If you are in any environment where
> transcript content must be protected (regulated industries, customer
> data, secrets in transit), the **whole** Claude Code installation
> needs handling — `claude-hud` is one of many tools touching it.

### Network behaviour

`claude-hud` makes **zero** outbound network calls. It does not phone
home, does not send telemetry, does not download anything at runtime.
The only subprocesses it spawns are local (`git`, `claude`, `vm_stat`,
`cmd.exe` on Windows), plus the user's `--extra-cmd` if configured.
Auditors can confirm with `lsof` or `strace`.

### Recommended posture

| Use | Recommendation |
|---|---|
| Personal dev machine | Default install. No additional hardening needed. |
| Shared lab machine / multi-user host | Use `CLAUDE_CONFIG_DIR` to isolate per-user. Ensure `~/.claude` is mode 0700 to keep transcripts and caches per-user-readable. |
| CI runner | Don't install. Statusline plugins offer no value in non-interactive contexts. |
| Devcontainer for an external user | OK to pre-install, but never bake an `--extra-cmd` you didn't author into `settings.json`. |
| Pair-programming / screen-share | Be aware the HUD displays the cwd path, the git branch, and (if enabled) session token totals. Those may leak project identity even if you keep the model response private. |

### Logging policy

`claude-hud` writes to two places only:

1. **stdout** — the HUD output Claude Code displays.
2. **stderr** — debug messages when `DEBUG=claude-hud` is set.

It never writes to a third-party log aggregator or external service.
**Never wire stderr to a third-party log shipper without auditing — debug
logs may include cwd paths, MCP server names, configuration counts, and
similar metadata.**

> [!CAUTION]
> If you must enable `--extra-cmd`, treat the value as code — review
> exactly what it does. Anything you put after `--extra-cmd` runs as
> your user, with your permissions, on every Claude Code prompt. A
> 3-second cap and 10 KB cap are the only built-in guardrails.

### Secrets handling

`claude-hud` stores no secrets. It does not read API keys. It does not
read `.env` files. It does not parse environment variables for tokens.
The only env vars consulted are listed in Section 3 (none secret).

### CVE / disclosure

The repo includes a `SECURITY.md` for vulnerability reporting. Use that
file's contact path rather than filing a public issue.

## 8. Developer Internals

This section is for contributors and operators forking the plugin.

### Repository layout

```text
claude-hud/
├── .editorconfig
├── .gitignore
├── .claude-plugin/         (Claude Code plugin manifest, shipped to users)
├── CHANGELOG.md
├── CLAUDE.README.md         (notes on running Claude Code against this repo)
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE                  (MIT)
├── MAINTAINERS.md
├── README.md
├── RELEASING.md
├── SECURITY.md
├── SUPPORT.md
├── TESTING.md
├── claude-hud-preview-*.png
├── package.json
├── package-lock.json
├── tsconfig.json
├── commands/
│   ├── configure.md         (slash command source: /claude-hud:configure)
│   └── setup.md             (slash command source: /claude-hud:setup)
├── src/
│   ├── claude-config-dir.ts
│   ├── config-reader.ts
│   ├── config.ts
│   ├── constants.ts
│   ├── cost.ts
│   ├── debug.ts
│   ├── extra-cmd.ts
│   ├── git.ts
│   ├── index.ts             (entry point)
│   ├── memory.ts
│   ├── speed-tracker.ts
│   ├── stdin.ts
│   ├── transcript.ts
│   ├── types.ts
│   ├── version.ts
│   ├── i18n/
│   │   ├── en.ts
│   │   ├── index.ts
│   │   ├── types.ts
│   │   └── zh.ts
│   ├── render/
│   │   ├── agents-line.ts
│   │   ├── colors.ts
│   │   ├── index.ts
│   │   ├── session-line.ts
│   │   ├── todos-line.ts
│   │   ├── tools-line.ts
│   │   └── lines/
│   │       ├── cost.ts
│   │       ├── environment.ts
│   │       ├── identity.ts
│   │       ├── index.ts
│   │       ├── memory.ts
│   │       ├── project.ts
│   │       ├── session-tokens.ts
│   │       └── usage.ts
│   └── utils/
│       └── terminal.ts
└── tests/
    ├── *.test.js            (14 test files)
    └── fixtures/
        ├── transcript-basic.jsonl
        ├── transcript-render.jsonl
        └── expected/
            └── render-basic.txt
```

### Build, test, develop

```bash
# Install dev dependencies
npm install

# One-shot build
npm run build              # tsc

# Watch mode while editing
npm run dev                # tsc --watch

# Run tests against compiled dist/
npm test                   # builds, then node --test

# Run with coverage
npm run test:coverage      # c8 + node --test

# Manual stdin smoke test (uses default config)
npm run test:stdin
```

The `prepare` script is intentionally absent — the plugin marketplace
ships the prebuilt `dist/` directly. CI / publishing pipelines should
run `tsc` before tagging releases.

### Module reference (every HIGH-priority module)

#### `src/index.ts` — driver

The `main()` function takes `Partial<MainDeps>` for testing. `MainDeps`
covers every external function (`readStdin`, `parseTranscript`, etc.)
plus `now: () => number` and `log: (...args: unknown[]) => void`. The
default deps are the real implementations. Tests override one or more
deps to assert specific paths.

The "TTY guard" at the bottom of the file:

```typescript
const scriptPath = fileURLToPath(import.meta.url);
const argvPath = process.argv[1];
const isSamePath = (a: string, b: string): boolean => {
  try {
    return realpathSync(a) === realpathSync(b);
  } catch {
    return a === b;
  }
};
if (argvPath && isSamePath(argvPath, scriptPath)) {
  void main();
}
```

ensures `main()` only runs when the file is the entry point — it is safe
to import `main` and `formatSessionDuration` from tests without
triggering a side-effecting render.

#### `src/stdin.ts` — JSON ingestion

Reads stdin with three timeouts (first byte, idle, total bytes). Idle
timeout is the unusual one: it allows Claude Code to dribble JSON in
multiple chunks, but parses as soon as the JSON is complete. If parsing
succeeds, it short-circuits without waiting for stream end.

Native context percent (Claude Code 2.1.6+) wins over manual calculation
in both `getContextPercent` and `getBufferedPercent`. The `Bedrock`
provider is detected by model id substring `anthropic.claude-`; a friendly
display label like `Claude Sonnet 4` is reconstructed via
`normalizeBedrockModelLabel`.

#### `src/transcript.ts` — JSONL streaming parse

Streams the JSONL file via `readline`, accumulates session token totals
across **all** assistant messages (`message.usage`). Tool / agent / todo
state is tracked via `Map`s keyed by tool-use id. The
`TodoWrite`/`TaskCreate`/`TaskUpdate` flow is non-trivial:

- `TodoWrite` rewrites the entire todo list. To preserve in-progress
  state when items are reordered, the parser builds a content-based
  reverse map (content -> task ids) before clearing, then re-establishes
  task-id-to-index mappings for items whose content matches.
- `TaskCreate` appends a todo with the supplied subject/description and
  registers either `input.taskId` or the block id as the lookup key.
- `TaskUpdate` resolves the index via the lookup map, with a fallback
  for numeric-string task ids that act as 1-based indices.

Issue #454 documents a duplicate-content edge case in this logic.

#### `src/config-reader.ts` — `countConfigs()`

The most complex single function in the codebase. Walks user scope and
project scope, dedupes scopes that overlap (when `cwd/.claude` resolves
to `~/.claude`), counts CLAUDE.md files at multiple known paths,
recursively counts rules markdown, computes MCP server set differences,
applies disabled MCP filters, and writes a sentinel-keyed cache.

The cross-platform path comparison (`pathsReferToSameLocation`) tries:
1. Normalise both paths (resolve, lowercase on Windows).
2. If equal, return true.
3. Otherwise, try `realpath` on both and compare the results.

This catches symlink chains and `\\` vs `/` Windows path variants.

#### `src/git.ts` — `getGitStatus()`

Executes 5 git subprocesses in sequence, each with `execFile` (no shell
interpolation). Tolerates missing remotes, no upstream, binary diffs
(skipped via `Number.isNaN(added) || Number.isNaN(deleted)` check), and
renames (`old -> new` parsed from porcelain). Returns `null` for any
non-git directory.

#### `src/render/index.ts` — width-aware rendering

Three responsibilities:

1. Dispatch on `lineLayout` to either `renderExpanded` or `renderCompact`.
2. Run the grapheme-aware visual-length and wrapping logic.
3. Emit `${RESET}${line}` to `console.log` for each visible line.

The terminal width resolution chain is `process.stdout.columns ->
process.stderr.columns -> process.env.COLUMNS -> UNKNOWN_TERMINAL_WIDTH
(40)`. Stderr is the secret weapon — when Claude Code pipes stdout, the
real terminal width is still readable from stderr.

#### `src/render/lines/*` — eight one-job renderers

Every line renderer takes `RenderContext` and returns `string | null`.
They never throw, never share state, never mutate the context. Each is
≤200 lines. This makes them trivially unit-testable: pass a synthetic
context, assert on the output string.

The compact-layout `session-line.ts` is the exception — it's a much
larger renderer that combines what would otherwise be 8 lines into one,
in a specific order, with optional separators. Its complexity is driven
by feature-toggle interactions rather than logic depth.

#### `src/render/colors.ts` — ANSI factory

Maintains the named-preset table, the hex-to-truecolor encoder, and
threshold-aware colour pickers (`getContextColor`, `getQuotaColor`)
that drive the bar styling. `coloredBar` and `quotaBar` are the bar
generators — both clamp percentage to [0,100] and width to non-negative
integers, then emit `█` for filled cells and dim `░` for empty.

### Code patterns and conventions

- **Pure functions everywhere except the driver and the renderer's I/O.**
  No classes, no global mutable state outside `version.ts`'s in-memory
  cache fields.
- **Explicit return-null-on-failure.** Every probe returns `null` (or
  `undefined`) on any error rather than throwing. The driver and the
  renderers all handle null gracefully.
- **Lenient validators.** `mergeConfig()` clamps invalid values to the
  default rather than rejecting. Bias is toward "render something" over
  "fail loudly" — a HUD that crashes is worse than a HUD that shows
  defaults.
- **Test seams via dependency injection.** Every external call is
  injectable. The compiled JS in `dist/` is what tests import — this
  catches type → JS gaps. Tests live in `tests/*.test.js` and use
  Node's built-in `node:test`.
- **mtime + size sentinels for caches.** Every cache validates by
  re-stat'ing inputs; never by TTL alone. Trades a few extra `stat`
  syscalls for instant invalidation.
- **No bundler.** `tsc` compiles to ESM `dist/`, period. Imports use
  `.js` extensions in source (Node ESM convention) so the same paths
  work pre- and post-compile.

### Testing strategy

The test suite is comprehensive for its size. 14 test files cover:

- `index.test.js` — driver, with happy paths, error paths, dependency
  injection, and entry-point isolation.
- `config.test.js` — `mergeConfig` validation, migration, color value
  acceptance.
- `core.test.js` — small primitives.
- `git.test.js` — porcelain parsing, numstat parsing, file stats,
  rename handling.
- `i18n.test.js` — locale lookup with fallback chain.
- `memory.test.js` — `vm_stat` parsing, formatBytes, dependency
  injection of `MemoryReader`.
- `render.test.js` — full render output across layouts and toggles.
- `render-width.test.js` — grapheme + ANSI width edge cases (emoji,
  CJK, ZWJ, control chars, OSC sequences).
- `speed-tracker.test.js` — sliding-window math.
- `stdin.test.js` — timeout / parse / size / TTY behaviours.
- `terminal.test.js` — adaptive bar width.
- `version.test.js` — binary path resolution, `.cmd`/`.bat` invocation,
  cache key behaviour.
- `extra-cmd.test.js` — argv parsing, sanitisation, JSON parse, label
  truncation.
- `integration.test.js` — end-to-end render against fixture transcripts.

The fixture files live at `tests/fixtures/transcript-basic.jsonl`,
`tests/fixtures/transcript-render.jsonl`, and the expected output is
captured in `tests/fixtures/expected/render-basic.txt`. Update fixtures
with `npm run test:update-snapshots`.

### Potential improvements

These are ideas surfaced by the open issues and reading the source —
not endorsed roadmap items.

- **Pricing-table autoupdate (issue #452).** Move the model→pricing
  table out of `src/cost.ts` and either fetch it on first run or expose
  a config-level override map.
- **Compact-layout regression (issues #408, #412).** v0.0.12's
  compact rendering diverges from v0.0.11; trace the regression to
  the `session-line.ts` rewrite.
- **`COLUMNS` injection in setup (issue #416).** Update
  `commands/setup.md` to always prepend `COLUMNS=$(tput cols) ` to the
  written `statusLine.command`. Also worth detecting at runtime when
  the fallback fires and warning to stderr.
- **Cache eviction.** Add a soft cap (e.g. 100 entries) to
  `transcript-cache/` and `config-cache/` with LRU eviction.
- **Codex-hud (issue #215).** Refactor `src/stdin.ts` types into a
  pluggable adapter so other Claude-Code-style tools can be wired up.
- **Native-vs-cache for `claude --version` (issue #429).** Hash the
  Claude Code binary content (or its `--version` output) instead of
  trusting mtime alone.

### Contributing

The repo includes `CONTRIBUTING.md`, `MAINTAINERS.md`, and `RELEASING.md`.
The project is MIT-licensed and accepts external PRs. Test coverage and
type strictness are non-negotiable — every PR runs the full test suite
via Node's built-in test runner.

> [!NOTE]
> Type changes that touch `src/types.ts` (especially `StdinData`) require
> updating both the i18n message catalog (`src/i18n/types.ts`) and the
> renderer expectations. The compiler will catch most of this, but the
> render fixtures need a manual snapshot update.

## 9. Integration Points

### Claude Code (the primary integration)

`claude-hud` consumes JSON from Claude Code's statusline subprocess
contract. The shape it cares about (a subset of what Claude Code emits)
lives in `src/types.ts`'s `StdinData` interface:

```typescript
interface StdinData {
  transcript_path?: string;       // Path to the JSONL transcript
  cwd?: string;                   // Working directory of the session
  model?: {
    id?: string;                  // e.g. "claude-opus-4-6"
    display_name?: string;        // e.g. "Opus 4.6 (1M context)"
  };
  context_window?: {
    context_window_size?: number;
    current_usage?: {
      input_tokens?: number;
      output_tokens?: number;
      cache_creation_input_tokens?: number;
      cache_read_input_tokens?: number;
    } | null;
    used_percentage?: number | null;       // Claude Code 2.1.6+
    remaining_percentage?: number | null;  // Claude Code 2.1.6+ (unused but typed)
  };
  cost?: {
    total_cost_usd?: number | null;
    total_duration_ms?: number | null;     // typed but unused by HUD
    total_api_duration_ms?: number | null; // typed but unused by HUD
    total_lines_added?: number | null;     // typed but unused by HUD
    total_lines_removed?: number | null;   // typed but unused by HUD
  } | null;
  rate_limits?: {
    five_hour?: { used_percentage?: number | null; resets_at?: number | null } | null;
    seven_day?: { used_percentage?: number | null; resets_at?: number | null } | null;
  } | null;
}
```

Every field is optional and every probe handles missing values gracefully.

### Claude Code transcript JSONL

Read by `parseTranscript()` line-by-line. The shape it consumes:

```typescript
interface TranscriptLine {
  timestamp?: string;          // ISO 8601
  type?: string;               // 'assistant', 'custom-title', etc.
  slug?: string;               // Auto-generated session slug
  customTitle?: string;        // /rename custom title
  message?: {
    content?: ContentBlock[];
    usage?: {
      input_tokens?: number;
      output_tokens?: number;
      cache_creation_input_tokens?: number;
      cache_read_input_tokens?: number;
    };
  };
}

interface ContentBlock {
  type: string;                // 'tool_use' | 'tool_result' | text
  id?: string;
  name?: string;               // e.g. 'Read', 'Bash', 'TodoWrite', 'Task'
  input?: Record<string, unknown>;
  tool_use_id?: string;
  is_error?: boolean;
}
```

Special-case tool names: `Task`/`Agent` populate the agents map;
`TodoWrite`/`TaskCreate`/`TaskUpdate` drive todos; `Read`/`Write`/`Edit`
extract `file_path` or `path`; `Glob`/`Grep` extract `pattern`; `Bash`
extracts the first 30 chars of `command`. Everything else lands in the
generic tools map.

### Claude Code settings.json

Read indirectly through `countConfigs()` to count MCPs and hooks. The
relevant fields:

```json
{
  "mcpServers": {
    "<name>": { /* opaque config */ }
  },
  "hooks": {
    "<name>": { /* opaque config */ }
  },
  "outputStyle": "tech-leader",
  "disabledMcpServers": ["names"],
  "disabledMcpjsonServers": ["names"]
}
```

Where the parser looks:

| File | Role |
|---|---|
| `~/.claude/settings.json` | User-scope MCPs, hooks, output style |
| `~/.claude/settings.local.json` | User-scope local hooks, output style |
| `~/.claude.json` | User-scope MCPs + `disabledMcpServers` |
| `<cwd>/.mcp.json` | Project MCPs (gated by `disabledMcpjsonServers`) |
| `<cwd>/.claude/settings.json` | Project hooks, output style |
| `<cwd>/.claude/settings.local.json` | Project local hooks, output style + `disabledMcpjsonServers` |

### Git CLI

| Subprocess | Why |
|---|---|
| `git rev-parse --abbrev-ref HEAD` | Branch name |
| `git --no-optional-locks status --porcelain` | Dirty / file stats |
| `git diff --numstat HEAD` | Per-file line diffs |
| `git rev-list --left-right --count @{upstream}...HEAD` | Ahead/behind |
| `git remote get-url origin` | Build branch URL for OSC-8 hyperlink |

All run via `execFile` (no shell). Timeouts: 1s for the first three,
2s for numstat, 1s for the last two.

### `claude` CLI

`getClaudeCodeVersion()` runs `claude --version` (or `cmd.exe /d /s /c
"<path> --version"` on Windows for `.cmd`/`.bat` shims) with a 2-second
timeout. Output is parsed by `_parseClaudeCodeVersion`'s regex:
`/\d+(?:\.\d+)+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/`. The result
is cached on disk keyed by binary path + mtime. `claude` must be
on `PATH` and executable; on Windows it tries every `PATHEXT` extension.

### macOS `vm_stat`

`getMemoryUsage()` runs `/usr/bin/vm_stat` (5-second timeout, encoding
`utf8`) and parses `Pages active`, `Pages wired down`, and `page size of
N bytes` to compute used memory. Falls back to `os.totalmem()` /
`os.freemem()` on parse failure. Other platforms use the `os` module
directly.

### OSC-8 hyperlinks

Both the project label and the git branch (when GitHub remote exists)
are wrapped in OSC-8 escape sequences:

```text
ESC ] 8 ; ; <uri> ESC \ <visible text> ESC ] 8 ; ; ESC \
```

In supporting terminals (iTerm2, modern Windows Terminal, kitty, recent
gnome-terminal), this makes the project name a clickable
`file://<cwd>` link and the branch name a clickable
`https://github.com/<owner>/<repo>/tree/<branch>` link.

### `--extra-cmd` user integration

The single user-visible CLI flag. The contract:

- Parsed from `process.argv` by `parseExtraCmdArg()` — supports both
  `--extra-cmd value` and `--extra-cmd=value`.
- Executed via `child_process.exec` with a 3-second timeout and 10 KB
  stdout cap.
- Output must be JSON of the shape `{ "label": "string" }`. Any other
  shape returns null.
- The `label` value is sanitised (CSI/OSC/control/bidi stripped) and
  truncated to 50 chars before display.

> [!IMPORTANT]
> `--extra-cmd` runs through the user's shell. The repo comments
> explicitly note this is intentional user input — shell injection is
> not a concern *only* because the user types their own command.
> Never accept a `--extra-cmd` value from an untrusted source.

### Integration table (one-liner summary)

| Surface | Direction | Description |
|---|---|---|
| Claude Code stdin | in | JSON status payload, max 256 KB |
| Claude Code transcript JSONL | in | Read line-by-line, mtime-cached |
| Claude Code settings.json / .mcp.json / settings.local.json / ~/.claude.json | in | Counts only |
| `claude --version` subprocess | in | Cached version string |
| `git` subprocesses | in | Branch / dirty / ahead-behind / file stats |
| `vm_stat` subprocess (macOS) | in | Memory usage |
| `--extra-cmd` user subprocess | in | Optional `{label}` JSON |
| Claude Code statusline (stdout) | out | Rendered HUD lines |
| `~/.claude/plugins/claude-hud/*-cache/` | out | Mtime-keyed caches |
| stderr | out | DEBUG-gated trace |

There are no other external integrations — no HTTP, no IPC, no
inter-process state, no telemetry.

---

_Generated April 2026 from jarrodwatts/claude-hud at 64a8099. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
