Last active
September 24, 2026 15:33
-
-
Save Seklfreak/955eaaabbcc1500fb8ae2ac658355e05 to your computer and use it in GitHub Desktop.
Claude statusline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "strings" | |
| "time" | |
| ) | |
| type Input struct { | |
| HookEventName string `json:"hook_event_name"` | |
| SessionID string `json:"session_id"` | |
| TranscriptPath string `json:"transcript_path"` | |
| Cwd string `json:"cwd"` | |
| Model struct { | |
| ID string `json:"id"` | |
| DisplayName string `json:"display_name"` | |
| } `json:"model"` | |
| Workspace struct { | |
| CurrentDir string `json:"current_dir"` | |
| ProjectDir string `json:"project_dir"` | |
| } `json:"workspace"` | |
| Version string `json:"version"` | |
| OutputStyle struct { | |
| Name string `json:"name"` | |
| } `json:"output_style"` | |
| Cost struct { | |
| TotalCostUSD float64 `json:"total_cost_usd"` | |
| TotalDurationMS int64 `json:"total_duration_ms"` | |
| TotalAPIDurationMS int64 `json:"total_api_duration_ms"` | |
| TotalLinesAdded int `json:"total_lines_added"` | |
| TotalLinesRemoved int `json:"total_lines_removed"` | |
| } `json:"cost"` | |
| ContextWindow struct { | |
| TotalInputTokens int64 `json:"total_input_tokens"` | |
| TotalOutputTokens int64 `json:"total_output_tokens"` | |
| ContextWindowSize int64 `json:"context_window_size"` | |
| UsedPercentage float64 `json:"used_percentage"` | |
| RemainingPercentage float64 `json:"remaining_percentage"` | |
| CurrentUsage struct { | |
| InputTokens int64 `json:"input_tokens"` | |
| OutputTokens int64 `json:"output_tokens"` | |
| CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` | |
| CacheReadInputTokens int64 `json:"cache_read_input_tokens"` | |
| } `json:"current_usage"` | |
| } `json:"context_window"` | |
| RateLimits *struct { | |
| FiveHour *RateLimitWindow `json:"five_hour"` | |
| SevenDay *RateLimitWindow `json:"seven_day"` | |
| } `json:"rate_limits"` | |
| PR *struct { | |
| Number int `json:"number"` | |
| URL string `json:"url"` | |
| ReviewState string `json:"review_state"` | |
| } `json:"pr"` | |
| } | |
| // RateLimitWindow is a claude.ai rolling usage window (5-hour or 7-day). | |
| // Only present for claude.ai Pro/Max accounts; Console/org-billed sessions | |
| // (e.g. the ZDR PHI profile) have no rate limits and omit this. | |
| type RateLimitWindow struct { | |
| UsedPercentage float64 `json:"used_percentage"` | |
| ResetsAt int64 `json:"resets_at"` | |
| } | |
| func main() { | |
| // Read JSON from stdin | |
| data, err := io.ReadAll(os.Stdin) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) | |
| os.Exit(1) | |
| } | |
| var input Input | |
| if err := json.Unmarshal(data, &input); err != nil { | |
| fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err) | |
| os.Exit(1) | |
| } | |
| // Get current directory basename | |
| dirName := filepath.Base(input.Workspace.CurrentDir) | |
| // Get git branch | |
| gitBranch := "" | |
| if branch, err := getGitBranch(input.Workspace.CurrentDir); err == nil && branch != "" { | |
| gitBranch = branch | |
| } | |
| // worktrunk names worktrees "<repo>.<branch-with-slashes-as-dashes>", so | |
| // showing both the directory and the branch repeats the same long branch | |
| // name twice. When the directory matches that pattern, collapse it down | |
| // to just the repo name. | |
| displayDir := dirName | |
| if gitBranch != "" { | |
| branchSlug := strings.ReplaceAll(gitBranch, "/", "-") | |
| if repo, slug, ok := strings.Cut(dirName, "."); ok && slug == branchSlug { | |
| displayDir = repo | |
| } | |
| } | |
| branchSuffix := "" | |
| if gitBranch != "" { | |
| branchSuffix = fmt.Sprintf(" (%s)", gitBranch) | |
| } | |
| // Build status line | |
| statusLine := displayDir + branchSuffix | |
| // Add PR badge, if this worktree has an open PR (clickable via OSC 8) | |
| if input.PR != nil && input.PR.Number > 0 { | |
| prText := fmt.Sprintf("#%d", input.PR.Number) | |
| if input.PR.ReviewState != "" { | |
| prText += " " + input.PR.ReviewState | |
| } | |
| statusLine += " | " + hyperlink(prText, input.PR.URL) | |
| } | |
| // Add model info | |
| if input.Model.DisplayName != "" { | |
| statusLine += fmt.Sprintf(" | %s", input.Model.DisplayName) | |
| } | |
| // Add context remaining (danger = how much is used up, i.e. NOT remaining) | |
| if input.ContextWindow.RemainingPercentage > 0 { | |
| remaining := input.ContextWindow.RemainingPercentage | |
| statusLine += fmt.Sprintf(" | %s remaining", colorizePercent(remaining, 100-remaining)) | |
| } | |
| // Add spend (PHI/Console sessions) or claude.ai usage-limit percentage | |
| // (default sessions with rate limits); falls back to spend either way | |
| // if rate limits aren't present in the input. | |
| statusLine += usageIndicator(input) | |
| // Add active config profile (PHI-dedicated profile is highlighted) | |
| statusLine += fmt.Sprintf(" | %s", profileBadge()) | |
| fmt.Print(statusLine) | |
| } | |
| func usageIndicator(input Input) string { | |
| if !isPHISession() && input.RateLimits != nil { | |
| var parts []string | |
| if w := input.RateLimits.FiveHour; w != nil { | |
| parts = append(parts, rateLimitPart("5h", w)) | |
| } | |
| if w := input.RateLimits.SevenDay; w != nil { | |
| parts = append(parts, rateLimitPart("7d", w)) | |
| } | |
| if len(parts) > 0 { | |
| return " | " + strings.Join(parts, ", ") | |
| } | |
| } | |
| if input.Cost.TotalCostUSD > 0 { | |
| return fmt.Sprintf(" | $%.2f", input.Cost.TotalCostUSD) | |
| } | |
| return "" | |
| } | |
| // rateLimitPart renders "5h NN%", appending a reset countdown once usage is | |
| // high enough to matter (the same >=50% threshold that turns it yellow) so | |
| // the reset time is actionable information rather than line-length clutter. | |
| func rateLimitPart(label string, w *RateLimitWindow) string { | |
| part := fmt.Sprintf("%s %s", label, colorizePercent(w.UsedPercentage, w.UsedPercentage)) | |
| if w.UsedPercentage >= 50 && w.ResetsAt > 0 { | |
| part += fmt.Sprintf(" (resets %s)", formatDuration(time.Until(time.Unix(w.ResetsAt, 0)))) | |
| } | |
| return part | |
| } | |
| // formatDuration renders a countdown like "2h14m" or "45m", floored at "now". | |
| func formatDuration(d time.Duration) string { | |
| if d <= 0 { | |
| return "now" | |
| } | |
| h := int(d.Hours()) | |
| m := int(d.Minutes()) % 60 | |
| if h > 0 { | |
| return fmt.Sprintf("%dh%dm", h, m) | |
| } | |
| return fmt.Sprintf("%dm", m) | |
| } | |
| // activeConfigDir resolves the CLAUDE_CONFIG_DIR this session is running | |
| // under, falling back to the default ~/.claude when unset. | |
| func activeConfigDir() string { | |
| homeDir, err := os.UserHomeDir() | |
| if err != nil { | |
| return "" | |
| } | |
| configDir := os.Getenv("CLAUDE_CONFIG_DIR") | |
| if configDir == "" { | |
| configDir = filepath.Join(homeDir, ".claude") | |
| } | |
| return filepath.Clean(configDir) | |
| } | |
| // isPHISession reports whether this session runs under the PHI-dedicated | |
| // profile (~/.phi-claude, kept logged into the ZDR account for real | |
| // customer data). | |
| func isPHISession() bool { | |
| homeDir, err := os.UserHomeDir() | |
| if err != nil { | |
| return false | |
| } | |
| return activeConfigDir() == filepath.Clean(filepath.Join(homeDir, ".phi-claude")) | |
| } | |
| // profileBadge renders the active profile: bold red "PHI" so it can't be | |
| // missed, or plain "default" otherwise. | |
| func profileBadge() string { | |
| if isPHISession() { | |
| return "\x1b[1;31mPHI\x1b[0m" | |
| } | |
| return "default" | |
| } | |
| const ( | |
| colorReset = "\x1b[0m" | |
| colorGreen = "\x1b[32m" | |
| colorYellow = "\x1b[33m" | |
| colorRed = "\x1b[31m" | |
| ) | |
| // colorizePercent renders value as "NN%" colored by danger: green under 50, | |
| // yellow 50-79, red 80+. danger and value are separate so callers can invert | |
| // the mapping for "remaining" figures, where a LOW value is the dangerous one. | |
| func colorizePercent(value, danger float64) string { | |
| color := colorGreen | |
| switch { | |
| case danger >= 80: | |
| color = colorRed | |
| case danger >= 50: | |
| color = colorYellow | |
| } | |
| return fmt.Sprintf("%s%.0f%%%s", color, value, colorReset) | |
| } | |
| // hyperlink wraps text in an OSC 8 terminal hyperlink pointing at url (iTerm2 | |
| // and most modern terminals render this as clickable; terminals without OSC 8 | |
| // support just show the plain text). Returns text unchanged if url is empty. | |
| func hyperlink(text, url string) string { | |
| if url == "" { | |
| return text | |
| } | |
| return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text) | |
| } | |
| func getGitBranch(dir string) (string, error) { | |
| cmd := exec.Command("git", "-C", dir, "--no-optional-locks", "branch", "--show-current") | |
| output, err := cmd.Output() | |
| if err != nil { | |
| return "", err | |
| } | |
| return strings.TrimSpace(string(output)), nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment