Claude Code를 사용하다 보면 하단 상태 표시줄(status line)이 기본적으로 제공되는데, 여기에 모델명·컨텍스트 사용률·비용·Git 브랜치 같은 정보를 직접 구성할 수 있습니다. 이 글에서는 statusline이 어떻게 동작하는지 개념을 설명하고, 설정 방법 두 가지를 비교한 뒤, 제가 실제로 사용 중인 커스텀 구성을 소개합니다.

제가 현재 사용하는 statusline의 출력 예시는 이렇습니다:

1
fable 5 ~/L/M/i/Documents/PKM ⎇ main 24.8k [░░░░░░░░] 2% · ↻3h50m ↑32%  ↻4d9h ↑3% $10.32

한 줄 안에 모델명, 경로, Git 브랜치, 토큰 수, 컨텍스트 사용률, rate limit 남은 시간, 세션 비용까지 담았습니다.


statusline이란?#

Claude Code statusline은 터미널 하단에 고정 표시되는 커스텀 상태 바입니다. Claude Code가 사용자가 지정한 셸 명령을 실행하고, 그 stdout 출력을 그대로 statusline에 렌더링하는 방식으로 동작합니다.

동작 메커니즘#

Claude Code는 스크립트를 실행할 때 stdin으로 현재 세션 상태를 JSON 형태로 전달합니다. 스크립트는 이 JSON을 파싱해 원하는 값을 추출하고, stdout에 출력하면 그 내용이 statusline에 표시됩니다.

갱신 시점:

  • 어시스턴트 메시지 수신 후
  • /compact 완료 후
  • 권한 모드 변경 시
  • vim 모드 토글 시

빠른 연속 변경은 300ms 디바운스로 묶어서 한 번만 실행합니다. 스크립트가 아직 실행 중에 새 갱신이 오면 진행 중인 실행을 취소하고 새로 시작합니다.

중요: statusline은 로컬에서 실행됩니다. API 토큰을 전혀 소비하지 않습니다.

stdin으로 전달되는 주요 JSON 필드#

필드 설명
model.id 모델 ID (예: claude-sonnet-4-6)
model.display_name 모델 표시명 (예: Sonnet 4.6)
workspace.current_dir 현재 작업 디렉토리
context_window.used_percentage 컨텍스트 윈도우 사용률 (%)
context_window.total_input_tokens 현재 컨텍스트의 입력 토큰 수
context_window.total_output_tokens 현재 컨텍스트의 출력 토큰 수
cost.total_cost_usd 세션 누적 비용 (USD, 클라이언트 추정치)
cost.total_duration_ms 세션 경과 시간 (ms)
rate_limits.five_hour.used_percentage 5시간 rate limit 사용률
rate_limits.five_hour.resets_at 5시간 윈도우 리셋 시각 (Unix epoch)
rate_limits.seven_day.used_percentage 7일 rate limit 사용률
rate_limits.seven_day.resets_at 7일 윈도우 리셋 시각 (Unix epoch)
session_id 세션 고유 ID (캐싱 키로 활용 가능)

rate_limits는 Claude.ai Pro/Max 구독자에게만 첫 API 응답 이후 제공됩니다. 없을 수 있는 필드는 // empty// 0 폴백으로 방어해야 합니다.


설정 방법: 인라인 vs 스크립트 파일#

statusline 설정은 ~/.claude/settings.jsonstatusLine 필드로 합니다. 중요한 점은 type은 항상 "command" 하나뿐이라는 것입니다. “직접 작성 모드”와 “스크립트 모드”가 별도로 있는 게 아니라, command 필드에 무엇을 쓰느냐의 차이입니다.

방법 A: 인라인 one-liner#

command에 셸 명령을 직접 적습니다. jq를 활용하면 간단한 표시는 한 줄로 충분합니다.

1
2
3
4
5
6
{
"statusLine": {
"type": "command",
"command": "jq -r '[\"[\", .model.display_name, \"] \", (.context_window.used_percentage // 0 | floor | tostring), \"% context\"] | join(\"\")'"
}
}

장점: 파일을 따로 만들 필요 없이 바로 시작할 수 있습니다.
단점: 명령이 길어지면 settings.json이 지저분해지고, 색상 처리나 조건 분기를 넣기 어렵습니다.

방법 B: 스크립트 파일#

command에 스크립트 경로를 적고, 로직은 별도 파일에 작성합니다.

1
2
3
4
5
6
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline.sh"
}
}

스크립트 파일(~/.claude/statusline.sh)을 만들고 실행 권한을 부여합니다:

1
chmod +x ~/.claude/statusline.sh

장점: 로직이 길어도 관리하기 좋고, 색상·조건 분기·여러 섹션을 깔끔하게 구성할 수 있습니다. Git으로 버전 관리도 됩니다. 샘플 JSON으로 독립적인 테스트도 가능합니다.
단점: 파일을 별도로 만들고 관리해야 합니다.

섹션이 8개 이상인 경우라면 스크립트 방식이 거의 유일한 선택지입니다.

/statusline 자동 생성 헬퍼#

자연어로 설명하면 Claude Code가 스크립트를 자동 생성하고 settings.json까지 업데이트해 줍니다:

1
/statusline show model name and context percentage with a progress bar

이것은 별개의 설정 방식이 아니라, 위의 “스크립트 파일 방식”을 자동화해 주는 헬퍼입니다.


내 statusline 구성#

최종 출력 형태#

1
fable 5 ~/L/M/i/Documents/PKM ⎇ main 24.8k [░░░░░░░░] 2% · ↻3h50m ↑32%  ↻4d9h ↑3% $10.32
항목 예시 색상
모델명 (축약) fable 5 cyan
현재 경로 (fish 스타일 축약) ~/L/M/i/Documents/PKM yellow
Git 브랜치 ⎇ main green
토큰 사용량 24.8k blue
컨텍스트 사용률 바 + % [░░░░░░░░] 2% 사용률에 따라 green/yellow/red
현재 세션 한도 ↻3h50m ↑32% 리셋시간 cyan, 사용률 구간별 색
주간 한도 ↻4d9h ↑3% 리셋시간 yellow, 사용률 구간별 색
세션 비용 $10.32 magenta

섹션별 구현 의도#

모델명 축약#

model.idsed 정규식으로 claude-sonnet-4-6sonnet-4.6 형태로 줄입니다. 패턴 매칭에 실패하면(새 네이밍 등) display_name을 소문자로 변환해 폴백합니다.

1
2
3
4
5
6
7
8
9
abbr=$(echo "$model_id" | sed -E \
's/claude-([a-z]+)-([0-9]+)-([0-9]+).*/\1-\2.\3/; \
s/claude-([a-z]+-[a-z]+)-([0-9]+)-([0-9]+).*/\1-\2.\3/')

if [ -z "$abbr" ] || [ "$abbr" = "$model_id" ]; then
abbr=$(echo "$model_display" | sed -E \
's/Claude //; s/ ([0-9]+)\.([0-9]+).*/\1.\2/' | \
tr '[:upper:]' '[:lower:]')
fi

모델명이 길면 statusline이 금방 꽉 차므로 최대한 짧게 유지하는 것이 목표입니다. claude-fable-5처럼 새 네이밍은 폴백 경로를 타서 fable 5로 표시됩니다.

fish shell 스타일 경로 축약#

$HOME~로 치환한 뒤, awk로 마지막 2개 세그먼트만 전체 표시하고 나머지는 첫 글자만 남깁니다.

1
2
~/Library/Mobile Documents/iCloud~md~obsidian/Documents/PKM
→ ~/L/M/i/Documents/PKM

iCloud 볼트처럼 경로가 긴 환경에서 공간을 아끼기 위한 처리입니다. fish shell에서 영감을 받았습니다.

Git 브랜치#

1
2
git_branch=$(git -C "$cwd" -c core.hooksPath=/dev/null \
symbolic-ref --short HEAD 2>/dev/null)

-c core.hooksPath=/dev/null을 추가해 git 훅 실행을 차단합니다. statusline은 자주 호출되는데, 훅이 실행되면 의도치 않은 부작용이 생길 수 있습니다. detached HEAD 상태면 rev-parse --short HEAD로 커밋 해시를 대신 표시합니다. git 저장소가 아닌 디렉토리라면 섹션 자체를 생략합니다.

토큰 사용량#

입력 + 출력 토큰을 합산해 1,000 이상이면 24.8k처럼 k 단위 소수점 1자리로 포맷합니다.

1
2
3
4
5
6
total_tokens=$(( total_in + total_out ))
if [ "$total_tokens" -ge 1000 ]; then
token_str=$(awk "BEGIN {printf \"%.1fk\", $total_tokens/1000}")
else
token_str="${total_tokens}"
fi

컨텍스트 사용률 프로그레스 바#

used_percentage를 8칸 블록 바로 변환합니다. 색상은 사용률 구간별로 다르게 줍니다:

  • 0~60%: green [████████]
  • 61~80%: yellow
  • 81%+: red
1
2
3
4
5
6
7
if [ "$pct_int" -le 60 ]; then
bar_color="$C_GREEN"
elif [ "$pct_int" -le 80 ]; then
bar_color="$C_YELLOW"
else
bar_color="$C_RED"
fi

Rate limit (현재 세션·주간)#

resets_at(Unix epoch)에서 현재 시각을 빼 남은 시간을 ↻3h50m, ↻4d9h 형태로 표시하고, 사용률을 ↑32%로 붙입니다.

1
2
3
4
5
6
7
8
format_remaining() {
_rem=$(( $1 - $(date +%s) ))
[ "$_rem" -lt 0 ] && _rem=0
if [ "$_rem" -ge 86400 ]; then printf '%dd%dh' $(( _rem/86400 )) $(( _rem%86400/3600 ))
elif [ "$_rem" -ge 3600 ]; then printf '%dh%02dm' $(( _rem/3600 )) $(( _rem%3600/60 ))
else printf '%dm' $(( _rem/60 ))
fi
}

사용률 색상은 컨텍스트 바와 동일한 60/80% 기준을 usage_color 함수로 공유해 일관성을 유지합니다. rate_limits 필드 자체가 없을 수 있으므로(API 키 직접 사용 시 등) // empty로 처리하고 필드가 없으면 rate limit 섹션을 통째로 생략합니다.

세션 비용#

cost.total_cost_usd를 소수점 2자리로 포맷해 $10.32 형태로 표시합니다. magenta 색상. 비용 필드가 없으면 섹션을 생략합니다.

1
2
3
4
5
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
if [ -n "$cost_usd" ]; then
cost_str=$(awk "BEGIN {printf \"%.2f\", $cost_usd}")
cost_part=$(printf " ${C_MAGENTA}\$%s${C_RESET}" "$cost_str")
fi

설계 원칙#

  1. 가볍게: statusline은 자주 실행됩니다. 외부 의존성은 jq, awk, sed, git 정도로 제한합니다.
  2. 방어적으로: 모든 필드 추출에 // empty 또는 // 0 폴백을 둡니다. 필드가 없는 환경(버전 차이, API 키 직접 사용 등)에서도 오류 없이 동작하고, 해당 섹션만 자연스럽게 생략됩니다.
  3. 일관성: 색상 구간 규칙(60%/80% 경계)을 컨텍스트 바와 rate limit이 usage_color 함수로 공유합니다.

전체 스크립트#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/bin/sh
# Claude Code status line script — developer edition

input=$(cat)

# ANSI color codes
C_CYAN='\033[36m'
C_YELLOW='\033[33m'
C_GREEN='\033[32m'
C_RED='\033[31m'
C_BLUE='\033[34m'
C_MAGENTA='\033[35m'
C_RESET='\033[0m'

# ── 1. Model name (abbreviated) ──────────────────────────────────────────────
model_id=$(echo "$input" | jq -r '.model.id // ""')
model_display=$(echo "$input" | jq -r '.model.display_name // ""')

# Build abbreviated name: extract family + version number
# Examples: claude-sonnet-4-5 → sonnet-4.5, claude-opus-4-6 → opus-4.6
abbr=$(echo "$model_id" | sed -E \
's/claude-([a-z]+)-([0-9]+)-([0-9]+).*/\1-\2.\3/; \
s/claude-([a-z]+-[a-z]+)-([0-9]+)-([0-9]+).*/\1-\2.\3/')

# Fallback to display name if abbr didn't change or is empty
if [ -z "$abbr" ] || [ "$abbr" = "$model_id" ]; then
abbr=$(echo "$model_display" | sed -E \
's/Claude //; s/ ([0-9]+)\.([0-9]+).*/\1.\2/' | \
tr '[:upper:]' '[:lower:]')
fi
[ -z "$abbr" ] && abbr="unknown"

model_part=$(printf "${C_CYAN}%s${C_RESET}" "$abbr")

# ── 2. Path (fish shell style: last 2 full, rest first-char only) ─────────────
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
if [ -n "$cwd" ]; then
home="$HOME"
cwd_display="${cwd#$home}"
if [ "$cwd_display" != "$cwd" ]; then
cwd_display="~$cwd_display"
fi

abbreviated=$(echo "$cwd_display" | awk '
{
n = split($0, segs, "/")
# Identify prefix and actual segments
prefix = ""
start = 1
if (segs[1] == "") { prefix = "/"; start = 2 }

# Collect non-empty segments
count = 0
for (i = start; i <= n; i++) {
if (segs[i] != "") {
count++
parts[count] = segs[i]
}
}

out = prefix
for (i = 1; i <= count; i++) {
# Keep last 2 segments full; abbreviate the rest to first char
if (count - i >= 2) {
seg = substr(parts[i], 1, 1)
} else {
seg = parts[i]
}
if (out == "" || out == "/") out = out seg
else out = out "/" seg
}
print out
}')

path_part=$(printf " ${C_YELLOW}%s${C_RESET}" "$abbreviated")
else
path_part=""
fi

# ── 3. Git branch ────────────────────────────────────────────────────────────
git_branch=""
if [ -n "$cwd" ]; then
git_root=$(git -C "$cwd" -c core.hooksPath=/dev/null \
rev-parse --show-toplevel 2>/dev/null)
if [ -n "$git_root" ]; then
git_branch=$(git -C "$cwd" -c core.hooksPath=/dev/null \
symbolic-ref --short HEAD 2>/dev/null)
if [ -z "$git_branch" ]; then
git_branch=$(git -C "$cwd" -c core.hooksPath=/dev/null \
rev-parse --short HEAD 2>/dev/null)
fi
if [ -n "$git_branch" ]; then
GIT_ICON=$(printf '\xe2\x8e\x87')
branch_part=$(printf " ${C_GREEN}%s %s${C_RESET}" "$GIT_ICON" "$git_branch")
fi
fi
fi
[ -z "$git_branch" ] && branch_part=""

# ── 5. Token usage (k, 1 decimal) ────────────────────────────────────────────
total_in=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
total_out=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
total_tokens=$(( total_in + total_out ))
if [ "$total_tokens" -ge 1000 ]; then
token_str=$(awk "BEGIN {printf \"%.1fk\", $total_tokens/1000}")
else
token_str="${total_tokens}"
fi
token_part=$(printf " ${C_BLUE}%s${C_RESET}" "$token_str")

# ── 6. Context usage progress bar (8 blocks) + percentage ────────────────────
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
BLOCK_FULL=$(printf '\xe2\x96\x88')
BLOCK_EMPTY=$(printf '\xe2\x96\x91')
BARS=8

if [ -n "$used_pct" ]; then
pct_int=$(printf "%.0f" "$used_pct")
filled=$(awk "BEGIN {n=int($used_pct*$BARS/100+0.5); if(n>$BARS)n=$BARS; print n}")
empty=$(( BARS - filled ))

bar=""
i=0
while [ "$i" -lt "$filled" ]; do
bar="${bar}${BLOCK_FULL}"
i=$((i + 1))
done
i=0
while [ "$i" -lt "$empty" ]; do
bar="${bar}${BLOCK_EMPTY}"
i=$((i + 1))
done

# Color: 0-60% green, 61-80% yellow, 81%+ red
if [ "$pct_int" -le 60 ]; then
bar_color="$C_GREEN"
elif [ "$pct_int" -le 80 ]; then
bar_color="$C_YELLOW"
else
bar_color="$C_RED"
fi

ctx_part=$(printf " ${bar_color}[%s]${C_RESET} %s%%" "$bar" "$pct_int")
else
empty_bar=""
i=0
while [ "$i" -lt "$BARS" ]; do
empty_bar="${empty_bar}${BLOCK_EMPTY}"
i=$((i + 1))
done
ctx_part=$(printf " ${C_GREEN}[%s]${C_RESET} --" "$empty_bar")%%
fi

# ── 7·8. Rate limits (5h session + 7d weekly) ────────────────────────────────
usage_color() {
_pct=$(printf "%.0f" "$1")
if [ "$_pct" -le 60 ]; then printf '%s' "$C_GREEN"
elif [ "$_pct" -le 80 ]; then printf '%s' "$C_YELLOW"
else printf '%s' "$C_RED"
fi
}

format_remaining() {
_rem=$(( $1 - $(date +%s) ))
[ "$_rem" -lt 0 ] && _rem=0
if [ "$_rem" -ge 86400 ]; then printf '%dd%dh' $(( _rem/86400 )) $(( _rem%86400/3600 ))
elif [ "$_rem" -ge 3600 ]; then printf '%dh%02dm' $(( _rem/3600 )) $(( _rem%3600/60 ))
else printf '%dm' $(( _rem/60 ))
fi
}

session_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
session_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
weekly_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
weekly_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')

make_rate_piece() {
_color="$1" _pct="$2" _reset="$3"
[ -z "$_pct" ] && return
_used=$(printf "%.0f" "$_pct")
_ucolor=$(usage_color "$_pct")
_time_str=""
[ -n "$_reset" ] && _time_str=$(printf "${_color}↻%s " "$(format_remaining "$_reset")")
printf "%s${_ucolor}↑%d%%${C_RESET}" "$_time_str" "$_used"
}

sess_piece=$(make_rate_piece "$C_CYAN" "$session_pct" "$session_reset")
week_piece=$(make_rate_piece "$C_YELLOW" "$weekly_pct" "$weekly_reset")

rate_part=""
if [ -n "$sess_piece" ] && [ -n "$week_piece" ]; then
rate_part=$(printf " · %s %s" "$sess_piece" "$week_piece")
elif [ -n "$sess_piece" ]; then
rate_part=$(printf " · %s" "$sess_piece")
elif [ -n "$week_piece" ]; then
rate_part=$(printf " · %s" "$week_piece")
fi

# ── 9. Session cost (USD) ────────────────────────────────────────────────────
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
cost_part=""
if [ -n "$cost_usd" ]; then
cost_str=$(awk "BEGIN {printf \"%.2f\", $cost_usd}")
cost_part=$(printf " ${C_MAGENTA}\$%s${C_RESET}" "$cost_str")
fi

# ── Output ────────────────────────────────────────────────────────────────────
printf "%s%s%s%s%s%s%s\n" \
"$model_part" \
"$path_part" \
"$branch_part" \
"$token_part" \
"$ctx_part" \
"$rate_part" \
"$cost_part"

테스트 방법#

스크립트를 수정하거나 새로 만들 때는 샘플 JSON을 파이프로 넘겨서 Claude Code를 열지 않고도 검증할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
echo '{
"model": {"id": "claude-sonnet-4-6", "display_name": "Sonnet 4.6"},
"workspace": {"current_dir": "/Users/yun/projects/myapp"},
"context_window": {"total_input_tokens": 24000, "total_output_tokens": 800, "used_percentage": 12.5},
"cost": {"total_cost_usd": 0.42},
"rate_limits": {
"five_hour": {"used_percentage": 32, "resets_at": 1750000000},
"seven_day": {"used_percentage": 3, "resets_at": 1750500000}
},
"session_id": "test-session-abc"
}' | bash ~/.claude/statusline.sh

마치며#

statusline 커스텀의 핵심 효용은 컨텍스트·비용·rate limit을 Claude와 대화하면서 항상 시야에 두는 것입니다. 컨텍스트가 80%를 넘으면 바 색깔이 노란색으로 바뀌고, 90%를 넘으면 빨간색이 됩니다. /compact를 언제 해야 할지 판단하거나, 요금이 얼마나 나왔는지 확인하거나, rate limit 소진 속도를 체크하는 데 유용합니다.

더 다양한 statusline 구성이 궁금하다면 커뮤니티 프로젝트인 ccstatusline이나 starship-claude도 참고할 만합니다.