이번 시간에는 Bash 명령의 출력을 줄여 준다는 Claude Code 플러그인 Token Optimizer가 실제로 토큰을 줄이는지 claude -p로 측정하며 배워가는 시간입니다. claude -p는 대화 화면 없이 질문 하나를 보내고 결과를 받는 실행 방식이에요.
큰 출력이 나오는 명령을 정해 준 과제와, 명령을 Claude에게 맡긴 과제를 도구를 켠 조건과 끈 조건으로 번갈아 보내 볼게요. 모델 출력은 비결정적이라 다시 돌리면 숫자가 달라질 수 있고, --model opus가 가리키는 모델도 시간이 지나면 달라질 수 있어요.
Token Optimizer가 줄이는 것과 공개 수치
Token Optimizer는 hook(Claude Code가 도구 실행 앞뒤에 부르는 스크립트)으로 된 플러그인이에요. Bash 명령은 그대로 실행하고, 끝난 뒤 결과를 요약으로 바꿔 Claude에게 넘기며, 원본은 디스크에 보관해 expand 명령으로 다시 꺼내게 합니다. 라이선스는 PolyForm Noncommercial 1.0.0에 소규모 사업 허가를 더한 것이라, 상업적으로 쓰려면 조건을 따로 확인해야 해요.
공식 설치는 Claude Code 안에서 아래 두 명령입니다.
/plugin marketplace add alexgreensh/token-optimizer
/plugin install token-optimizer@alexgreensh-token-optimizer
저자의 통제 A/B(조건마다 20번, Haiku 4.5)는 git status/diff/log와 ls -la 과제에서 토큰 −11%, 480건을 찾는 grep 과제에서 −15.7%, 줄일 것이 없는 짧은 세션에서 +~160토큰이라고 밝혔고, "This is not a 'Token Optimizer saves X% in every session' number"라고 적었어요. 그건 그들의 과제와 모델로 잰 수치이니 우리 기준에서 직접 확인해 볼게요.
실습 준비
이 글의 명령은 모두 실습 코드의 token-optimizer-bash-hooks/ 디렉터리에서 실행합니다.
Dockerfile,wrap.sh: Token Optimizer v5.13.57을 넣은 이미지to-lab:5.13.57과, 컨테이너 안의 hook 진입점to-wrapmake_settings.py: 플러그인의 hook을 모두docker exec로 감싼work/.claude/settings.json을 만드는 스크립트gen_project.py: 고정 시드로 Python 패키지와 git 기록(work/), 정답(truth.json)을 만드는 스크립트lab.sh: 컨테이너를 띄우고 지우는 함수와claude -p를 부르는 함수 모음prompt-cmd.txt,prompt-free.txt,schedule.txt,run.sh: 두 과제의 질문과, 섞은 순서로 30번 호출하는 스크립트summarize.py,calls.py: 결과를 조건별로 모으는 스크립트와 호출 하나의 도구 결과를 보여 주는 스크립트README.md: 파일 목록
따라 하시려면 Claude Code 2.1.309와 구독 로그인, Docker, jq, macOS 기본 python3·git(Xcode Command Line Tools)이 필요해요. Claude가 실행하는 명령은 Mac에서 그대로 돌기 때문에, 테스트는 Mac에 기본으로 없는 pytest 대신 표준 라이브러리 unittest로 만들었습니다.
측정에서는 플러그인을 Mac에 설치하지 않았어요. 플러그인이 등록하는 hook 16개를 실습 디렉터리의 .claude/settings.json에 옮겨 적고, 각 hook을 컨테이너 안에서 실행합니다.
이미지부터 만들게요. 빌드 기록은 길어서 생략합니다.
docker build -t to-lab:5.13.57 .
이미지에서 플러그인의 hook 목록을 꺼내고, 실습 프로젝트와 도구 조건의 설정을 만들어요.
docker run --rm to-lab:5.13.57 cat /opt/to/hooks/hooks.json > hooks.json
python3 gen_project.py
python3 make_settings.py tool
jq -c '.hooks.PostToolUse[0]' work/.claude/settings.json
work/: 커밋 161개, 테스트 파일 8개
{"commits": 161, "culprit": "b76ceae", "function": "bulk_discount", "pricing_asserts": 31, "shop_defs": 48, "bulk_50": 13500}
work/.claude/settings.json: tool, 이벤트 11개, hook 16개
{"matcher":"Bash|PowerShell|Read|Glob|Grep|Agent|Edit|Write|MultiEdit|NotebookEdit|mcp__.*","hooks":[{"type":"command","command":"docker exec -i -e TO_LAB_LOG -e CLAUDE_PROJECT_DIR -w \"${CLAUDE_PROJECT_DIR:-/}\" to-lab to-wrap tool hooks/posttooluse_runner.py","timeout":10}]}
matcher와 timeout은 플러그인 원본 그대로이고, 명령만 docker exec로 바뀌었어요. 기준 조건은 make_settings.py pass로 만들어요. 같은 hook, 같은 컨테이너를 거치지만 to-wrap pass는 hook 입력을 읽고 아무것도 내지 않아서, 두 조건은 Token Optimizer가 도는지만 달라요.
lab.sh의 앞부분은 이렇습니다(발췌).
LAB="$PWD"
WORK="$LAB/work"
PLUGIN="$LAB/plugin/token-optimizer/5.13.57"
TRANSCRIPTS="$HOME/.claude/projects/$(printf '%s' "$WORK" | sed 's/[^A-Za-z0-9]/-/g')"
to_start() {
docker rm -f to-lab > /dev/null 2>&1
mkdir -p "$TRANSCRIPTS" "$LAB/out/hooks" "$PLUGIN/skills/token-optimizer/scripts"
docker run -d --name to-lab --label to-lab=1 --network none \
-e CLAUDE_PLUGIN_ROOT="$PLUGIN" \
-v "$WORK:$WORK" -v "$LAB/out/hooks:$LAB/out/hooks" -v "$TRANSCRIPTS:$TRANSCRIPTS:ro" \
to-lab:5.13.57 sleep infinity > /dev/null
docker exec to-lab sh -c "mkdir -p '$PLUGIN' && cp -a /opt/to/. '$PLUGIN/'"
printf '%s\n' 'import os, subprocess, sys' \
'sys.exit(subprocess.call(["docker", "exec", "-i", "to-lab", "python3", os.path.abspath(__file__), *sys.argv[1:]]))' \
> "$PLUGIN/skills/token-optimizer/scripts/measure.py"
}
to_stop() {
docker rm -f to-lab > /dev/null 2>&1
rm -rf "$LAB/plugin"
}
ask() {
local cond="$1" prompt="$2" name="$3"
python3 gen_project.py > /dev/null
python3 make_settings.py "$cond" > /dev/null
to_start
(cd "$WORK" && TO_LAB_LOG="$LAB/out/hooks/$name" claude -p "$(cat "$LAB/$prompt")" \
--model opus --effort medium \
--setting-sources project --strict-mcp-config \
--tools "Bash,Read,Grep,Glob" --allowedTools "Bash Read Grep Glob" \
--output-format stream-json --verbose --include-hook-events \
< /dev/null) > "out/$name.jsonl" 2> "out/$name.err"
docker exec to-lab python3 "$PLUGIN/skills/token-optimizer/scripts/measure.py" \
compression-stats --days 1 --json > "out/$name.stats.json" 2> /dev/null
to_stop
}
ask는 호출마다 프로젝트와 설정을 새로 만들고 컨테이너를 새로 띄워, 앞 호출의 도구 상태가 넘어가지 않게 합니다. 컨테이너 설정에서 볼 것은 네 가지예요.
-v "$WORK:$WORK": hook이 받는 경로는 Mac 경로라서, 작업 디렉터리를 컨테이너에도 같은 절대 경로로 붙입니다."$TRANSCRIPTS:$TRANSCRIPTS:ro": hook이 읽는 대화 기록 디렉터리를 읽기 전용으로 붙여, 내~/.claude가 바뀌지 않게 합니다.--network none: 저자는 텔레메트리가 없다고 밝혔지만, 네트워크를 아예 끊어 둡니다.measure.py를 만드는 세 줄: 압축된 결과 끝의expand안내를 Mac에서 실행하면 컨테이너 안 보관본을 읽어 옵니다. 실제 설치와 다른 점은 안내문에 찍히는 경로뿐이에요.
실습 1: 큰 출력 명령을 정해 준 과제
먼저 저자 A/B처럼 큰 출력이 나오는 명령을 정해 준 과제예요. 1~4번은 줄인 결과로도 답할 수 있고, 5번 답은 줄일 때 빠지는 줄에만 일부러 두었어요. Claude가 원본을 다시 찾는지 보려고 도구에 불리하게 넣은 질문이고, 5번 없이 잰 결과는 없습니다.
cat prompt-cmd.txt
이 저장소에서 다음 세 명령을 순서대로 한 번씩, 파이프나 옵션을 더하지 말고 그대로 실행하세요: `git log`, `grep -rn assertEqual tests`, `grep -rn 'def ' shop`. 그다음 실행 결과를 근거로 아래 질문에 답하세요. 파일은 수정하지 마세요.
1. 커밋은 모두 몇 개입니까?
2. 제목이 `Tidy pricing.bulk_discount (rev 1)`인 커밋의 해시 앞 7자리는 무엇입니까?
3. tests/test_pricing.py 안에 assertEqual이 있는 줄은 몇 개입니까?
4. shop/ 아래 `def `가 있는 줄은 모두 몇 개입니까?
5. tests/test_pricing.py에서 `bulk_discount(50, 300)`의 기대값은 얼마입니까?
마지막 다섯 줄은 정확히 `A1: <값>`부터 `A5: <값>`까지 한 줄에 하나씩 써 주세요.
run.sh는 schedule.txt의 순서대로 ask를 30번 불러요. 조건마다 워밍업 1번을 먼저 버리고, 두 과제와 두 조건(tool, pass)을 회차마다 순서를 바꿔 섞었습니다.
chmod +x run.sh
./run.sh
같은 명령의 원본과 Token Optimizer를 거친 결과를 나란히 볼게요. calls.py는 호출 하나에서 Claude가 부른 도구와 받은 결과의 글자 수를 차례로 찍어요.
python3 calls.py cmd-pass-01
python3 calls.py cmd-tool-02
1. Bash "git log" -> 25,110자
2. Bash "grep -rn assertEqual tests" -> 17,400자
3. Bash "grep -rn 'def ' shop" -> 1,972자
1. Bash "git log" -> 12,585자
2. Bash "grep -rn assertEqual tests" -> 2,654자
3. Bash "grep -rn 'def ' shop" -> 1,854자
4. Bash "python3 <실습 디렉터리>/plugin/token-optimizer/5.13.57/skills/token-optimizer/scripts/measure.py expand 41c8c5dede620d37" -> 17,400자
grep 결과가 17,400자에서 2,654자로 줄었어요. 그런데 도구 조건의 Claude는 네 번째로 expand를 불러 17,400자 원본을 그대로 다시 받았습니다. 줄인 결과가 어떻게 생겼는지 볼게요(발췌).
python3 calls.py cmd-tool-02 2 | head -n 5
python3 calls.py cmd-tool-02 2 | tail -n 2
[Search results: 241 matches across 8 files. ALL matching filenames listed; per-line content shown for the top 20. A file not listed here has NO matches.]
tests/test_tax.py:7: self.assertEqual(vat(8, 1), 21)
tests/test_tax.py:10: self.assertEqual(vat(52, 4), 124)
tests/test_tax.py:13: self.assertEqual(vat(11, 8), 62)
... 27 more matches in tests/test_tax.py ...
python3 <실습 디렉터리>/plugin/token-optimizer/5.13.57/skills/token-optimizer/scripts/measure.py expand 41c8c5dede620d37
]
끝에는 원본 명령을 다시 돌리지 말고 이 expand 명령으로 보관본을 읽으라는 안내가 붙어요. 파일마다 앞 세 줄과 나머지 개수만 남겨서 3번 질문(31줄)은 답할 수 있지만, bulk_discount(50, 300) 줄은 빠졌어요.
20번 모두 모아 볼게요. 기준 조건은 개별 회차를 빼고 머리 줄과 중앙값만 보였어요.
python3 summarize.py cmd | grep -v 'cmd-pass-'
cmd-pass: 10회, 정답 50/50, 압축 0회
중앙값: 세 명령 결과 44,482자, 입력 62,408, 캐시 쓰기 10,252, 출력 1,260, $0.1179, 4.0턴, 22.2초
cmd-tool: 10회, 정답 50/50, 압축 30회
cmd-tool-01: 정답 5/5, 도구 Bash Bash Bash Grep Grep Grep, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 303자, 입력 77,856, $0.1594, 7턴
cmd-tool-02: 정답 5/5, 도구 Bash Bash Bash Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 17,400자, 입력 67,567, $0.1876, 5턴
cmd-tool-03: 정답 5/5, 도구 Bash Bash Bash Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 17,400자, 입력 85,512, $0.2046, 5턴
cmd-tool-04: 정답 5/5, 도구 Bash Bash Bash Grep Grep, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 125자, 입력 77,749, $0.1319, 6턴
cmd-tool-05: 정답 5/5, 도구 Bash Bash Bash Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 17,400자, 입력 67,648, $0.1943, 5턴
cmd-tool-06: 정답 5/5, 도구 Bash Bash Bash Grep Grep, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 221자, 입력 59,432, $0.1268, 6턴
cmd-tool-07: 정답 5/5, 도구 Bash Bash Bash Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 17,400자, 입력 52,294, $0.1901, 5턴
cmd-tool-08: 정답 5/5, 도구 Bash Bash Bash Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 485자, 입력 59,359, $0.1247, 5턴
cmd-tool-09: 정답 5/5, 도구 Bash Bash Bash Grep Grep Bash, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 128자, 입력 95,919, $0.1359, 7턴
cmd-tool-10: 정답 5/5, 도구 Bash Bash Bash Grep, 압축 3회, 세 명령 결과 17,093자, 그 뒤 결과 58자, 입력 59,071, $0.1201, 5턴
중앙값: 세 명령 결과 17,093자, 입력 67,608, 캐시 쓰기 12,678, 출력 1,492, $0.1476, 5.0턴, 26.8초
입력은 한 호출의 모든 API 요청에서 input_tokens, cache_creation_input_tokens, cache_read_input_tokens를 더한 값이에요. 도구 조건 10번의 세 명령 30개가 모두 압축돼 44,482자에서 17,093자(38%)가 됐고, 정답은 두 조건 모두 50/50이었습니다.
하지만 도구 조건 10번은 모두 세 명령 뒤에 도구를 더 불렀어요. 5회는 expand로 원본을 다시 꺼냈고(그중 4회는 17,400자 전부), 5회는 내장 Grep으로 그 줄을 찾았으며, 이 중 5회는 압축본으로 답할 수 있던 개수도 다시 확인했어요.
그래서 num_turns 중앙값이 4에서 5로, 입력 토큰 중앙값이 62,408에서 67,608로 8% 늘었습니다. 입력 합은 API 요청 수를 따라 움직여서, 기준 조건도 8회는 62,287~62,530, 요청이 하나 더 많았던 2회는 93,608·93,732로 두 덩어리였어요. 평균으로는 68,645 대 70,241(+2.3%)이라, 10회씩으로는 요청을 묶는 방식에 따른 차이가 도구 효과와 비슷한 크기입니다.
저자 A/B는 답이 압축본 안에 있는 질문을 썼고, 같은 일을 하지 않은 회차는 비교 전에 뺐다고 밝혔어요("Runs that did not do equal work were quarantined before comparison"). 그 방식이라면 이 도구 10회는 모두 빠졌을 회차라, 이 8%를 저자 수치의 반박으로 읽으시면 안 돼요.
비용 추정 중앙값($0.1179 대 $0.1476)은 조심해서 봐 주세요. 기록 마지막 줄의 결과 이벤트는 --output-format json이 돌려주는 결과 JSON과 같은 필드를 담고 있어요. lab.sh의 excerpt는 거기서 usage 네 필드, total_cost_usd, num_turns, duration_ms만 남기는 jq 함수이고, 기준 1회차와 도구 10회차를 골라 보면 이렇습니다(발췌).
source ./lab.sh
excerpt out/cmd-pass-01.jsonl out/cmd-tool-10.jsonl
{"usage":{"input_tokens":6,"cache_creation_input_tokens":10195,"cache_read_input_tokens":52138,"output_tokens":1266},"total_cost_usd":0.1173316,"num_turns":4,"duration_ms":25426}
{"usage":{"input_tokens":8,"cache_creation_input_tokens":10682,"cache_read_input_tokens":48381,"output_tokens":1248},"total_cost_usd":0.1201242,"num_turns":5,"duration_ms":21858}
도구 10회차는 Grep 한 번으로 끝나 입력 합이 59,071로 기준(62,339)보다 적은데도 비용 추정은 조금 더 높아요. API 요청마다 캐시에 쓰고 읽은 토큰을 보면 이유가 보입니다.
for n in cmd-pass-01 cmd-tool-10; do
jq -c 'select(.type == "assistant") | .message.usage | {cache_creation_input_tokens, cache_read_input_tokens}' "out/$n.jsonl" | uniq
done
{"cache_creation_input_tokens":0,"cache_read_input_tokens":7630}
{"cache_creation_input_tokens":0,"cache_read_input_tokens":22254}
{"cache_creation_input_tokens":10195,"cache_read_input_tokens":22254}
{"cache_creation_input_tokens":0,"cache_read_input_tokens":7630}
{"cache_creation_input_tokens":7638,"cache_read_input_tokens":7630}
{"cache_creation_input_tokens":2585,"cache_read_input_tokens":15268}
{"cache_creation_input_tokens":459,"cache_read_input_tokens":17853}
기준 조건은 git log 원본이 매 호출 똑같아서, 앞 호출이 캐시에 써 둔 것을 그대로 읽었어요(2번째 요청의 캐시 쓰기 0). 도구 조건은 결과 끝의 보관 키가 호출마다 달라서 앞 호출의 캐시를 쓰지 못하고 압축본을 늘 새로 씁니다. 같은 출력을 몇 분 간격으로 되풀이하는 이 측정에서만 생기는 차이라, 조건 비교는 캐시 상태와 상관없는 입력 토큰 합으로 보시는 게 안전해요.
앞 캐시가 없던 기준 워밍업 1회는 2번째 요청에서 git log 원본 14,624토큰을 새로 써 $0.2312였어요. 도구 조건의 비용 중앙값을 올린 것은 원본 17,400자를 다시 받은 4회($0.1876~$0.2046)입니다.
첫 요청의 입력 합은 두 조건 모두 7,632토큰(도구 1회는 7,631)으로 같았고, 도구 조건 14번 모두 시작·프롬프트·종료 hook의 출력이 비어 있었어요. 마지막 요청의 문맥은 기준 조건이 약 32,449토큰(22,254+10,195), 도구 조건이 약 18,312토큰(17,853+459)이었지만, 이 과제는 명령 세 개로 끝나 이 차이가 쌓일 요청이 많지 않았습니다.
실습 2: 명령을 Claude에게 맡긴 과제
이번에는 실제 사용처럼 명령을 Claude가 고르게 했어요. 질문(prompt-free.txt)은 실패하는 테스트 하나의 원인 커밋을 git 기록에서 찾아 설명하고, 마지막 줄에 ANSWER: <커밋 해시 앞 7자리> <함수 이름>을 쓰라는 내용입니다.
python3 summarize.py free | grep -E '^free|중앙값'
free-pass: 4회, 정답 4/4, 압축 0회, head·tail을 붙인 Bash 11/15
중앙값: Bash 결과 4,928자, 입력 46,003, 캐시 쓰기 2,764, 출력 1,131, $0.0534, 5.0턴, 22.5초
free-tool: 4회, 정답 4/4, 압축 0회, head·tail을 붙인 Bash 10/14
중앙값: Bash 결과 4,638자, 입력 35,180, 캐시 쓰기 2,662, 출력 1,056, $0.0477, 4.0턴, 21.8초
도구 조건 4번에서 압축은 한 번도 일어나지 않았어요. 두 조건 8번에서 Claude가 고른 Bash 29개 중 21개에 | head나 | tail이 붙어 있었습니다.
python3 calls.py free-tool-02
1. Bash "python3 -m unittest 2>&1 | tail -30; git log --oneline | wc -l; ls -R | grep -v pyc | head -30" -> 1,159자
2. Bash "grep -n \"def bulk_discount\" -A15 shop/pricing.py; git log --oneline -L '/def bulk_discount/,+12:shop/pricing.py' | head -120" -> 3,515자
3. Bash "git show --stat --format='%H %s' b76ceae; git log --format=%h -S'qty > 50' -- shop/pricing.py" -> 155자
Claude가 처음부터 tail -30, git log --oneline -L처럼 필요한 부분만 받아서, 결과가 Token Optimizer가 줄일 크기나 형태가 아니었어요. 종료 코드가 0이 아닌 명령은 Claude Code가 실패 hook(PostToolUseFailure)으로 넘기고 플러그인은 이 경로에서 출력을 줄이지 않지만, 이번에는 그런 명령이 2건뿐이라 압축이 없던 이유는 아니에요. 입력 중앙값의 차이(46,003 대 35,180)는 압축이 아니라 턴 수(5 대 4)의 차이예요.
정리
범위: Claude Code 2.1.309 claude -p, Opus 5.5(claude-opus-5-5), effort medium, Token Optimizer v5.13.57의 hook 16개를 Docker 컨테이너에서 실행, 고정 시드 Python 패키지와 git 커밋 161개, 워밍업 2번 + 명령을 정해 준 과제 20번 + 명령을 맡긴 과제 8번, 모두 30번 호출.
git log와grep두 개를 그대로 실행하게 하자 Claude가 받은 결과는 44,482자에서 17,093자로 줄었고, 정답은 두 조건 모두 50/50이었습니다. 큰 출력 명령을 그대로 부르는 작업이라면 출력은 확실히 줄어요.- 답을 일부러 잘리는 줄에 둔 질문 때문에 도구 조건 10번 모두
expand나 Grep으로 다시 찾았고(그중 5회는 다른 답도 다시 확인), 입력 토큰 중앙값이 8%(평균 2.3%) 늘었습니다. 저자의 −11%, −15.7%는 답이 압축본 안에 있는 질문으로, 추가 작업을 한 회차를 빼고 잰 수치라 이 결과로 반박되지 않아요. - 명령을 Claude에게 맡기자 Opus는 Bash 29개 중 21개에
head·tail을 붙였고, 압축은 4번 중 한 번도 일어나지 않았습니다. 실제 사용에서 효과가 나려면 큰 출력을 그대로 받는 명령이 있어야 해요. - 명령을 정해 준 과제의 첫 요청 입력 합은 두 조건이 같았고(7,632토큰, 도구 1회는 7,631), 도구 조건 14번에서 시작·프롬프트·종료 hook은 아무것도 넣지 않았습니다. 플러그인의 스킬·명령 설명 없이 hook만 옮긴 이번 조건에서는 저자가 밝힌 짧은 세션의 +~160토큰이 보이지 않았어요.
- 비용 추정은 기준 조건이 앞 호출의 캐시를 읽어 유리하게 나왔습니다. 같은 출력을 되풀이하는 측정이라면 입력 토큰 합으로 비교하세요.