이번 시간에는 코드를 미리 그래프로 만들어 두는 GitNexus를 Claude Code에 붙였을 때, 에이전트가 덜 찾고 덜 읽어서 토큰이 줄어드는지를 claude -p로 측정하며 배워가는 시간입니다. claude -p는 대화 화면 없이 질문 하나를 보내고 결과를 받는 실행 방식이에요.
GitNexus를 붙인 조건과 기본 도구만 쓰는 조건에 같은 질문을 번갈아 보내 비교할게요. 다시 돌리면 숫자가 달라질 수 있고, --model opus가 가리키는 모델도 시간이 지나면 달라질 수 있어요.
GitNexus가 하는 일
gitnexus analyze는 저장소를 파싱해 함수·클래스와 그 사이의 호출·상속 관계, 실행 흐름을 그래프 DB에 저장해요. 에이전트는 이 그래프를 MCP 도구 17개(impact, context, trace, cypher 등)나 같은 이름의 CLI로 조회합니다.
README는 "so AI agents never miss code"라고 소개하고, 토큰 효율로 "no 10-query chains to understand one function"을 내세우며 조회 4번 이상 걸릴 답을 impact 1번으로 얻는 그림을 보여 줘요. 토큰을 몇 % 줄인다는 수치는 없어서, 정답과 도구 호출 수, 입력 토큰을 직접 재 볼게요. 라이선스는 PolyForm Noncommercial 1.0.0이라 개인 학습·연구에는 쓸 수 있지만, 상업적으로 쓰려면 따로 라이선스가 필요해요.
실습 준비
이 글의 명령은 모두 실습 코드의 gitnexus-code-graph/ 디렉터리에서 실행합니다.
lab.sh: 저장소 받기, 컨테이너 띄우기, 인덱스 만들기,claude -p호출과 호출 순서를 담은 셸 함수 모음summarize.py: 결과를 질문·조건별로 모으고 정답을 판정하는 스크립트answers.json: 질문 4개와 정답 목록
따라 하시려면 Docker, Claude Code 2.1.309와 구독 로그인, git, jq, perl(macOS 기본)이 필요하고, Claude의 Bash 명령은 Mac에서 실행돼요. 셸에 ANTHROPIC_API_KEY나 ENABLE_TOOL_SEARCH가 있으면 과금 방식이나 도구 설명을 싣는 방식이 바뀌니 비우고 실행하세요.
공식 설치는 npm install -g gitnexus@1.6.12 뒤 저장소에서 gitnexus analyze와 gitnexus setup을 실행하는 거예요.
analyze: 인덱스(.gitnexus/),CLAUDE.md,AGENTS.md, 스킬 6개(.claude/skills/gitnexus-*)를 저장소에 씁니다.setup: 홈의~/.claude.json에 MCP 서버를,~/.claude/settings.json에 Grep·Glob·Bash 직전에 그래프 문맥을 붙이는 훅과 git 커밋 뒤 인덱스가 낡았는지 알리는 훅을 등록해요.
여기서는 Mac에 설치하지 않고 공식 Docker 이미지로 돌리며, 측정을 --setting-sources project로 하므로 setup의 MCP 서버·훅을 실습 저장소 안 설정으로 옮겼어요.
아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.
source ./lab.sh
py312() { docker run --rm --network none -v "$PWD":/w -w /w python:3.12-slim python -I "$@"; }
calls() { jq -r --arg p "$R/" 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use") | "\(.name) \(.input | tostring | gsub($p; ""))"' "$1" | cut -c1-120; }
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}' "$1"; }
steps() { jq -s -r '[.[] | select(.type == "assistant") | {id: .message.id, u: .message.usage}] | reduce .[] as $x ([]; if any(.[]; .id == $x.id) then . else . + [$x] end) | map(.u | .input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens) | join(" -> ")' "$1"; }
hooks() { jq -r 'select(.subtype == "hook_response") | "\(.hook_name) \(.output | .[0:120])"' "$1"; }
calls는 부른 도구를, excerpt는 결과 JSON의 주요 필드를, steps는 API 요청마다 실린 입력 토큰을, hooks는 훅 응답을 보여 줘요.
get은 rich v15.0.0을 runs/src/rich에 받아 커밋 해시를 찍고, up은 GitNexus 컨테이너를 띄워요. up의 핵심은 이렇습니다(발췌).
docker run -d --name "$CTR" --network none --memory 3g --cpus 2.5 \
-e GITNEXUS_HOME="$R/gnhome" -e NO_UPDATE_NOTIFIER=1 \
-e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0='*' \
-v "$R:$R" --entrypoint sleep "$IMG" infinity > /dev/null
컨테이너는 --network none이고, 훅과 MCP 서버가 저장소를 절대 경로로 찾아서 runs/를 Mac과 같은 경로로 마운트해요.
get
up
index는 기준 조건의 runs/a/rich와 같은 길이의 경로 runs/b/rich에 저장소를 복사해 컨테이너 안에서 gitnexus analyze를 옵션 없이 실행하고, 인덱스와 연결 설정을 runs/index.tar로 묶어 매번 똑같이 되살려요. 출력 앞부분은 이렇습니다(발췌).
index
Repository indexed successfully (21.0s)
4,090 nodes | 10,819 edges | 192 clusters | 326 flows
112M
인덱스는 21초, 112MB였고 네트워크가 막힌 컨테이너에서 만들어졌으니 코드가 밖으로 나가지 않아요. .gitnexus/는 실제로 쓸 때처럼 작업 트리 안에 있어서, Grep 도구는 건너뛰지만 Bash grep -r는 들어갈 수 있어요(이번 24번에는 그런 호출이 없었어요).
analyze가 쓴 CLAUDE.md는 3,556바이트이고, 호출·의존 관계를 읽기만 하는 질문에도 "Graph first; text search only for empty/UNKNOWN/literals."라고 그래프 도구를 먼저 쓰라고 지시해요. 이 문서는 고치지 않고 그대로 썼어요.
index가 부르는 connect는 GitNexus를 Claude Code에 잇는 파일 세 개를 써요.
runs/bin/gitnexus: GitNexus 조건에서만 PATH 앞에 두는 CLI예요.CLAUDE.md가 권하는node .gitnexus/run.cjs도 이걸 씁니다.runs/b/rich/.claude/settings.json:setup이 쓰는 훅 두 개를 matcher·timeout 그대로 옮기고, 명령만docker exec -i gitnexus-lab node /app/gitnexus/hooks/claude/gitnexus-hook.cjs로 바꿨어요.runs/mcp-b.json:docker exec -i -w <저장소> gitnexus-lab gitnexus mcp를 서버 이름gitnexus로 등록해요. 기준 조건은 빈 설정runs/mcp-a.json을 씁니다.
ask는 질문 하나를 claude -p로 보내요(발췌).
fresh "$c"
if [ "$c" = b ]; then path="$R/bin:$PATH"; allow="$allow mcp__gitnexus"; fi
(cd "$R/$c/rich" && PATH="$path" perl -e 'alarm shift; exec @ARGV' 900 \
claude -p "$prompt" --tools "$TOOLS" --allowedTools "$allow" \
--model opus --effort medium \
--setting-sources project --strict-mcp-config --mcp-config "$R/mcp-$c.json" \
--no-session-persistence --include-hook-events --output-format stream-json --verbose \
< /dev/null) > "$out" 2> "${out%.jsonl}.err"
fresh는 호출마다 저장소를 새로 복사하고 GitNexus 조건이면 index.tar를 풀어요. 두 조건의 도구 목록은 같고, GitNexus 조건에만 PATH의 CLI와 MCP 도구 허용(mcp__gitnexus)이 더해져요. ToolSearch가 있으면 Claude Code 기본 동작처럼 MCP 도구는 처음에 이름만 실리고, 고른 도구의 설명만 그다음 요청부터 실립니다.
실습 1: 영향 범위 질문 하나를 두 조건으로
GitNexus가 내세우는 impact(바꾸면 어디까지 영향받는지)에 맞는 질문으로 볼게요. 정답은 9개 모듈의 12항목이에요(안쪽 함수 하나는 바깥 함수와 함께 셈).
jq -r .q2.question answers.json
rich 저장소에서 `rich/_loop.py`의 `loop_last` 시그니처나 반환 형태가 바뀌면 직접 영향받는 모듈과 함수를 모두 찾아 줘. tests와 docs는 제외. 코드는 수정하지 마.
run은 질문·조건을 섞어 정해 둔 순서대로 26번 호출하고, 앞의 두 번은 조건별 워밍업이라 집계에서 빼요. 결과는 out/<순번>-<조건>-<질문>.jsonl에 남고, 조건은 a가 기준, b가 GitNexus예요.
run
6번째(기준)와 5번째(GitNexus) 호출이 부른 도구예요.
calls out/06-a-q2.jsonl
calls out/05-b-q2.jsonl
Grep {"pattern":"loop_last|_loop\\b|from \\._loop|from \\.\\._loop","path":"rich","output_mode":"content"}
Read {"file_path":"a/rich/rich/_loop.py"}
Bash {"command":"python3 -I - <<'EOF'\nimport ast,glob\nfor f in sorted(glob.glob(\"rich/*.py\")):\n t=ast.parse(open
Grep {"pattern":"last, node|next\\(|levels|stack\\.","path":"rich/tree.py","output_mode":"content"}
ToolSearch {"query":"select:mcp__gitnexus__impact,mcp__gitnexus__context","max_results":2}
mcp__gitnexus__impact {"target":"loop_last","file_path":"rich/_loop.py","direction":"upstream","repo":"rich","maxDepth":
Grep {"pattern":"loop_last","path":"rich","output_mode":"content"}
Bash {"command":"cd rich && sed -n 105,115p tree.py; echo ---; sed -n 165,180p tree.py; echo ---; grep -n \"def \\|^clas
기준 조건은 Grep으로 호출 위치를 찾고, Mac의 python3로 소스를 파싱해 호출을 감싼 함수 이름을 뽑았어요. GitNexus 조건은 ToolSearch로 impact와 context 설명을 불러온 뒤 impact와 Grep을 한 요청에서 함께 불렀습니다. 둘 다 정답이었어요.
GitNexus 쪽 훅 응답이에요.
hooks out/05-b-q2.jsonl
PreToolUse:Grep {"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"[GitNexus] Local augment is unavailable (the gr
PreToolUse:Bash
PostToolUse:Bash
훅은 그래프 문맥 대신 "DB를 다른 GitNexus 프로세스가 잡고 있으니 MCP query 도구를 쓰라"는 안내 한 줄만 붙였습니다.
그래프 DB는 한 프로세스만 쓸 수 있어서, 훅은 MCP 서버가 DB를 열고 있으면 조회를 건너뛰고 이 안내를 저장소마다 10분에 한 번 보내도록 만들어져 있어요. 소스를 보면 Mac에 직접 설치해도 같은 판정을 합니다.
두 호출의 결과 JSON이에요(발췌).
excerpt out/06-a-q2.jsonl
excerpt out/05-b-q2.jsonl
{
"usage": {
"input_tokens": 8,
"cache_creation_input_tokens": 3277,
"cache_read_input_tokens": 45649,
"output_tokens": 2508
},
"total_cost_usd": 0.0855378,
"num_turns": 5,
"duration_ms": 26423
}
{
"usage": {
"input_tokens": 8,
"cache_creation_input_tokens": 7139,
"cache_read_input_tokens": 91304,
"output_tokens": 2405
},
"total_cost_usd": 0.1235048,
"num_turns": 5,
"duration_ms": 30790
}
num_turns는 둘 다 5인데, 이 측정에서는 늘 도구 호출 수에 1을 더한 값이었어요. 입력이 다시 실리는 단위는 API 요청이고, 한 요청에서 도구 여러 개를 함께 부르면 요청은 하나라 둘은 달라요.
이 글에서 "입력"은 usage의 세 필드(input_tokens, cache_creation_input_tokens, cache_read_input_tokens) 합이에요. 캐시에서 읽은 토큰도 요청마다 모델이 다시 받는 입력이라 넣었고, 기준 48,934, GitNexus 98,451이었어요. 요청마다 나눠 보면 이렇습니다.
steps out/06-a-q2.jsonl
steps out/05-b-q2.jsonl
10288 -> 12119 -> 12962 -> 13565
13322 -> 24210 -> 29570 -> 31349
요청은 둘 다 4번이었고, 첫 요청부터 13,322 대 10,288로 3,034토큰 더 실렸어요. 이 조건에만 실리는 것은 CLAUDE.md, GitNexus 스킬 6개의 설명, 설명을 아직 싣지 않은 MCP 도구 17개의 이름, git 상태에 보이는 미추적 파일 등이에요.
두 번째 요청에서는 차이가 12,091토큰으로 커졌어요. ToolSearch가 불러온 impact·context 설명(MCP 서버가 내놓는 설명과 입력 형식을 합쳐 20,881자와 7,527자)이 이때 10,888토큰으로 들어가 그 뒤 요청마다 다시 실렸고, 위험도·영향받는 실행 흐름까지 담은 impact 결과(10,742자)는 세 번째 요청부터 더해졌어요.
실습 2: 질문 4개를 세 번씩
나머지 질문은 이래요. 모두 tests와 docs를 빼고 묻고, 끝에 "코드는 수정하지 마."가 붙어요.
- q1:
_render_buffer가 정의된 곳(1곳)과 직접 호출하는 함수(4개) - q3:
rich.print("[bold]hi[/bold]")에서rich/markup.py의_parse까지 호출 경로(6단계) - q4:
Highlighter를 직접·간접 상속하는 클래스(9개)와highlight오버라이드 여부
summarize.py는 정답 목록의 항목이 모두 답에 있으면 정답으로 쳐요. 기본 규칙은 함수 이름이 있는 줄이나 그 위 40줄, 아래 3줄 안에 파일 이름이 있어야 하고, 느슨한 규칙은 답 어디에든 있으면 됩니다.
py312 summarize.py
q1 기준: 정답 3/3, MCP 쓴 회차 0, 입력 56,662, 요청 5번, num_turns 6, Read 2
q1 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 82,304, 요청 5번, num_turns 5, Read 0
q2 기준: 정답 3/3, MCP 쓴 회차 0, 입력 48,934, 요청 4번, num_turns 5, Read 1
q2 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 98,451, 요청 4번, num_turns 5, Read 0
q3 기준: 정답 3/3, MCP 쓴 회차 0, 입력 50,793, 요청 4번, num_turns 7, Read 4
q3 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 109,976, 요청 6번, num_turns 6, Read 0
q4 기준: 정답 2/3, MCP 쓴 회차 0, 입력 57,158, 요청 5번, num_turns 5, Read 0
q4 GitNexus: 정답 3/3, MCP 쓴 회차 3, 입력 71,477, 요청 4번, num_turns 5, Read 0
전체 기준: 12회, 정답 11/12 (느슨한 규칙 11/12), MCP를 쓴 회차 0, CLI를 쓴 회차 0
중앙값: 입력 50,988 (캐시 쓰기 4,364, 캐시 읽기 45,649), 출력 1,887, 요청 4번, num_turns 5, 23.4초, $0.0839
도구 호출 중앙값: 전체 4, Bash 2, Grep 1, Glob 0, Read 1, 읽은 파일 1개, GitNexus MCP 0
전체 GitNexus: 12회, 정답 12/12 (느슨한 규칙 12/12), MCP를 쓴 회차 12, CLI를 쓴 회차 0
중앙값: 입력 94,520 (캐시 쓰기 7,252, 캐시 읽기 88,677), 출력 1,896, 요청 5번, num_turns 5, 25.7초, $0.1213
도구 호출 중앙값: 전체 4, Bash 0.5, Grep 1, Glob 0, Read 0, 읽은 파일 0개, GitNexus MCP 1
GitNexus 조건은 12번 모두 MCP 도구를 썼어요(q1 context, q2 impact, q3 trace, q4 cypher). CLI와 스킬은 한 번도 쓰지 않았고, 입력 중앙값은 네 질문 모두 GitNexus 쪽이 컸어요.
조회 여러 번 대신 한 번이면 된다는 README의 주장은 GitNexus MCP 호출 중앙값 1로는 맞았어요. 하지만 Claude가 그 결과를 Grep·Bash로 다시 확인해 도구 호출 중앙값은 4 대 4, 요청 수 중앙값은 4 대 5였어요. Read로 연 파일은 1개에서 0개로 줄었지만 두 조건 모두 Bash sed -n으로도 코드를 봤어요.
자동 판정에서 틀린 한 번은 기준 q4로, examples/log.py의 RequestHighlighter를 빠뜨렸어요. 답을 직접 읽을 때는 표의 호출 관계가 맞는지로 셌어요. 기준 q1 한 번(22번째)은 표에 호출 세 곳을 _write_buffer가 아닌 _check_buffer 안이라고 적어 오답으로 셌고, 그래서 기준은 10번이에요.
GitNexus q3 세 번은 trace가 rich.print에서 _parse까지 잇지 못해 소스를 읽어 경로를 이었는데, 요약 경로에 get_console을 한 단계로 넣었어요. 표에는 get_console이 돌려준 Console의 print를 호출한다고 맞게 적어서 정답으로 셌지만, 이것까지 오답으로 보면 10 대 9로 뒤집혀요.
도구 설명의 무게는 같은 q1에서 context만 불러온 14번째 호출과 context·impact를 함께 불러온 1번째 호출을 견주면 보여요.
steps out/14-b-q1.jsonl
steps out/01-b-q1.jsonl
13300 -> 16224 -> 17597
13301 -> 24189 -> 25060 -> 25581 -> 26006
두 호출 모두 실제로 부른 GitNexus 도구는 context 한 번이었어요. 쓰지 않은 impact 설명까지 불러온 쪽은 두 번째 요청이 7,965토큰 더 컸고, 요청도 5번 대 3번이라 입력 합계가 47,121 대 114,137로 갈렸어요.
정리
범위: Claude Code 2.1.309의 claude -p, Opus 5.5(claude-opus-5-5), effort medium, GitNexus 1.6.12 공식 Docker 이미지와 기본 인덱스, 도구 Bash,Read,Grep,Glob,ToolSearch,Skill, rich v15.0.0 질문 4개를 조건마다 3번씩 모두 24번.
- 자동 판정 정답은 GitNexus 12번, 기준 11번이었고, 직접 읽으면 판정 기준에 따라 10 대 12에서 10 대 9까지 달라졌어요. "코드를 놓치지 않는다"는 차이는 3번씩으로는 가릴 수 없을 만큼 작았습니다.
- GitNexus MCP는 12번 모두 쓰였고 Read로 연 파일은 줄었지만, 도구 호출 수는 같았고 입력 토큰 중앙값은 50,988에서 94,520으로 늘었어요. 읽은 파일 수보다 요청마다 다시 실리는 입력을 보세요.
- 첫 요청에 3천 토큰 남짓이 더 실리고,
ToolSearch로 불러온 설명은 그 뒤 요청마다 다시 실려요.impact설명 하나가 요청마다 약 8천 토큰이니(q1 두 호출 비교) 요청이 많은 작업일수록 커집니다. - MCP 서버가 그래프 DB를 열고 있으면 훅은 그래프 문맥 대신 안내 한 줄만 보냈어요(12번 모두). 훅에 기대한다면
--include-hook-events로 실제로 붙는 내용을 확인하세요. - 인덱스는 21초, 112MB였고 코드가 밖으로 나가지 않았어요.
trace는 객체 타입을 모르는 호출(write_console.print)을 잇지 못했으니, 호출 경로 답은 소스로 확인하세요.