# AI 에이전트

lean-ctx를 쓰면 85가지 넘는 셸 출력을 압축해 준다는데, 과연 진짜일까?

@Hong2026-10-24

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
lean-ctx를 Claude Code에 붙이면 테스트와 git 출력이 줄어 토큰도 정말 줄어들까?
답
압축본에서 빠지는 값을 묻자 Claude가 원본을 다시 조회해, pytest 출력이 5,524바이트에서 524바이트로 줄었는데도 입력 토큰 중앙값은 29,918에서 47,252로 늘었고, 명령을 맡긴 과제에서는 압축이 한 번도 일어나지 않았다.
조건
2026-10-24, Claude Code 2.1.309 claude -p, Opus 5.5, effort medium, lean-ctx 3.11.4 셸 hook만, 30번 호출

환경

측정일
2026-10-24
도구
lean-ctx 3.11.4, Bash 명령을 바꾸는 hook(lean-ctx hook rewrite)만 사용, Docker 컨테이너 안에서 실행
Claude Code
2.1.309, --model opus(결과 JSON modelUsage의 모델 ID는 모든 호출에서 claude-opus-5-5), --effort medium
데이터
고정 시드로 만든 Python 저장소(테스트 214개 중 3개 실패, 커밋 120개)
반복
워밍업 2번(버림), 명령을 정해 준 과제 조건별 9번, 명령을 맡긴 과제 조건별 5번, 섞은 순서로 모두 30번 호출
실행
macOS, Docker, 명령은 두 조건 모두 같은 상주 컨테이너 안에서 실행
비용
구독 로그인으로 호출, 금액은 Claude Code가 API 목록 단가로 환산한 추정치(실제 청구액 아님)
목차
  1. lean-ctx가 줄이는 것과 공개 수치
  2. 실습 준비
  3. 실습 1: 명령을 정해 준 과제
  4. 실습 2: 명령을 맡긴 과제
  5. 정리

이번 시간에는 Claude Code가 실행한 셸 명령의 출력을 줄여 준다는 lean-ctx가 실제로 토큰을 줄이는지를 claude -p로 측정하며 배워가는 시간입니다. claude -p는 대화 화면 없이 질문 하나를 보내고 결과를 받는 실행 방식이에요.

실패한 테스트의 원인을 찾는 과제를, 처음 두 명령을 정해 준 경우와 명령을 모두 Claude에게 맡긴 경우로 나눠 보냅니다. 다시 돌리면 숫자가 달라질 수 있어요.

lean-ctx가 줄이는 것과 공개 수치

lean-ctx는 hook(Claude Code가 도구를 부르기 직전에 끼어드는 스크립트)으로 Bash 명령을 lean-ctx -c '…'로 바꿔 실행하고, README 표현으로 "85+ shell-output patterns compress git, npm, cargo, docker, kubectl, terraform and more" 출력을 줄여 돌려줍니다. 예전 벤치마크 수치는 저자가 철회했어요(BENCHMARKS.md: "previously appeared here are withdrawn. They are not a current LeanCTX claim").

README에는 "A powered with/without quality study has not been run yet"이라고도 적혀 있습니다. 그래서 이번에는 출력이 실제로 줄어드는지, 줄어든 출력으로 Claude가 일을 마치는지를 봅니다.

실습 준비

이 글의 명령은 모두 실습 코드의 lean-ctx-shell-compress/ 디렉터리에서 실행합니다.

  • Dockerfile: python:3.12-slim에 git, pytest, lean-ctx 3.11.4 릴리스 바이너리를 넣은 이미지
  • make_project.py: 고정 시드로 실습 저장소 work/proj를 만들고 정답을 JSON 한 줄로 찍는 스크립트
  • hook.py, make_settings.sh: Bash 명령을 컨테이너에서 실행하게 바꾸는 hook과, 그 hook을 거는 설정 파일 settings.json을 만드는 스크립트
  • lab.sh: 컨테이너와 저장소를 새로 만드는 fresh, claude -p를 부르는 ask, 같은 명령의 출력 두 가지를 저장하는 pair
  • prompt-a.txt, prompt-b.txt, schedule.txt, run.sh: 두 과제의 질문과, 정해 둔 순서로 30번 호출하는 스크립트
  • summarize.py, calls.py: 결과를 과제·조건별로 모으는 스크립트와 호출 하나의 도구 사용을 보여 주는 스크립트
  • README.md: 파일 목록

평소에 쓰실 때는 brew tap yvgude/lean-ctx && brew install lean-ctx로 설치하고 lean-ctx init --agent claude를 실행하면 됩니다. 이 명령이 Bash hook과 Read·Grep·Glob hook, MCP 서버, ~/.claude/CLAUDE.md 안내문, 스킬을 함께 설치해요.

여기서는 Mac에 설치하지 않고 Docker로 감쌌습니다. lean-ctx hook이 바꾼 명령은 Mac 셸이 실행하기 때문에, hook.py가 Bash 명령을 상주 컨테이너 lc-lab에서 실행하도록 바꾸고 lean-ctx 조건(tool)에서만 lean-ctx 자신의 hook rewrite가 고른 명령을 거치게 했어요. hook.py의 핵심 부분(발췌)입니다.

if mode == "tool":
    r = subprocess.run(["lean-ctx", "hook", "rewrite"], input=raw, capture_output=True, text=True, timeout=20)
    try:
        inner = json.loads(r.stdout)["hookSpecificOutput"]["updatedInput"]["command"]
        route = "lean-ctx"
    except (ValueError, KeyError, TypeError):
        route = "lean-ctx-skip"

cwd = event.get("cwd") or os.environ["CLAUDE_PROJECT_DIR"]
tool_input["command"] = f"docker exec -e CLAUDECODE=1 -w {shlex.quote(cwd)} lc-lab bash -c {shlex.quote(inner)}"

기준 조건(base)도 같은 hook과 같은 컨테이너를 지나가니, 두 조건의 차이는 lean-ctx가 명령을 바꾸는지 하나뿐이에요. Read·Grep·Glob hook, MCP 서버, CLAUDE.md 안내문은 붙이지 않았고, Bash 호출마다 컨테이너를 거치는 지연은 두 조건에 똑같이 더해집니다.

lab.sh에서 claude -p를 부르는 부분(발췌)입니다.

  (cd work/proj && LCLAB_MODE="$cond" LCLAB_LOG="$root/${out%.jsonl}.hook.jsonl" \
    perl -e 'alarm shift; exec @ARGV' 900 \
    claude -p "$prompt" --tools "Bash,Read,Grep,Glob" \
      --model opus --effort medium \
      --setting-sources project --settings "$root/settings.json" --strict-mcp-config \
      --no-session-persistence --include-hook-events \
      --output-format stream-json --verbose \
      < /dev/null) > "$out" 2> "${out%.jsonl}.err"

hook 설정은 --settings로 실습 저장소 밖에 둡니다. 저장소 안 .claude/에 두면 Claude가 그 파일을 열어 보는 일이 있었어요.

따라 하시려면 Apple Silicon Mac, Claude Code 2.1.309, 구독 로그인, Docker, jq가 필요해요. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

py() { docker run --rm --network none -v "$PWD:$PWD" -w "$PWD" lean-ctx-lab:3.11.4 python "$@"; }

excerpt() { jq 'select(.type == "result") | {usage: (.usage | {input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens}), total_cost_usd, num_turns, duration_ms}' "$@"; }

source ./lab.sh

py는 실습 디렉터리를 같은 경로로 붙인 컨테이너에서 Python 스크립트를 실행하고, excerpt는 stream-json 마지막 줄의 결과 이벤트(--output-format json의 결과 JSON과 같은 필드)에서 토큰 네 필드, 비용 추정, 턴 수, 시간만 JSON 모양 그대로 보여 줍니다.

이미지를 만들고 설정 파일과 실습 저장소를 준비합니다. zip에서 푼 스크립트는 실행 권한이 없어서 chmod +x를 먼저 해 주세요.

docker build -q -t lean-ctx-lab:3.11.4 .
chmod +x make_settings.sh run.sh
./make_settings.sh
fresh
cat out/answer.json
sha256:a4e77fab7664f5449e06d71d0d8db85a2a140d0b527d0e9b8a42bead600742bb
settings.json을 만들었습니다
{"function": "tier_discount", "commit": "f8d66e2", "inv07_subtotal": 35161.7}

할인율을 고르는 함수 tier_discount는 커밋 f8d66e2에서 >=가 >로 바뀌어 수량이 50처럼 구간 경계와 같을 때 할인율을 한 단계 낮게 고릅니다. 이 커밋 메시지에는 "No behaviour change intended"라고 적혀 있어서 메시지만 보고는 찾을 수 없어요.

fresh는 호출마다 컨테이너와 저장소를 새로 만듭니다. 텔레메트리는 이미지 환경 변수(LEAN_CTX_TELEMETRY=off, DO_NOT_TRACK=1), lean-ctx telemetry off, 네트워크 차단(--network none)으로 끕니다.

실습 1: 명령을 정해 준 과제

먼저 Claude 없이 출력이 얼마나 줄어드는지 볼게요. pair는 같은 명령을 컨테이너에서 그대로 한 번, lean-ctx -c로 한 번 실행해 저장합니다.

pair pytest "python -m pytest"
pair gitlog "git log -p -- ledger/discounts.py"
sed "s|$PWD|.|" out/pair/pytest.lc.txt
grep -c "35161.7" out/pair/pytest.raw.txt out/pair/pytest.lc.txt
tail -n 6 out/pair/gitlog.lc.txt | sed "s|$PWD|.|"
    5524 out/pair/pytest.raw.txt
     524 out/pair/pytest.lc.txt
    6048 total
    2804 out/pair/gitlog.raw.txt
    1072 out/pair/gitlog.lc.txt
    3876 total
pytest: 211 passed, 3 failed (0.15s)
  FAIL: tests/test_discounts.py::test_tier_discount[50-0.1] - assert 0.05 == 0.1
  FAIL: tests/test_invoice.py::test_build_invoice[inv07] - AssertionError: ass...
  FAIL: tests/test_invoice.py::test_build_invoice[inv19] - AssertionError: ass...
[lean-ctx: full output -> ./work/home/.local/state/lean-ctx/tee/python_-m_pytest_217d2897.log (redacted, 24h TTL)]
out/pair/pytest.raw.txt:1
out/pair/pytest.lc.txt:0
f8d66e2 refactor(discounts): use a tier table [ledger/discounts.py] +3/-5
d3e7531 perf(discounts): memoize tier lookups [ledger/discounts.py] +3/-0
9af4a59 feat: initial ledger package with tests [ledger/discounts.py] +9/-0

[4 commits, +19/-6 total]
[lean-ctx: full output -> ./work/home/.local/state/lean-ctx/tee/git_log_-p_--_ledger_discounts_py_9f4af22a.log (redacted, 24h TTL)]

pytest 출력은 5,524바이트에서 524바이트로 줄었어요. 실패 목록은 남았지만, 실패한 송장 테스트 inv07이 기대한 subtotal 값 35161.7은 원본에만 있고 압축본에는 없습니다.

git log -p는 2,804바이트에서 1,072바이트로 줄었는데, 맨 위 커밋의 diff만 남기고 버그를 들인 f8d66e2는 한 줄 요약으로 바꿨어요. 원본은 lean-ctx가 따로 저장한 파일로 갑니다. 크기는 실습 디렉터리 경로 길이와 pytest의 비교 출력 순서에 따라 실행마다 수십 바이트 달라질 수 있어요.

그래서 질문 prompt-b.txt는 이 두 명령을 그대로 먼저 실행하게 하고, 압축본에서 빠지는 값을 함께 물어요.

cat prompt-b.txt
이 저장소에서 테스트 일부가 실패합니다. 먼저 `python -m pytest`를 옵션이나 파이프를 붙이지 말고 그대로 실행하고, 이어서 `git log -p -- ledger/discounts.py`를 그대로 실행해 주세요. 그다음에는 필요한 명령을 자유롭게 써도 됩니다. 파일은 고치지 마세요.

아래 세 가지를 찾아 주세요.
1. 실패의 원인이 된 함수 이름
2. 그 버그를 처음 들인 커밋의 짧은 해시(7자)
3. 테스트 `test_build_invoice[inv07]`이 기대한 송장의 subtotal 값

답의 마지막 줄은 `ANSWER: <함수 이름> <커밋 7자> <subtotal>` 형식으로 써 주세요.

run.sh는 schedule.txt에 섞어 적어 둔 순서대로 ask를 30번 부르고, 이미 성공한 호출은 건너뜁니다. 한도 오류로 멈추면 같은 명령을 다시 돌리면 돼요.

summarize.py는 조건별로 정답 수, lean-ctx 표시([lean-ctx)가 붙은 결과 수(압축), lean-ctx가 막은 명령 수(차단), lean-ctx가 저장한 원본 파일을 읽으려 한 수를 세고 중앙값을 찍습니다. 기록 이름은 순번-질문-조건이라 b-tool은 prompt-b.txt를 lean-ctx 조건으로 보낸 호출이에요. 입력은 세 필드의 합이고, ./run.sh의 출력은 앞 두 줄만 보인 발췌예요.

./run.sh
py summarize.py b-base b-tool
w1-a-base
w2-a-tool
b-base: 9회, 정답 9/9, pytest를 두 번 이상 실행한 회차 0, 압축 0, 차단 0, 원본 파일 읽기 시도 0
  중앙값: Bash 2번, 도구 결과 8,357자, 입력 29,918, 출력 1,006, $0.0605, 3턴, 18.6초
b-tool: 9회, 정답 9/9, pytest를 두 번 이상 실행한 회차 8, 압축 21, 차단 1, 원본 파일 읽기 시도 5
  중앙값: Bash 4번, 도구 결과 8,650자, 입력 47,252, 출력 1,665, $0.0952, 6턴, 27.0초

정답은 두 조건 모두 9번 중 9번이었어요. 하지만 lean-ctx 조건은 9번 중 8번이 pytest를 다시 돌렸고, 남은 1번은 lean-ctx가 저장한 원본 파일을 cat으로 읽었습니다.

중앙값으로 턴은 3에서 6, 입력은 29,918에서 47,252(1.6배), 비용 추정은 $0.0605에서 $0.0952로 늘었어요. 도구 결과 글자 수 중앙값은 8,357과 8,650으로 비슷해서, 줄인 만큼을 다시 조회하는 데 썼습니다.

기준 5회차와 lean-ctx 11회차의 결과 JSON(발췌), 11회차의 도구 호출과 3번째 도구 결과를 볼게요.

excerpt out/05-b-base.jsonl out/11-b-tool.jsonl
py calls.py out/11-b-tool.jsonl
py calls.py out/11-b-tool.jsonl 3
{
  "usage": {
    "input_tokens": 6,
    "cache_creation_input_tokens": 4411,
    "cache_read_input_tokens": 25479,
    "output_tokens": 1006
  },
  "total_cost_usd": 0.06052780000000001,
  "num_turns": 3,
  "duration_ms": 18604
}
{
  "usage": {
    "input_tokens": 10,
    "cache_creation_input_tokens": 6020,
    "cache_read_input_tokens": 45272,
    "output_tokens": 1713
  },
  "total_cost_usd": 0.09151440000000001,
  "num_turns": 7,
  "duration_ms": 26991
}
1. Bash python -m pytest -> 532자, lean-ctx 압축
2. Bash git log -p -- ledger/discounts.py -> 1,068자, lean-ctx 압축
3. Read ./work/home/.local/state/lean-ctx/tee/python_-m_pytest_217d2897.log -> 266자
4. Bash git --no-pager show f8d66e2 d3e7531 9af4a59 -- ledger/discounts.py | cat -> 2,094자
5. Bash python -m pytest "tests/test_invoice.py::test_build_invoice[inv07]" -vv 2>&1 | cat; grep -rn "inv07" tests | h -> 4,515자, lean-ctx 압축
6. Bash grep -n "assert" tests/test_invoice.py; ls tests -> 175자
Claude requested permissions to read from ./work/home/.local/state/lean-ctx/tee/python_-m_pytest_217d2897.log, but you haven't granted it yet.

Claude는 압축본 끝의 원본 파일 경로를 Read로 열려 했지만, 작업 디렉터리 밖이라 claude -p에서 거부됐어요. 원본 파일 읽기 시도 5번 중 Read 4번이 이렇게 거부됐어요.

이번 측정은 Bash hook만 걸어서 Read가 기본 권한 검사를 거쳤어요. lean-ctx init이 함께 거는 Read hook은 Claude Code에서 allow를 돌려주니, 실제 설치에서는 이 읽기가 통과했을 가능성이 커요(이번에 재지는 않았습니다).

num_turns는 30번 모두 도구 호출 수에 1을 더한 값이라, 거부된 Read 4번도 한 턴씩 셌어요. 이걸 빼면 lean-ctx 조건의 턴 중앙값은 6이 아니라 5이고, 이 Read는 git log -p와 같은 요청에서 함께 나가서 입력 토큰은 거의 늘리지 않았어요.

그 뒤 git show로 접힌 diff를, -vv를 붙인 pytest로 송장 값을 다시 받았어요. 9번 모두 답에서 lean-ctx가 출력을 요약했다고 따로 알렸습니다.

실습 2: 명령을 맡긴 과제

이번에는 prompt-a.txt로 명령을 정해 주지 않고 함수와 커밋만 물었어요. -v를 붙이면 회차별 줄도 찍습니다.

cat prompt-a.txt
py summarize.py a-base a-tool -v
이 저장소에서 테스트 일부가 실패합니다. 실패의 원인이 된 함수 이름과, 그 버그를 처음 들인 커밋의 짧은 해시(7자)를 찾아 주세요. 파일은 고치지 마세요. 답의 마지막 줄은 `ANSWER: <함수 이름> <커밋 7자>` 형식으로 써 주세요.
a-base: 5회, 정답 5/5, pytest를 두 번 이상 실행한 회차 4, 압축 0, 차단 0, 원본 파일 읽기 시도 0
  중앙값: Bash 5번, 도구 결과 5,763자, 입력 56,285, 출력 1,716, $0.0734, 6턴, 29.6초
  03-a-base: O, Bash 5, 압축 0, 차단 0, 입력 56,285, $0.0734, 6턴
  08-a-base: O, Bash 4, 압축 0, 차단 0, 입력 46,069, $0.0731, 5턴
  14-a-base: O, Bash 5, 압축 0, 차단 0, 입력 57,466, $0.0806, 6턴
  19-a-base: O, Bash 5, 압축 0, 차단 0, 입력 56,276, $0.0721, 6턴
  25-a-base: O, Bash 5, 압축 0, 차단 0, 입력 58,326, $0.0831, 6턴
a-tool: 5회, 정답 5/5, pytest를 두 번 이상 실행한 회차 5, 압축 0, 차단 5, 원본 파일 읽기 시도 0
  중앙값: Bash 5번, 도구 결과 6,462자, 입력 58,324, 출력 2,282, $0.0943, 6턴, 33.2초
  06-a-tool: O, Bash 7, 압축 0, 차단 2, 입력 124,543, $0.1421, 11턴
  09-a-tool: O, Bash 5, 압축 0, 차단 1, 입력 54,869, $0.0737, 6턴
  10-a-tool: O, Bash 6, 압축 0, 차단 0, 입력 66,998, $0.0955, 7턴
  12-a-tool: O, Bash 5, 압축 0, 차단 1, 입력 58,324, $0.0943, 6턴
  18-a-tool: O, Bash 5, 압축 0, 차단 1, 입력 57,977, $0.0899, 6턴

lean-ctx 조건 5번 동안 압축은 0번이었어요. Claude는 처음부터 python3 -m pytest -q 2>&1 | tail -30처럼 출력을 스스로 줄였고, lean-ctx는 이런 -q 출력을 줄이지 않았습니다.

대신 5번 중 4번에서 lean-ctx의 셸 보안 검사가 명령을 모두 5번 막았어요. 9회차의 도구 호출입니다.

py calls.py out/09-a-tool.jsonl
1. Bash ls && git log --oneline | wc -l && (ls tests test 2>/dev/null) -> 170자
2. Bash python3 -m pytest -q -p no:cacheprovider 2>&1 | tail -40 -> 334자, 차단
3. Bash python3 -m pytest -q 2>&1 | tail -40; git status --short -> 3,281자
4. Bash ls ledger; cat ledger/discounts.py; git log --oneline -- ledger/discounts.py -> 611자
5. Bash git log -p --format='=== %h %s' -- ledger/discounts.py | grep -E '^(===|[+-][^+-])' -> 965자

2번째 결과는 "Interpreter 'python3' with inline code execution flag '-p' is blocked"였어요. 캐시 파일을 남기지 않으려고 붙인 pytest 옵션 -p no:cacheprovider를 인라인 코드 실행으로 본 거예요. lean-ctx 문서는 파이프 중 한 단계라도 검사를 통과하지 못하면 명령 전체를 그대로 둔다고 설명하지만, 3.11.4는 파이프 전체를 감싸 막았어요.

나머지 4번은 이전 커밋을 임시 디렉터리에 풀어 테스트하려던 반복문의 mktemp(허용 목록에 없음) 3번과 python3 -c 1번이었고, 막힐 때마다 Claude는 명령을 바꿔 다시 돌았습니다. 두 번 막힌 06회차는 11턴, $0.1421까지 갔어요.

조건 전체로는 턴 중앙값이 6으로 같고 입력도 56,285 대 58,324로 비슷했어요. 비용 추정 중앙값 $0.0734 대 $0.0943의 차이는 주로 출력(1,716 대 2,282)에서 나왔고, 차단이 없던 10회차도 $0.0955였어요.

정리

범위: Claude Code 2.1.309의 claude -p, Opus 5.5(claude-opus-5-5), effort medium, Docker 컨테이너 안에서 돌린 lean-ctx 3.11.4의 Bash hook만(Read·Grep·Glob hook·MCP 서버·CLAUDE.md 안내문 없음), 테스트 214개·커밋 120개 저장소 하나, 두 과제 28번 + 워밍업 2번.

  • lean-ctx는 python -m pytest 출력을 5,524바이트에서 524바이트로, git log -p를 2,804바이트에서 1,072바이트로 줄였습니다. 실패 목록은 남지만 assert가 비교한 값과 이전 커밋의 diff는 빠지니, 그 값이 필요한 일이면 다시 조회한다고 보세요.
  • 명령을 정해 준 과제에서 lean-ctx 조건은 9번 모두 정답이었지만 9번 모두 원본을 다시 구했고, 중앙값으로 입력 토큰은 29,918에서 47,252, 비용 추정은 $0.0605에서 $0.0952, 턴은 3에서 6(거부된 Read를 빼면 5)으로 늘었습니다.
  • 압축본이 가리키는 원본 파일은 작업 디렉터리 밖이라, Bash hook만 건 이번 측정에서는 Read가 4번 거부됐습니다. Read hook까지 거는 lean-ctx init 설치에서는 통과했을 가능성이 크니, Bash hook만 따로 걸어 쓴다면 그 경로의 읽기 권한을 함께 정해 두세요.
  • 명령을 Claude에게 맡기자 Claude가 -q, | tail로 출력을 먼저 줄여 5번 동안 압축이 한 번도 일어나지 않았습니다. 대신 lean-ctx의 셸 보안 검사(shell_security, 기본값 enforce)가 pytest -p no:cacheprovider, mktemp, python3 -c를 막아 5번 중 4번에서 명령을 다시 보냈어요.
내 링크 만들기