复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-claude-hud-9973a118f645 ,按照其中的说明把「setup」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
Note: Placeholders like {RUNTIME_PATH}, {SOURCE}, and {GENERATED_COMMAND} should be substituted with actual detected values.
Check for inconsistent plugin state that can occur after failed installations:
macOS/Linux:
# Check 1: Cache exists?
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
CACHE_EXISTS=$(ls -d "$CLAUDE_DIR/plugins/cache"/*/claude-hud 2>/dev/null && echo "YES" || echo "NO")
# Check 2: Registry entry exists?
REGISTRY_EXISTS=$(grep -q "claude-hud" "$CLAUDE_DIR/plugins/installed_plugins.json" 2>/dev/null && echo "YES" || echo "NO")
# Check 3: Temp files left behind?
TEMP_FILES=$(ls -d "$CLAUDE_DIR/plugins/cache/temp_local_"* 2>/dev/null | head -1)
echo "Cache: $CACHE_EXISTS | Registry: $REGISTRY_EXISTS | Temp: ${TEMP_FILES:-none}"
Windows (PowerShell):
$claudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
$cache = (Get-ChildItem (Join-Path $claudeDir "plugins\cache") -Directory | ForEach-Object { Test-Path (Join-Path $_.FullName "claude-hud") }) -contains $true
$registry = (Get-Content (Join-Path $claudeDir "plugins\installed_plugins.json") -ErrorAction SilentlyContinue) -match "claude-hud"
$temp = Get-ChildItem (Join-Path $claudeDir "plugins\cache\temp_local_*") -ErrorAction SilentlyContinue
Write-Host "Cache: $cache | Registry: $registry | Temp: $($temp.Count) files"
| Cache | Registry | Meaning | Action |
|---|---|---|---|
| YES | YES | Normal install (may still be broken) | Continue to Step 1 |
| YES | NO | Ghost install - cache orphaned | Clean up cache |
| NO | YES | Ghost install - registry stale | Clean up registry |
| NO | NO | Not installed | Continue to Step 1 |
If temp files exist, a previous install was interrupted. Clean them up.
If ghost installation detected, ask user if they want to reset. If yes:
macOS/Linux:
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
# Remove orphaned cache (handles both direct and marketplace installs)
rm -rf "$CLAUDE_DIR/plugins/cache"/*/claude-hud
# Remove temp files from failed installs
rm -rf "$CLAUDE_DIR/plugins/cache/temp_local_"*
# Reset registry (removes ALL plugins - warn user first!)
# Only run if user confirms they have no other plugins they want to keep:
echo '{"version": 2, "plugins": {}}' > "$CLAUDE_DIR/plugins/installed_plugins.json"
Windows (PowerShell):
$claudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
# Remove orphaned cache (handles both direct and marketplace installs)
Get-ChildItem (Join-Path $claudeDir "plugins\cache") -Directory | ForEach-Object { Remove-Item -Recurse -Force (Join-Path $_.FullName "claude-hud") -ErrorAction SilentlyContinue }
# Remove temp files
Remove-Item -Recurse -Force (Join-Path $claudeDir "plugins\cache\temp_local_*") -ErrorAction SilentlyContinue
# Reset registry (removes ALL plugins - warn user first!)
'{"version": 2, "plugins": {}}' | Set-Content (Join-Path $claudeDir "plugins\installed_plugins.json")
After cleanup, tell user to run /plugin install claude-hud again followed by /reload-plugins — no restart needed. (Restart Claude Code only if the reinstall still misbehaves; the fresh session re-reads the plugin registry from scratch.)
On Linux only, if install fails with EXDEV: cross-device link not permitted, check whether /tmp and home are on different filesystems:
[ "$(df --output=source ~ /tmp 2>/dev/null | tail -2 | uniq | wc -l)" = "2" ] && echo "CROSS_DEVICE"
The underlying Claude Code bug (#14799) has been fixed, so this mostly affects older Claude Code versions — suggest updating Claude Code first. If updating isn't an option and the check outputs CROSS_DEVICE, the workaround is:
mkdir -p ~/.cache/tmp && TMPDIR=~/.cache/tmp claude /plugin install claude-hud
IMPORTANT: Use the environment context values (Platform: and Shell:) as your starting point. On win32, also check $OSTYPE via the Bash tool. Some Windows sessions report Shell: powershell while the command path exposed to Claude Code is Git Bash/MSYS2. When $OSTYPE is msys or cygwin, the PowerShell command format can fail before PowerShell runs because bash expands $env:VAR, $p, and $(...) expressions first (see #531).
On win32, run this check first:
echo $OSTYPE
| Platform | Shell | OSTYPE | Command Format |
|---|---|---|---|
darwin | any | any | bash (macOS instructions) |
linux | any | any | bash (Linux instructions) |
win32 | bash | any | bash — Windows + Git Bash instructions |
win32 | powershell, pwsh, or cmd | msys or cygwin | bash — Windows + Git Bash instructions (the active command environment is MSYS/Cygwin; PowerShell syntax is unsafe here) |
win32 | powershell, pwsh, or cmd | other / empty | PowerShell — Windows + PowerShell instructions |
macOS/Linux (Platform: darwin or linux):
Get plugin path (sorted by dotted numeric version, not modification time):
ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/*/ 2>/dev/null | awk -F/ '{ print $(NF-1) "\t" $(0) }' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+[[:space:]]' | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1 | cut -f2-
If empty, the plugin is not installed. Go back to Step 0 to check for ghost installation or EXDEV issues. If Step 0 was clean, ask the user to install via /plugin install claude-hud first.
Get runtime absolute path:
darwin or linux, prefer bun for performance and fall back to node:
command -v bun 2>/dev/null || command -v node 2>/dev/null
win32 + bash, require node. Do not fall back to bun on Windows:
command -v node 2>/dev/null
If empty, stop setup and explain that the current shell cannot find the required runtime.
winget is available, recommend:
winget install OpenJS.NodeJS.LTS
/claude-hud:setup.Verify the runtime exists:
ls -la {RUNTIME_PATH}
If it doesn't exist, re-detect or ask user to verify their installation.
Determine source file based on runtime:
darwin or linux, use src/index.ts when the runtime is bun. Otherwise use dist/index.js.dist/index.js.Generate command (quotes around runtime path handle spaces):
The command exports so the HUD knows the real terminal width. Claude Code pipes the subprocess stdout, so is unavailable at runtime. Since Claude Code v2.1.153, and are set natively to the current terminal dimensions before the statusLine command runs, so the inherited value is the primary source. The probe and the 120 fallback remain for older Claude Code versions where may be absent. The accounts for Claude Code's input area padding (2 columns on each side).
Windows + Git Bash (Platform: win32, Shell: bash):
Do not use PowerShell commands when the shell is bash. Claude Code invokes statusLine commands through bash, which will interpret PowerShell variables like $env and $p before PowerShell ever sees them.
On Windows require node and always use dist/index.js.
Important: Do not reuse the macOS/Linux awk-based command on Windows + Git Bash. The awk fragment requires '"'"' quoting to nest single quotes inside bash -c '...'. After JSON encoding and decoding, this quoting breaks on Windows Git Bash, causing a silent syntax error that prevents the HUD process from starting (see #326).
Instead, use sort -V (GNU version sort, included with Git for Windows) which avoids nested single quotes entirely. Also avoid wrapping the generated command in a second bash -c ... layer. Claude Code is already invoking the statusline through bash, so the direct shell command lets exec replace that shell instead of spawning an extra bash wrapper first. The command still exports COLUMNS so the HUD receives the real terminal width, and it uses the marketplace-aware cache glob:
cols=${COLUMNS:-}; case "$cols" in ""|*[!0-9]*) cols=$(stty size 2>/dev/null </dev/tty | awk '{print $2}');; esac; case "$cols" in ""|*[!0-9]*) cols=120;; esac; export COLUMNS=$(( cols > 4 ? cols - 4 : 1 )); plugin_dir=$(ls -1d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/*/ 2>/dev/null | sort -V | tail -1); exec "{RUNTIME_PATH}" "${plugin_dir}{SOURCE}"
Windows + PowerShell (Platform: win32, Shell: powershell, pwsh, or cmd, OSTYPE: other/empty):
Before proceeding: if
echo $OSTYPEreturnedmsysorcygwin, use the Windows + Git Bash instructions above. In that environment, bash can expand PowerShell variables before PowerShell runs.
Get plugin path:
$claudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
(Get-ChildItem (Join-Path $claudeDir "plugins\cache\*\claude-hud\*") -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1).FullName
The trailing \* on the cache glob is required. Without it, Get-ChildItem returns the claude-hud directory itself, whose name does not match the ^\d+(\.\d+)+$ version pattern, so the lookup resolves to $null and any subsequent Join-Path throws (see #521).
If empty or errors, the plugin is not installed. Ask the user to install via marketplace first.
Get runtime absolute path (require node on Windows):
if (Get-Command node -ErrorAction SilentlyContinue) { (Get-Command node).Source } else { Write-Error "Node.js not found" }
If node is not found, stop setup and explain that the current PowerShell session cannot find Node.js.
winget is available, recommend:
winget install OpenJS.NodeJS.LTS
/claude-hud:setup.Use dist\index.js.
Write the Windows statusline launcher.
Windows PowerShell startup plus Get-ChildItem | Sort-Object [version] can exceed Claude Code's render cadence on every statusLine refresh. Write a small Node launcher once during setup, then invoke it through cmd.exe on each refresh. The launcher uses the setup-time validated node.exe, preserves update discovery by finding the latest installed claude-hud version, and prefers inherited COLUMNS before falling back to 120.
The launcher file at $claudeDir/plugins/claude-hud/statusline.mjs should contain:
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const envColumns = Number.parseInt(process.env.COLUMNS ?? '', 10);
const width = Number.isFinite(envColumns) && envColumns > 0 ? envColumns : 120;
process.env.COLUMNS = String(Math.max(1, width - 4));
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
const cacheDir = path.join(claudeDir, 'plugins', 'cache');
function versionParts(value) {
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value);
return match ? match.slice(1, 4).map(Number) : null;
}
function compareVersions(a, b) {
for (let i = 0; i < 3; i += 1) {
if (a[i] !== b[i]) return a[i] - b[i];
}
return 0;
}
const candidates = [];
try {
for (const marketplace of fs.readdirSync(cacheDir, { withFileTypes: true })) {
if (!marketplace.isDirectory()) continue;
const pluginRoot = path.join(cacheDir, marketplace.name, 'claude-hud');
let versions = [];
try {
versions = fs.readdirSync(pluginRoot, { withFileTypes: true });
} catch {
continue;
}
for (const version of versions) {
if (!version.isDirectory()) continue;
const parts = versionParts(version.name);
if (!parts) continue;
const dir = path.join(pluginRoot, version.name);
if (fs.existsSync(path.join(dir, 'dist', 'index.js'))) {
candidates.push({ dir, parts });
}
}
}
} catch {
process.exit(0);
}
candidates.sort((a, b) => compareVersions(a.parts, b.parts));
const latest = candidates.at(-1);
if (!latest) process.exit(0);
const hud = await import(pathToFileURL(path.join(latest.dir, 'dist', 'index.js')).href);
if (typeof hud.main === 'function') {
await hud.main();
}
WSL (Windows Subsystem for Linux): If running in WSL, use the macOS/Linux instructions. Ensure the plugin is installed in the Linux environment (${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/...), not the Windows side.
Run the generated command. It should produce output (the HUD lines) within a few seconds.
Before writing to settings.json, check whether a statusLine key already exists and protect the user's current configuration. This covers the existing-statusLine overwrite issue tracked in #547.
macOS/Linux:
SETTINGS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
EXISTING_COMMAND=""
EXISTING_COMMAND_PREVIEW=""
if [ -f "$SETTINGS" ]; then
EXISTING_COMMAND=$("{RUNTIME_PATH}" -e '
const fs = require("fs");
const settingsPath = process.argv[1];
try {
const text = fs.readFileSync(settingsPath, "utf8");
if (text.trim() === "") process.exit(0);
const json = JSON.parse(text);
const command = json && json.statusLine && typeof json.statusLine.command === "string"
? json.statusLine.command
: "";
process.stdout.write(command);
} catch (error) {
console.error("Unable to read statusLine.command from settings.json: " + error.message);
process.exit(1);
}
' "$SETTINGS") || exit 1
EXISTING_COMMAND_PREVIEW=$(printf '%s' "$EXISTING_COMMAND" | "{RUNTIME_PATH}" -e '
let value = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", chunk => { value += chunk; });
process.stdin.on("end", () => {
const redacted = value
.replace(/\b(Bearer)\s+["\x27]?[^"\x27\s]+/gi, "$1 [REDACTED]")
.replace(/\b(Authorization\s*:\s*)["\x27]?[^"\x27\s]+/gi, "$1[REDACTED]")
.replace(/\b(token|api[_-]?key|secret|password|pass|auth)(=|:)\s*["\x27]?[^"\x27\s]+/gi, "$1$2[REDACTED]")
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]")
.replace(/\bgh[pousr]_[A-Za-z0-9_]{8,}\b/g, "[GITHUB_TOKEN_REDACTED]")
.replace(/\s+/g, " ")
.trim();
process.stdout.write(redacted.length > 160 ? redacted.slice(0, 157) + "..." : redacted);
});
')
fi
Windows (PowerShell):
$settingsPath = if ($env:CLAUDE_CONFIG_DIR) { Join-Path $env:CLAUDE_CONFIG_DIR "settings.json" } else { Join-Path $HOME ".claude\settings.json" }
$existingCommand = ""
$existingCommandPreview = ""
if (Test-Path $settingsPath) {
try {
$json = Get-Content $settingsPath -Raw | ConvertFrom-Json
if ($json.statusLine -and $json.statusLine.command) {
$existingCommand = $json.statusLine.command
}
} catch {
Write-Error "Unable to read statusLine.command from settings.json: $($_.Exception.Message)"
throw
}
if ($existingCommand -ne "") {
$existingCommandPreview = $existingCommand `
-replace "(?i)\b(Bearer)\s+[`"']?[^`"'\s]+", '$1 [REDACTED]' `
-replace "(?i)\b(Authorization\s*:\s*)[`"']?[^`"'\s]+", '$1[REDACTED]' `
-replace "(?i)\b(token|api[_-]?key|secret|password|pass|auth)(=|:)\s*[`"']?[^`"'\s]+", '$1$2[REDACTED]' `
-replace "\bsk-[A-Za-z0-9_-]{8,}\b", 'sk-[REDACTED]' `
-replace "\bgh[pousr]_[A-Za-z0-9_]{8,}\b", '[GITHUB_TOKEN_REDACTED]' `
-replace "\s+", " "
$existingCommandPreview = $existingCommandPreview.Trim()
if ($existingCommandPreview.Length -gt 160) {
$existingCommandPreview = $existingCommandPreview.Substring(0, 157) + "..."
}
}
}
If EXISTING_COMMAND / $existingCommand is non-empty, classify it:
| Pattern in command | Classification | Source label |
|---|---|---|
Contains claude-hud | Reinstall (own config) | claude-hud |
Contains claude-pace | Known project | claude-pace |
Contains cc-statusline or ccstatusline | Known project | cc-statusline |
Contains statusline.sh or statusline.js or statusline.py | Likely another statusline | statusline script |
| Any other non-empty value | Custom script | custom |
| Empty / missing key | Clean install | (none) |
Always create a backup of settings.json before modifying it, regardless of whether a statusline exists. This protects against corruption (see [#315]) and gives users a recovery path.
macOS/Linux:
SETTINGS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
BACKUP_TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH=""
if [ -f "$SETTINGS" ]; then
BACKUP_PATH="${SETTINGS}.bak.${BACKUP_TIMESTAMP}"
if cp "$SETTINGS" "$BACKUP_PATH"; then
echo "Backup created: $BACKUP_PATH"
else
echo "Failed to create backup at: $BACKUP_PATH" >&2
exit 1
fi
fi
Windows (PowerShell):
$backupPath = ""
if (Test-Path $settingsPath) {
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backupPath = "${settingsPath}.bak.${timestamp}"
Copy-Item $settingsPath $backupPath -ErrorAction Stop
Write-Host "Backup created: $backupPath"
}
If the statusline is empty (clean install): Skip this step. Proceed directly to Step 3.
If the statusline is claude-hud (reinstall): Skip this step. The new command replaces the old one — this is an idempotent update. Proceed to Step 3.
If the statusline belongs to a known project or is a custom script: Use AskUserQuestion to ask the user what to do.
Use AskUserQuestion:
Set {REDACTED_COMMAND_PREVIEW} to EXISTING_COMMAND_PREVIEW on macOS/Linux or $existingCommandPreview on Windows. Use only the redacted/truncated preview in the prompt and normal output. Do not print the full previous command because it may contain tokens or secrets.
If the user chooses "Keep" or "Cancel": Stop setup. The backup from 2.5.3 is still available if one was created. Tell the user:
No changes were made to your settings. Your existing statusline is preserved. Setup created no settings mutation apart from the backup file at
{BACKUP_PATH or $backupPath}if that value is set.
If the user chooses "Replace": Proceed to Step 3. The backup from 2.5.3 ensures the previous configuration can be restored.
Store the previous statusLine.command value in a file alongside the settings backup. This makes it easy to restore if the user later wants to switch back.
macOS/Linux:
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
if [ -n "$EXISTING_COMMAND" ]; then
PREVIOUS_COMMAND_DIR="$CLAUDE_DIR/plugins/claude-hud"
PREVIOUS_COMMAND_PATH="$PREVIOUS_COMMAND_DIR/previous-statusline.txt"
mkdir -p "$PREVIOUS_COMMAND_DIR"
chmod 700 "$PREVIOUS_COMMAND_DIR" 2>/dev/null || true
if (
umask 077
printf '%s' "$EXISTING_COMMAND" > "$PREVIOUS_COMMAND_PATH"
); then
chmod 600 "$PREVIOUS_COMMAND_PATH" 2>/dev/null || true
echo "Previous statusline command saved to: $PREVIOUS_COMMAND_PATH"
else
echo "Failed to save previous statusline command to: $PREVIOUS_COMMAND_PATH" >&2
exit 1
fi
fi
Windows (PowerShell):
$claudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
$pluginDir = Join-Path $claudeDir "plugins\claude-hud"
if (-not (Test-Path $pluginDir)) { New-Item -ItemType Directory -Force -Path $pluginDir | Out-Null }
if ($existingCommand -ne "") {
$previousCommandPath = Join-Path $pluginDir "previous-statusline.txt"
[System.IO.File]::WriteAllText($previousCommandPath, $existingCommand, (New-Object System.Text.UTF8Encoding $false))
try {
$acl = Get-Acl $previousCommandPath
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
"FullControl",
"Allow"
)
$acl.SetAccessRule($rule)
Set-Acl -Path $previousCommandPath -AclObject $acl
} catch {
Write-Warning "Saved previous statusline command, but could not tighten file ACLs: $($_.Exception.Message)"
}
}
Read the settings file and merge in the statusLine config, preserving all existing settings:
darwin or linux, or Platform win32 + Shell bash: ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.jsonwin32 + Shell powershell, pwsh, or cmd: settings.json inside $env:CLAUDE_CONFIG_DIR when set, otherwise Join-Path $HOME ".claude"If the file doesn't exist, create it. If it contains invalid JSON, report the error and do not overwrite.
If a write fails with File has been unexpectedly modified, re-read the file and retry the merge once.
A timestamped backup was already created in Step 2.5.3. If Step 2.5.4 prompted the user and they chose "Keep" or "Cancel", do not reach this step — setup has already exited.
{
"statusLine": {
"type": "command",
"command": "{GENERATED_COMMAND}"
}
}
JSON safety: Write settings.json with a real JSON serializer or editor API, not manual string concatenation.
If you must inspect the saved JSON manually, the embedded bash command must preserve escaped backslashes inside the awk fragment.
For example, the saved JSON should contain \\$(NF-1) and \\$0, not \$(NF-1) and \$0.
Windows PowerShell 5.1 BOM: on Windows PowerShell 5.1 (the default shell on Windows 10/11), Set-Content -Encoding UTF8 and Out-File -Encoding UTF8 emit a UTF-8 BOM (EF BB BF). RFC 8259 §8.1 forbids BOM in JSON. PowerShell 7+ added -Encoding utf8NoBOM, but PS 5.1 did not. Use [System.IO.File]::WriteAllText with New-Object System.Text.UTF8Encoding $false to write UTF-8 without a BOM from both PS versions:
[System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding $false))
Verify the first bytes are 7B 0D 0A ({ + CRLF) or 7B 0A ({ + LF), not EF BB BF:
[System.IO.File]::ReadAllBytes($path)[0..2]
After successfully writing the config, tell the user:
✅ Config written. Claude Code reloads settings automatically — the HUD should appear below your input field after your next message in this session (no restart needed). If it doesn't show up after your next interaction, restart Claude Code (quit and run
claudeagain) — older Claude Code versions require a restart to pick up statusLine changes.
Then continue directly to Step 4 in the same session.
Windows note: Keep the settings-reload guidance separate from runtime installation guidance.
node is available in PATH — that shell restart is unrelated to Claude Code picking up the statusLine config.Note: The generated command dynamically finds and runs the latest installed plugin version. Updates are automatic - no need to re-run setup after plugin updates. If the HUD suddenly stops working, re-run /claude-hud:setup to verify the plugin is still installed.
Restoring a previous statusline: If the user previously had a different statusline and wants to restore it, use the backup path printed in Step 2.5.3. The previous command is stored in ~/.claude/plugins/claude-hud/previous-statusline.txt. To restore:
ls -t ~/.claude/settings.json.bak.* | head -1cp ~/.claude/settings.json.bak.{timestamp} ~/.claude/settings.jsonAfter the statusLine is applied, ask the user if they'd like to enable additional HUD features beyond the default 2-line display.
Use AskUserQuestion:
If user selects any options, write plugins/claude-hud/config.json inside the Claude config directory (${CLAUDE_CONFIG_DIR:-$HOME/.claude} on bash, $env:CLAUDE_CONFIG_DIR or Join-Path $HOME ".claude" on PowerShell). Create directories if needed:
| Selection | Config keys |
|---|---|
| Tools activity | display.showTools: true |
| Agents & Todos | display.showAgents: true, display.showTodos: true |
| Session info | display.showDuration: true, display.showConfigCounts: true |
| Session name | display.showSessionName: true |
| Custom line | display.customLine: "<user's text>" — ask user for the text (max 80 chars) |
Merge with existing config if the file already exists. Only write keys the user selected — don't write false for unselected items (defaults handle that).
If user selects nothing (or picks "Other" and says skip/none), do not create a config file. The defaults are fine.
Claude Code only re-runs the statusline after an interaction (a new assistant message, /compact finishing, a permission-mode change, or a vim-mode toggle). Time-based HUD data — session duration, usage reset countdowns, the prompt-cache countdown — therefore goes stale between messages. Claude Code supports an optional refreshInterval key (seconds, minimum 1) on the statusLine settings object that re-runs the command every N seconds.
Of those, only the usage reset countdown is shown by default — session duration and the prompt-cache countdown tick only if the user enabled them (e.g. "Session info" in Step 4, or via config). Mention this if the user seems unsure whether the timer is worth it.
Ask with AskUserQuestion:
If the user picks an interval, merge refreshInterval: <N> into the existing statusLine object in settings.json — preserve type, command, and any other keys. Follow the same rules as Step 3: real JSON serializer, no BOM on Windows, retry once on a concurrent-modification error. Do not re-create the backup; the Step 2.5.3 backup already covers this session.
If the user picks "Other" and gives a numeric interval (e.g. "10" or "10 seconds"), use that value, clamped to a minimum of 1. Treat a non-numeric "Other" answer that declines (skip/none/no) like "No timer".
If the user picks "No timer", do not write the key. If a refreshInterval key already exists in statusLine from a previous run and the user explicitly chose "No timer", remove it.
refreshInterval lives in settings.json, which Claude Code reloads automatically — ticking should start after the user's next interaction. If countdowns don't tick in the current session, tell the user it will take effect after a Claude Code restart; do not treat a non-ticking timer as a setup failure in Step 5.
Each refresh re-runs the full HUD command (runtime startup, transcript parse, git status), so 5 seconds is the recommended default; only suggest 1 second when the user wants visibly smooth countdowns.
Settings reload automatically, so the HUD can appear in the same session where setup was run — it renders after the user's next interaction (their answer to the question below counts as one). No restart is needed on current Claude Code versions.
Use AskUserQuestion:
If yes: Ask the user if they'd like to ⭐ star the claude-hud repository on GitHub to support the project. If they agree and gh CLI is available, first check whether their gh version supports gh repo star. If it does, run gh repo star jarrodwatts/claude-hud. Otherwise fall back to gh api -X PUT /user/starred/jarrodwatts/claude-hud. Only run the star command if they explicitly say yes.
If no: Debug systematically:
Trigger an interaction, then restart if needed:
/compact finishing, a permission-mode change) — sending any message should make it appearclaude again) — older Claude Code versions require a restart to pick up statusLine changes — then re-run /claude-hud:setup to verifyVerify config was applied:
${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json on bash, or settings.json inside $env:CLAUDE_CONFIG_DIR when set, otherwise Join-Path $HOME ".claude" on PowerShell)Test the command manually and capture error output:
{GENERATED_COMMAND} 2>&1
Common issues to check:
"command not found" or empty output:
ls -la {RUNTIME_PATH}command -v node often returns a symlink that can break after version updatescommand -v node on Windows, command -v bun or command -v node on macOS/Linux), and verify with realpath {RUNTIME_PATH} (or readlink -f {RUNTIME_PATH}) to get the true absolute path"No such file or directory" for plugin:
ls "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/Windows shell mismatch (for example, "bash not recognized"):
Platform: + Shell:Windows: HUD shows only "initializing..." with no error (PowerShell shell, MSYS/Cygwin command environment):
COLUMNSprocess.stdout.columnsCOLUMNSLINESCOLUMNSstty size </dev/ttyCOLUMNS- 4The grep pattern uses [[:space:]] rather than \t to match the tab
separator emitted by awk. GNU grep (BRE/ERE) does not interpret
\t as a tab character — it emits warning: stray \ before t and
treats the pattern as literal t, so the regex never matches the awk
output and plugin_dir resolves to an empty string. The runtime then
exits with Module not found "src/index.ts" and no HUD appears.
Setup verification can hide this because some shells alias grep to
alternatives (e.g. ugrep) that do expand \t, while the actual
statusLine subprocess invokes /usr/bin/grep. [[:space:]] is a
POSIX character class supported by both BSD grep (macOS default) and
GNU grep (Linux default).
When runtime is bun - add --env-file /dev/null to prevent Bun from auto-loading project .env files:
bash -c 'cols=${COLUMNS:-}; case "$cols" in ""|*[!0-9]*) cols=$(stty size 2>/dev/null </dev/tty | awk '"'"'{print $2}'"'"');; esac; case "$cols" in ""|*[!0-9]*) cols=120;; esac; export COLUMNS=$(( cols > 4 ? cols - 4 : 1 )); plugin_dir=$(ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/*/ 2>/dev/null | awk -F/ '"'"'{ print $(NF-1) "\t" $(0) }'"'"' | grep -E '"'"'^[0-9]+\.[0-9]+\.[0-9]+[[:space:]]'"'"' | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1 | cut -f2-); exec "{RUNTIME_PATH}" --env-file /dev/null "${plugin_dir}{SOURCE}"'
When runtime is node:
bash -c 'cols=${COLUMNS:-}; case "$cols" in ""|*[!0-9]*) cols=$(stty size 2>/dev/null </dev/tty | awk '"'"'{print $2}'"'"');; esac; case "$cols" in ""|*[!0-9]*) cols=120;; esac; export COLUMNS=$(( cols > 4 ? cols - 4 : 1 )); plugin_dir=$(ls -d "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/*/ 2>/dev/null | awk -F/ '"'"'{ print $(NF-1) "\t" $(0) }'"'"' | grep -E '"'"'^[0-9]+\.[0-9]+\.[0-9]+[[:space:]]'"'"' | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n | tail -1 | cut -f2-); exec "{RUNTIME_PATH}" "${plugin_dir}{SOURCE}"'
Write it using [System.IO.File]::WriteAllText with New-Object System.Text.UTF8Encoding $false so the file is UTF-8 without a BOM:
$wrapperDir = Join-Path $claudeDir "plugins\claude-hud"
New-Item -ItemType Directory -Force -Path $wrapperDir | Out-Null
$wrapperPath = Join-Path $wrapperDir "statusline.mjs"
$wrapperBody = @'
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const envColumns = Number.parseInt(process.env.COLUMNS ?? '', 10);
const width = Number.isFinite(envColumns) && envColumns > 0 ? envColumns : 120;
process.env.COLUMNS = String(Math.max(1, width - 4));
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
const cacheDir = path.join(claudeDir, 'plugins', 'cache');
function versionParts(value) {
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value);
return match ? match.slice(1, 4).map(Number) : null;
}
function compareVersions(a, b) {
for (let i = 0; i < 3; i += 1) {
if (a[i] !== b[i]) return a[i] - b[i];
}
return 0;
}
const candidates = [];
try {
for (const marketplace of fs.readdirSync(cacheDir, { withFileTypes: true })) {
if (!marketplace.isDirectory()) continue;
const pluginRoot = path.join(cacheDir, marketplace.name, 'claude-hud');
let versions = [];
try {
versions = fs.readdirSync(pluginRoot, { withFileTypes: true });
} catch {
continue;
}
for (const version of versions) {
if (!version.isDirectory()) continue;
const parts = versionParts(version.name);
if (!parts) continue;
const dir = path.join(pluginRoot, version.name);
if (fs.existsSync(path.join(dir, 'dist', 'index.js'))) {
candidates.push({ dir, parts });
}
}
}
} catch {
process.exit(0);
}
candidates.sort((a, b) => compareVersions(a.parts, b.parts));
const latest = candidates.at(-1);
if (!latest) process.exit(0);
const hud = await import(pathToFileURL(path.join(latest.dir, 'dist', 'index.js')).href);
if (typeof hud.main === 'function') {
await hud.main();
}
'@
[System.IO.File]::WriteAllText($wrapperPath, $wrapperBody, (New-Object System.Text.UTF8Encoding $false))
$runtimePath is the value detected in step 2 (the absolute path returned by (Get-Command node).Source, typically C:\Program Files\nodejs\node.exe). Set-Content -Encoding UTF8 and Out-File -Encoding UTF8 on Windows PowerShell 5.1 both emit a UTF-8 BOM. WriteAllText + UTF8Encoding $false writes without a BOM in both PowerShell 5.1 and PowerShell 7+.
Generate command:
{CMD_PATH} /d /s /c ""{RUNTIME_PATH}" "{WRAPPER_PATH}""
{CMD_PATH} is the absolute cmd.exe path, preferably $env:SystemRoot\System32\cmd.exe. {WRAPPER_PATH} is the value of $wrapperPath from step 4 (typically C:\Users\<user>\.claude\plugins\claude-hud\statusline.mjs). If you build the string in PowerShell, use:
$cmdPath = Join-Path $env:SystemRoot "System32\cmd.exe"
if (-not (Test-Path $cmdPath)) { $cmdPath = "cmd.exe" }
$generatedCommand = $cmdPath + ' /d /s /c ""' + $runtimePath + '" "' + $wrapperPath + '""'
Shell: powershell with $OSTYPE=msys or $OSTYPE=cygwin, causing bash to process the command before PowerShellecho $OSTYPE in the Bash tool — if it returns msys or cygwin, this is the issuemsys/cygwin, follow the Windows + Git Bash path in Step 1Windows + PowerShell: HUD silent or "initializing..." with no error in any log (OSTYPE is not msys/cygwin):
[Console]::WindowWidth threw System.IO.IOException: The handle is invalid., when the cache glob resolved the claude-hud directory instead of a version directory, or when PowerShell startup exceeded the render cadence.cmd.exe to mirror Claude Code's invocation:
'{}' | & cmd.exe /c '{GENERATED_COMMAND}'
If you see either error, the existing setup predates the Node launcher format. Re-run /claude-hud:setup to regenerate statusline.mjs and a cmd.exe-launched command. See #521.Windows: PowerShell execution policy error:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedPermission denied:
chmod +x {RUNTIME_PATH}WSL confusion:
ls "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/*/claude-hud/If still stuck: Show the user the exact command that was generated and the error, so they can report it or debug further