
Claude Code (터미널)를 위한 커스텀 상태 표시줄(statusline) 제작 — 디자인 공유
요약
Claude Code 터미널 환경에서 사용할 수 있는 커스텀 상태 표시줄(statusline) 제작 가이드와 디자인을 공유합니다. 사용자 정보, Git 브랜치, 모델 상태, 컨텍스트 및 사용량 제한 등을 한눈에 확인할 수 있는 UI 구성법을 다룹니다.
핵심 포인트
- Claude Code v2.1.220 기준 커스텀 상태 표시줄 구현 방법
- 사용자, 프로젝트, Git 브랜치, 모델 및 사고 수준 표시
- 컨텍스트 사용량 및 시간별 사용 제한 게이지 시각화
- 프롬프트를 이용한 설정 및 테마의 간편한 복제 방법
안녕하세요 여러분, 터미널 내 Claude Code를 위한 작은 커스텀 상태 표시줄(statusline)을 제작하여 이곳에 공유하고자 합니다. 거창한 것은 아니며, 제 개인적인 워크플로우를 위해 만든 것이지만 다른 분들에게도 유용하거나 시도해 볼 만한 재미있는 요소가 될 것 같아 가져왔습니다. 이 상태 표시줄은 사용자 이름, 프로젝트/디렉토리, 현재 git 브랜치, 활성화된 모델 및 해당 모델의 사고 수준(thinking level), 권한 우회(bypass permissions) 활성화 여부, 그리고 현재 실행 중인 서브 에이전트(sub-agents)의 수를 보여줍니다. 그 아래에는 세 개의 바(bar)가 있습니다: 컨텍스트 사용량(context usage), 5시간 사용 제한, 그리고 7일 사용 제한(초기화 날짜 포함)입니다. 또한 토큰 사용량과 현재 작업이 표시된 서브 에이전트의 실시간 목록도 제공합니다. 획기적인 것은 아니지만, 별도의 명령어를 실행할 필요 없이 모든 것을 한눈에 파악할 수 있는 방법입니다. 혹시 본인의 설정에 맞춰 수정하고 싶은 분들을 위해 제가 사용한 프롬프트/설정(config)을 아래에 게시합니다. 피드백이나 개선을 위한 아이디어가 있다면 기쁘게 듣겠습니다. https://preview.redd.it/wkkhsurci5hh1.png?width=1940&format=png&auto=webp&s=b70479bdc0b9590ae9ad8731ba596065939a5675 # Claude Code 상태 표시줄 + 에이전트 디스플레이 — 복제 가이드 작동 중인 macOS 머신에서 Claude Code 입력창 아래에 렌더링되는 모든 요소와, 이를 다른 컴퓨터에서 바이트 단위로 동일하게 재현하는 방법입니다. 2026-08-03 기준 작동 중인 머신에서 캡처되었으며, Claude Code 버전은 v2.1.220 입니다. > 이 파일을 프롬프트로 사용하기. 이 파일 전체를 새 머신으로 복사하거나(또는 해당 리포지토리를 클론한 후) 다음 내용을 Claude Code에 붙여넣으세요: > > > 리포지토리 루트에 있는 statusline.md를 읽고, 내 Claude Code 상태 표시줄, 서브 에이전트 상태 표시줄, 그리고 커스텀 테마를 설명된 내용과 정확히 일치하게 설정해 줘. 주어진 내용과 정확히 일치하는 파일들을 생성하고, 기존에 내가 가지고 있는 키들을 누락시키지 않으면서 settings.json의 키들을 나의 기존 ~/.claude/settings.json에 병합해 줘. 그런 다음 마지막에 있는 검증 명령어를 실행하고 렌더링된 출력을 보여줘. > > > 에이전트가 필요로 하는 모든 것은 이 파일에 들어 있습니다 — 다른 머신에 접속할 필요가 없습니다. --- ## 1.
외형
입력창 아래에 두 줄이 위치하며, 각 줄은 전체 너비의 어두운 띠(bg 235)로 그려집니다. 띠의 왼쪽 가장자리에는 세 개의 게이지 중 가장 상태가 좋지 않은 색상을 띤 유색 왼쪽 가장자리 글리프(▌)가 표시됩니다:
▌ youruser | PMS | main | Opus 5 [high] | BYPASS 2 AGENTS
▌ ━━╸──── 41% | ━╸───── 32% (3h12m left) | ─────── 18% (resets Mon Aug 10)
(구분선은 박스 그리기 글리프가 아닌, 어두운 회색의 일반 | 기호입니다.)
1행 — identity (식별 정보): OS 사용자 이름 (cyan) · 프로젝트 폴더 (blue) · git 브랜치 (green) · 노력 수준(effort level)이 포함된 모델 이름 (white) · 배지(badges).
2행 — gauges (게이지): 항상 다음 순서를 따르며, 각 게이지는 7칸의 굵은 선 바(heavy-line bar)와 퍼센트(%)로 구성됩니다:
| 게이지 | 노란색 전환 | 빨간색 전환 | 접미사 (Suffix) |
|---|---|---|---|
| context | 20% | 30% | — |
| 5h limit | 50% | 80% | (3h12m left) — 리셋까지 남은 시간 |
| 7d limit | 50% | 80% | (resets Mon Aug 10) — 리셋 날짜 |
컨텍스트(context) 임계값은 의도적으로 공격적(20/30)으로 설정되었습니다. 이는 "거의 가득 찼음"을 알리는 신호가 아니라, 압축(compaction)이 곧 진행될 것임을 알리는 **조기 경보 (early warning)**입니다. 속도 제한(rate-limit) 임계값은 관례적인 50/80을 따릅니다.
Badges (배지): 1행에 유색 칩 형태로 렌더링되며, 해당되는 경우에만 표시됩니다:
BYPASS/AUTO-EDIT/PLAN— 권한 모드 (permission mode)FAST— 빠른 모드 (fast mode) 활성화N AGENTS— 실행 중인 서브에이전트(subagent) 수 (§4의 서브에이전트 상태 표시줄 참조)agent:<name>,style:<name>, vim 모드,PR #123(리뷰 상태에 따라 색상 지정)
Agent panel rows (에이전트 패널 행). 에이전트 패널의 각 서브에이전트는 상태 색상이 입혀진 가장자리를 가진 일치하는 띠를 가집니다:
▌ Explore · ━╸───── 28% · 14.2k tok · 1m3s · [high] · search for the relay handler
가장자리 색상: 빨간색 = 실패/취소(failed/cancelled), 초록색 = 완료(completed), 노란색 = 대기/큐(pending/queued), 흰색 = 실행 중(running). 여유 공간을 위해 마지막 에이전트 행 뒤에는 빈 줄이 추가됩니다.
2.
생성할 파일: ~/.claude/ 아래에 세 개의 파일을 생성합니다: | 파일 | 용도 | | ------------------------- | ------------------------------------------------------------- | | statusline.js | 입력창 아래의 두 줄 상태 표시줄 (status line) | | subagent-statusline.js | 에이전트 패널 내 에이전트별 행 본문 + 에이전트 수 피드 | | themes/pms.json | 호박색(amber) 액센트가 적용된 커스텀 다크 테마 | 모든 파일은 의존성 없는 Node (child_process, fs, os, path만 사용) 환경에서 작동하며, jq나 npm install이 필요하지 않습니다. PATH에 Node ≥ 18 버전이 설정되어 있어야 합니다. > 소스 머신에는 jq 기반의 bash 버전인 레거시 ~/.claude/statusline-command.sh가 존재합니다. 이는 settings.json에서 참조되지 않는 대체된(superseded) 파일이므로, 이를 포팅하지 마세요. ---
3. ~/.claude/statusline.js
이 파일을 정확히 생성하세요:
#!/usr/bin/env node
'use strict';
// Claude Code 상태 표시줄 (status line) — 세 줄 구성, 의존성 없음.
// 1행: 사용자 이름 | 프로젝트 폴더 | git 브랜치 | 모델 이름 [effort] | 배지 (badges)
// 2행: context | 5h | 7d — 각 항목은 라벨이 없는 7자 막대 + 백분율 순서로 표시됨.
// context: 노란색 >=20%, 빨간색 >=30% (조기 경고)
// 5h / 7d: 노란색 >=50%, 빨간색 >=80%
// 5h는 리셋까지 남은 시간을 표시하며, 7d는 리셋 날짜를 표시함.
const { execSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function safe(fn, fallback) {
try {
const v = fn();
return v === undefined || v === null ? fallback : v;
} catch (e) {
return fallback;
}
}
let raw = '';
try {
raw = fs.readFileSync(0, 'utf8');
} catch (e) {
raw = '';
}
let input = {};
try {
input = raw ?
JSON.parse(raw) : {}; } catch (e) { input = {}; } const RESET = '\x1b[0m'; const DIM = '\x1b[2m'; const CYAN = '\x1b[36m'; const WHITE = '\x1b[37m'; const BLUE = '\x1b[34m'; const GREEN = '\x1b[32m'; const YELLOW = '\x1b[33m'; const RED = '\x1b[31m'; // 어두운 회색 (256-colour) + dim, 따라서 구분선이 일반 DIM보다 더 뒤로 물러나 보이게 함 const GREY = '\x1b[38;5;240m'; const SEP = `${DIM}${GREY}|${RESET}`; const BG = '\x1b[48;5;235m'; // 하네스(harness)는 TTY 없이 실행되며 (process.stdout.columns가 null임), status JSON에 너비를 전달하지 않으므로 COLUMNS가 우리가 얻을 수 있는 유일한 너비 신호입니다. // 만약 이 값이 누락되었거나 비현실적이라면, 상태 표시줄이 추가 행으로 줄바꿈되어 너무 긴 줄을 출력하는 위험을 감수하기보다 밴드(band) 전체를 건너뜁니다. const cols = parseInt(process.env.COLUMNS, 10); const useBand = Number.isFinite(cols) && cols >= 20 && cols <= 1000; function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); } // 한 행을 그립니다: 색상이 있는 왼쪽 가장자리, 그 다음 전체 너비까지 이어지는 어두운 밴드. // 콘텐츠 내부의 모든 RESET은 배경색도 지우기 때문에, 각 RESET 직후에 BG로 다시 설정합니다. function band(content, edgeColor) { if (!useBand) return content; const inner = cols - 2; // 가장자리 글리프(glyph)용 1열, 앞쪽 공백용 1열 const visible = stripAnsi(content).length; if (visible > inner) return content; // 안전하게 패딩(padding)하기에는 너무 김 — 그대로 둡니다 const body = content.replace(/\x1b\[0m/g, RESET + BG); return `${edgeColor}▌${RESET}${BG} ${body}${' '.repeat(inner - visible)}${RESET}`; } // --- 사용자 이름 (OS user) --- function getUsername() { return safe(() => os.userInfo().username, ''); } // --- 프로젝트 폴더 --- const cwd = input.cwd || safe(() => input.workspace.current_dir, '') || process.cwd(); const projectDir = cwd ?
path.basename(cwd) : ''; // --- git branch + dirty-file count --- function getGitInfo(dir) { if (!dir) return ''; const opts = { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] }; let branch = ''; try { branch = execSync('git --no-optional-locks rev-parse --abbrev-ref HEAD', opts) .toString() .trim(); } catch (e) { return ''; } return branch; } // --- model name + effort --- // strip any trailing parenthetical (e.g. "Opus 5 (1M context)" -> "Opus 5") const modelName = safe(() => input.model.display_name, '').replace(/\s*\([^)]*\)\s*$/, '').trim(); const effort = safe(() => input.effort.level, ''); const modelText = modelName ? (effort ? `${modelName} [${effort}]` : modelName) : ''; // --- progress bar (7 cells) --- function renderBar(pct, yellowAt, redAt) { if (pct === null || pct === undefined || pct === '') return ''; let p = Math.round(Number(pct)); if (Number.isNaN(p)) return ''; if (p < 0) p = 0; if (p > 100) p = 100; const width = 7; const filled = Math.round((p * width) / 100); const empty = width - filled; let color = GREEN; if (p >= redAt) color = RED; else if (p >= yellowAt) color = YELLOW; // heavy line with a rounded cap on the leading edge, over a dim track let bar; if (filled === 0) { bar = `${DIM}${'─'.repeat(width)}${RESET}`; } else if (filled >= width) { bar = `${color}${'━'.repeat(width)}${RESET}`; } else { bar = `${color}${'━'.repeat(filled - 1)}╸${RESET}` + `${DIM}${'─'.repeat(empty)}${RESET}`; } return `${bar} ${p}%`; } // --- badges --- function badge(text, fg, bg) { return `\x1b[38;5;${fg};48;5;${bg}m ${text} ${RESET}`; } // The status JSON carries no permission mode, so read it from the launch args of // the nearest exttt{claude} ancestor. Cached per session: process args never change, // so this cannot go stale for a flag-launched session — but it also means a // mid-session Shift+Tab change is NOT reflected.
function getPermissionMode(sessionId) { const cacheFile = path.join(os.tmpdir(), `statusline-permmode-${sessionId || 'nosession'}`); try { return fs.readFileSync(cacheFile, 'utf8'); } catch (e) { /* 캐시 미스 — 다음 로직 실행 */ } let mode = ''; let read = false; try { const out = execSync('ps -axo pid=,ppid=,args=', { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 8 * 1024 * 1024, }).toString(); read = true; const procs = new Map(); out.split('\n').forEach((line) => { const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/); if (m) procs.set(Number(m[1]), { ppid: Number(m[2]), args: m[3] }); }); let pid = process.ppid; for (let i = 0; i < 10 && pid && procs.has(pid); i += 1) { const proc = procs.get(pid); if (/--dangerously-skip-permissions/.test(proc.args)) { mode = 'bypassPermissions'; break; } const m = proc.args.match(/--permission-mode[= ]+(\S+)/); if (m) { mode = m[1]; break; } pid = proc.ppid; } } catch (e) { mode = ''; } // 실제로 읽은 경우에만 캐시 — 일시적인 ps 오류 후 빈 문자열을 캐싱하면 세션 나머지 기간 동안 배지 표시가 숨겨질 수 있음. if (read) { try { fs.writeFileSync(cacheFile, mode); } catch (e) { /* 무시 */ } } return mode; } // 서브 에이전트 개수, subagent-statusline.js에서 작성됨. 이 명령어는 에이전트 패널이 보이는 동안에만 실행되므로, STALE_MS보다 오래된 읽기는 에이전트가 완료되었음을 의미하며 카운트를 표시해서는 안 됨. function getAgentCount(sessionId) { const STALE_MS = 12000; try { const f = path.join(os.tmpdir(), `statusline-subagents-${sessionId || 'nosession'}.json`); const d = JSON.parse(fs.readFileSync(f, 'utf8')); if (!d || typeof d.count !== 'number' || d.count <= 0) return 0; if (Date.now() - d.ts > STALE_MS) return 0; return d.count; } catch (e) { return 0; } } // 게이지의 심각도: 0은 녹색, 1은 노란색, 2는 빨간색, 읽기가 없을 때는 null.
function barState(pct, yellowAt, redAt) { if (pct === null || pct === undefined || pct === '') return null; const p = Math.round(Number(pct)); if (Number.isNaN(p)) return null; if (p >= redAt) return 2; if (p >= yellowAt) return 1; return 0; } // --- resets_at helpers (unix seconds/ms or ISO-8601 string) --- function toEpochMs(v) { if (v === null || v === undefined || v === '') return null; if (typeof v === 'number') { return v > 1e12 ? v : v * 1000; } const parsed = Date.parse(v); return Number.isNaN(parsed) ? null : parsed; } function formatTimeLeft(resetsAt) { const ms = toEpochMs(resetsAt); if (ms === null) return ''; const diff = ms - Date.now(); if (diff <= 0) return ''; const days = Math.floor(diff / 86400000); const hours = Math.floor((diff % 86400000) / 3600000); const mins = Math.floor((diff % 3600000) / 60000); if (days > 0) return `${days}d${hours}h left`; if (hours > 0) return `${hours}h${mins}m left`; return `${mins}m left`; } function formatResetDate(resetsAt) { const ms = toEpochMs(resetsAt); if (ms === null) return ''; const d = new Date(ms); const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; return `resets ${days[d.getDay()]} ${months[d.getMonth()]} ${d.getDate()}`; } // --- Row 1 --- const row1Parts = []; const username = getUsername(); if (username) row1Parts.push(`${CYAN}${username}${RESET}`); if (projectDir) row1Parts.push(`${BLUE}${projectDir}${RESET}`); const gitInfo = getGitInfo(cwd); if (gitInfo) row1Parts.push(`${GREEN}${gitInfo}${RESET}`); if (modelText) row1Parts.push(`${WHITE}${modelText}${RESET}`); // --- status badges --- const sessionId = safe(() => input.session_id, ''); const ba
AI 자동 생성 콘텐츠
본 콘텐츠는 r/ClaudeAI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기