---
title: "Claude Code 상태줄(statusLine)에 사용량 한도와 git 상태 띄우기"
description: "Claude Code의 상태줄을 활용해 모델 사용량과 git 상태를 한 줄로 표시하는 커스텀 스크립트 제작 과정을 정리했습니다. 데이터 유무에 따른 예외 처리와 OS별 시간 변환 차이, git 정보 추출 시 주의사항 등 안정적인 상태줄을 구현하기 위한 핵심 팁과 셸 스크립트 작성 노하우를 확인해 보세요"
date: 2026-06-26
updated: 2026-06-26T03:09:47.008Z
tags: [claude-code, statusline, bash, git]
canonical: https://blog.wooncloud.com/posts/claude-code-statusline
---

![Claude Code statusLine](/images/posts/claude-code-statusline/dab9444c-1750-40ca-970c-20949c427e60.webp)

Claude Code 화면 맨 아래에는 한 줄짜리 상태줄(statusLine)을 띄울 수 있다. 기본값은 비어 있지만, 임의의 셸 스크립트를 꽂아 원하는 정보를 표시할 수 있다. 나는 여기에 **현재 모델·컨텍스트 사용률·5시간/주간 사용량 한도·git 변경 상태**까지 한 줄로 모아 봤다. 이 글은 그 statusLine을 만든 과정과, 만들면서 부딪힌 몇 가지 함정을 정리한 것이다.

완성된 모습은 이렇다.

```text
~/work/app | ⎇ main ●3 ↑2 | Claude Opus 4.8 | ctx:23% | 5h:24% ⟳14:22 | 주간:82% ⟳06/30
```

왼쪽부터 디렉토리, git 브랜치(+변경 파일 수·푸시 대기), 모델, 컨텍스트 사용률, 5시간 윈도우 사용률(+리셋 시각), 주간 사용률(+리셋 날짜) 순이다.

## statusLine은 어떻게 동작하나

statusLine은 `~/.claude/settings.json`에 커맨드 하나를 등록하면 끝이다.

```json
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline-command.sh"
  }
}
```

등록한 커맨드는 **매 갱신마다 stdin으로 JSON 한 덩어리**를 받는다. 스크립트는 그걸 읽어 한 줄을 stdout으로 내보내면 되고, 그 출력이 그대로 상태줄에 렌더된다. ANSI 색상 코드도 그대로 먹는다.

핵심은 이 JSON에 무엇이 들어오느냐다. 주요 필드만 추리면 다음과 같다.

```json
{
  "model": { "display_name": "Claude Opus 4.8" },
  "cwd": "/Users/me/work/app",
  "workspace": { "git_worktree": "main" },
  "context_window": {
    "used_percentage": 23,
    "remaining_percentage": 77,
    "context_window_size": 200000
  },
  "cost": {
    "total_cost_usd": 0.42,
    "total_lines_added": 156,
    "total_lines_removed": 23
  },
  "rate_limits": {
    "five_hour": { "used_percentage": 24.3, "resets_at": 1782300000 },
    "seven_day": { "used_percentage": 81.7, "resets_at": 1782620000 }
  },
  "exceeds_200k_tokens": false
}
```

`cost`(세션 누적 비용·줄 수), `context_window`(컨텍스트 사용률), 그리고 이 글의 주인공인 `rate_limits`까지. 사용량 한도는 따로 API를 호출할 필요 없이 입력에 통째로 들어온다.

## rate_limits — 조건부로만 존재한다

`rate_limits`는 편하지만 함정이 하나 있다. **항상 오지 않는다.**

- Claude.ai **Pro/Max 구독자**에게만 온다(무료 요금제엔 없음).
- 세션의 **첫 API 응답 이후**부터 채워진다(시작 직후엔 없음).
- `five_hour`와 `seven_day`는 **각각 독립적으로 빠질 수 있다.**

그래서 `rate_limits.five_hour.used_percentage`를 그냥 읽으면 어떤 세션에선 `null`이 박혀 출력이 깨진다. `jq`의 `// empty`로 "없으면 빈 문자열"을 만들고, 빈 값이면 세그먼트 자체를 건너뛰는 게 핵심이다.

```bash
input=$(cat)
five_h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')

# five_h 가 비었으면(구독 아님·첫 응답 전) 이 블록 자체를 건너뛴다
if [ -n "$five_h" ]; then
  five_int=$(printf '%.0f' "$five_h")
  echo "5h:${five_int}%"
fi
```

이 "있을 때만 그린다"는 원칙을 모든 선택적 세그먼트(git, rate_limit, cost…)에 똑같이 적용하면, 어떤 환경에서 실행돼도 깨지지 않는 statusLine이 된다.

## 리셋 시각: epoch → 로컬 시간, BSD/GNU 양쪽 대응

`resets_at`은 Unix epoch(초)다. `82%`만 보여주면 "언제 풀리지?"가 궁금해지니 리셋 시각도 같이 띄운다. 그런데 epoch를 사람이 읽는 시간으로 바꾸는 `date` 문법이 macOS(BSD)와 Linux(GNU)에서 다르다.

- BSD(macOS): `date -r 1782300000 +%H:%M`
- GNU(Linux): `date -d @1782300000 +%H:%M`

둘 다 시도해서 먼저 되는 쪽을 쓰는 헬퍼로 감싼다.

```bash
# epoch + strftime 포맷 → 로컬 시각. BSD·GNU 양쪽 폴백.
fmt_time() {
  date -r "$1" "+$2" 2>/dev/null || date -d "@$1" "+$2" 2>/dev/null
}
```

5시간 윈도우는 오늘 안에 풀리니 `%H:%M`(시:분), 주간 윈도우는 며칠 뒤라 `%m/%d`(월/일)로 포맷을 달리했다. 또 `%-m` 같은 패딩 제거 지시자는 BSD `date`가 못 알아들으니 `%m`(앞자리 0 포함)으로 둬서 이식성을 지켰다.

## git 상태: JSON엔 없으니 직접 계산

브랜치 이름은 `workspace.git_worktree`로 오지만, **변경 여부나 ahead/behind는 JSON에 없다.** 스크립트 안에서 git을 직접 호출해 계산해야 한다.

```bash
git_info=""
if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then
  # 변경 파일 수(staged+unstaged+untracked)
  dirty=$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null | grep -c .)
  # 원격 대비: @{upstream}...HEAD 는 "behind ahead" 순으로 출력
  ab=$(git -C "$cwd" --no-optional-locks rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null)
  behind=$(printf '%s' "$ab" | awk '{print $1}')
  ahead=$(printf '%s' "$ab" | awk '{print $2}')

  [ "${dirty:-0}" -gt 0 ] && git_info="${git_info} ●${dirty}"
  [ "${ahead:-0}" -gt 0 ] && git_info="${git_info} ↑${ahead}"
  [ "${behind:-0}" -gt 0 ] && git_info="${git_info} ↓${behind}"
fi
```

두 가지만 짚어두자.

- **`git rev-list --left-right --count @{upstream}...HEAD`의 출력 순서는 "behind ahead"다.** 왼쪽(upstream에만 있는 커밋 = 내가 뒤처진 수), 오른쪽(HEAD에만 있는 커밋 = 내가 앞선 수). 헷갈려서 ↑↓를 뒤집기 쉽다. upstream이 없는 브랜치면 빈 출력이라 ahead/behind는 자연히 생략된다.
- **`--no-optional-locks`를 붙인다.** statusLine은 자주 실행되는 핫패스라, 마침 다른 git 명령이 도는 순간 인덱스 락을 건드리면 경합이 난다. 이 플래그는 git이 락 잡는 부수효과를 피하게 한다.

## bash 함정: `${arr[*]}`는 IFS의 첫 글자만 쓴다

세그먼트들을 배열에 모아 ` | `로 이어 붙이려 했다. 흔히 쓰는 패턴은 이거다.

```bash
parts=("$dir" "$branch" "$model")
IFS=' | '; echo "${parts[*]}"   # 기대: dir | branch | model
```

그런데 결과는 `dir branch model` — **공백 한 칸**으로만 구분된다. `${arr[*]}`는 IFS 문자열의 **첫 글자 하나**만 구분자로 쓰기 때문이다(`' | '`의 첫 글자는 공백). 멀티 문자 구분자를 원하면 직접 이어 붙여야 한다.

```bash
sep=" | "
out=""
for p in "${parts[@]}"; do
  [ -z "$out" ] && out="$p" || out="$out$sep$p"
done
printf '%s' "$out"
```

## 색상은 헬퍼 하나로

컨텍스트·5시간·주간 사용률 세 곳이 똑같은 임계값 색상 규칙(여유=초록, 주의=노랑, 위험=빨강)을 쓰니, 함수 하나로 묶어 중복을 없앤다.

```bash
# 정수 퍼센트 → ANSI 색상 (≥80 빨강 · ≥50 노랑 · 그 외 초록)
pct_color() {
  if [ "$1" -ge 80 ]; then printf '\033[31m'
  elif [ "$1" -ge 50 ]; then printf '\033[33m'
  else printf '\033[32m'; fi
}
```

## 전체 스크립트

위 조각들을 모은 `~/.claude/statusline-command.sh` 전체다. 모든 선택적 세그먼트가 "데이터 있을 때만" 그려지므로, 구독 여부·git 저장소 여부와 무관하게 안전하게 동작한다.

```bash
#!/usr/bin/env bash
input=$(cat)

# --- 추출 (없으면 빈 문자열) ---
cwd=$(echo "$input"     | jq -r '.cwd // empty')
model=$(echo "$input"   | jq -r '.model.display_name // empty')
used=$(echo "$input"    | jq -r '.context_window.used_percentage // empty')
branch=$(echo "$input"  | jq -r '.workspace.git_worktree // empty')
five_h=$(echo "$input"  | jq -r '.rate_limits.five_hour.used_percentage // empty')
seven_d=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
five_reset=$(echo "$input"  | jq -r '.rate_limits.five_hour.resets_at // empty')
seven_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')

# --- 색상 ---
RESET='\033[0m'; CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; BLUE='\033[34m'; DIM='\033[2m'
pct_color() {
  if [ "$1" -ge 80 ]; then printf '\033[31m'
  elif [ "$1" -ge 50 ]; then printf '\033[33m'
  else printf '\033[32m'; fi
}
fmt_time() { date -r "$1" "+$2" 2>/dev/null || date -d "@$1" "+$2" 2>/dev/null; }

# --- 디렉토리: $HOME → ~ ---
short_dir="${cwd/#$HOME/\~}"; [ -z "$cwd" ] && short_dir="$(pwd | sed "s|$HOME|~|")"

# --- 브랜치 (JSON에 없으면 git 으로) ---
[ -z "$branch" ] && branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null || true)

# --- git 변경/ahead-behind ---
git_info=""
if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then
  dirty=$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null | grep -c .)
  ab=$(git -C "$cwd" --no-optional-locks rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null)
  behind=$(printf '%s' "$ab" | awk '{print $1}'); ahead=$(printf '%s' "$ab" | awk '{print $2}')
  [ "${dirty:-0}" -gt 0 ]  && git_info="${git_info} ${YELLOW}●${dirty}${RESET}"
  [ "${ahead:-0}" -gt 0 ]  && git_info="${git_info} ${CYAN}↑${ahead}${RESET}"
  [ "${behind:-0}" -gt 0 ] && git_info="${git_info} ${CYAN}↓${behind}${RESET}"
fi

# --- 세그먼트 조립 ---
parts=()
parts+=("$(printf "${CYAN}${short_dir}${RESET}")")
[ -n "$branch" ] && [ "$branch" != "HEAD" ] && parts+=("$(printf "${GREEN} ${branch}${RESET}${git_info}")")
[ -n "$model" ] && parts+=("$(printf "${BLUE}${model}${RESET}")")

if [ -n "$used" ]; then
  u=$(printf '%.0f' "$used")
  parts+=("$(printf "$(pct_color "$u")ctx:${u}%%${RESET}")")
fi
if [ -n "$five_h" ]; then
  v=$(printf '%.0f' "$five_h"); r=""
  [ -n "$five_reset" ] && r=" ${DIM}⟳$(fmt_time "$five_reset" '%H:%M')${RESET}"
  parts+=("$(printf "$(pct_color "$v")5h:${v}%%${RESET}${r}")")
fi
if [ -n "$seven_d" ]; then
  v=$(printf '%.0f' "$seven_d"); r=""
  [ -n "$seven_reset" ] && r=" ${DIM}⟳$(fmt_time "$seven_reset" '%m/%d')${RESET}"
  parts+=("$(printf "$(pct_color "$v")주간:${v}%%${RESET}${r}")")
fi

# --- ' | ' 로 수동 조립 (${parts[*]} 는 IFS 첫 글자만 쓰므로) ---
sep="$(printf " ${DIM}|${RESET} ")"; out=""
for p in "${parts[@]}"; do [ -z "$out" ] && out="$p" || out="$out$sep$p"; done
printf '%s' "$out"
```

스크립트엔 실행 권한을 주고(`chmod +x`), `settings.json`의 `statusLine.command`에 경로를 적으면 다음 프롬프트부터 적용된다.

## 정리

statusLine은 결국 "stdin JSON을 받아 한 줄을 뱉는 스크립트"다. 그래서 표시할 수 있는 건 입력 JSON에 들어오는 것 + 셸에서 직접 구할 수 있는 것(git 등)의 합집합이다. 만들면서 기억해 둘 점만 추리면:

- `rate_limits`는 Pro/Max + 첫 응답 이후에만, 그리고 윈도우별로 독립적으로 온다 → **항상 `// empty`로 방어**하고 없으면 세그먼트를 생략한다.
- epoch 변환은 `date -r`(BSD)·`date -d @`(GNU)를 **둘 다 폴백**해야 macOS/Linux 양쪽에서 돈다.
- ahead/behind는 `rev-list --left-right --count @{upstream}...HEAD`로, **"behind ahead" 순서**에 주의.
- `${arr[*]}`의 구분자는 **IFS 첫 글자 하나뿐** — 멀티 문자 구분자는 직접 이어 붙인다.

핫패스에서 도는 스크립트인 만큼 git 호출엔 `--no-optional-locks`를 붙이고, 외부 명령(jq·git·date) 호출 수가 과하지 않은지 한 번씩 점검하면 충분히 가볍게 유지된다.
