Skip to content

v0.2.21 — 감사 큐 A7~A20 (큐가 비었다)

Choose a tag to compare

@xzawed xzawed released this 06 Sep 02:04
· 10 commits to main since this release
84deba0

v0.2.21 — 감사 큐의 나머지 열네 건

2026-09-06. v0.2.20이 감사 큐 상위 6건(A1A6)을 닫았고, 이 릴리스가 **나머지 14건(A7A20)**을
닫는다. docs/10-service-audit-queue.md의 A 섹션은 이로써 비었다.

전 항목 동일한 절차를 지켰다 — 배포 번들로 재현 → 실패하는 테스트 먼저 → 수정 → 같은
페이로드로 재측정 → Grok 반증
(루트 CLAUDE.md "작업 수행 방법"). 아래 수치는 전부 그
재측정값이다. 유닛: 530 → 555 passed.


1. hook의 방어 계층이 출하 상태에서 무장돼 있지 않았다 (A7)

resolveHookMode명시적이고 대소문자까지 정확한 GROK_BUILD_AUTH_MODE만 인정하고
나머지를 unknown으로 읽었다. 서버의 resolveAuthMode는 trim + lowercase 하고 미설정을
subscription으로 읽는다. 그런데 .mcp.json에는 env 블록이 없다 — 즉 미설정이 곧 출하
구성
이고, subscription deny 분기는 실제 사용자 환경에서 한 번도 무장된 적이 없었다.
그 분기를 짚는 테스트들이 전부 변수를 손으로 세팅해서 녹색이었던 것이 이 결함을 가렸다.

배포 번들 실측(grok 설치됨, auth.json 없음, 동일 페이로드):

GROK_BUILD_AUTH_MODE before after
미설정 (출하 구성) ALLOW DENY
subscription DENY DENY
Subscription ALLOW DENY
subscription ALLOW DENY
api ALLOW ALLOW
잘못된 값 ALLOW ALLOW

trim/lowercase를 여기서 다시 구현하지 않고 서버의 파서에 위임했다 — "이 모드가 뭔가"의
정의를 하나로 만들어야 같은 드리프트가 재발하지 않는다. 다만 throw는 물려받지 않는다:
잘못된 값은 애초에 서버를 못 뜨게 하므로(A12), 거기서 "grok login을 실행하세요"라고
안내하면 엉뚱한 곳을 고치게 만든다. 그 경우만 unknown → allow로 남는다.

남는 위험은 코드 주석에 GROK_BIN_DIR·GROK_HOME같은 형태로 적었다 —
GROK_BUILD_AUTH_MODE=api를 서버 전용 .mcp.json env에만 넣은 사람은 여기서 subscription으로
읽혀 오차단될 수 있다. 해법도 같다(실행 환경에 export). 버전 키 캐시라 손편집한 .mcp.json
어차피 업데이트를 넘기지 못한다.

영구 고정: 서버가 모드를 해석하는 모든 입력에 대해 hook이 같은 값을 내야 한다는 속성
테스트. 케이스별 나열이 아니라 속성으로 박아야 다음 편집에서 두 파서가 다시 갈라지지 않는다.

2. worktree diff가 자기 자신과 모순됐다 (A8)

한 응답 안에서:

filesChanged: ["tracked.txt", "hello.txt", "newdir/nested.txt"]
diffStat:     "tracked.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)"

권위 있어 보이는 쪽이 틀렸다. git diff는 어떤 형태로도 untracked를 못 본다. 그리고
--always-approve에서 grok이 가장 많이 만드는 것이 신규 파일이다.

세는 유일한 방법은 스테이징인데, diff는 스테이징하면 안 된다 — 인덱스를 남겨두면
나중의 apply나 오너 자신의 git commit이 집는 것이 달라진다. 큐가 제안한
add -A → stat → reset은 창(window)이 있는 변형이고, 그 사이에 죽으면 스테이징된 채로
남는다. 그래서 GIT_INDEX_FILE로 worktree 밖의 임시 인덱스에 대고 같은 뷰를 만든다.
저장소의 인덱스·ref·파일은 하나도 쓰지 않는다. 남기는 흔적은 스테이징 내용의 loose blob뿐이고
(참조되지 않으며 gc가 정리) 그 사실을 주석에 적었다.

before after
diffStat 1 file changed, 1 insertion(+), 1 deletion(-) 3 files changed, 3 insertions(+), 1 deletion(-)
worktree 상태 호출 전후 git status --porcelain -uall 바이트 동일

tmpdir을 못 쓰거나 아주 큰 worktree에서 add -A가 예산을 넘기면 tracked-only stat으로
폴백한다 — 부분 답이 실패한 검사보다 낫다. 테스트는 stub이 아니라 실제 git이다. 결함이
사는 곳이 "git이 실제로 뭐라고 보고하는가"이기 때문이다.

3. 취소된 확인 프롬프트가 성공으로 보고됐다 (A9)

grok memory clear --global(stdin 없음, 격리된 GROK_HOME) 실측:

{ "status": "ok", "exitCode": 0, "stdoutTail": "…Are you sure? [y/N] Cancelled.\n" }   isError: false

아무것도 지우지 않은 파괴적 명령이, 성공한 것과 같은 모양으로 돌아왔다. exit code는
거짓말이 아니다(정말 0으로 끝난다). 문제는 둘을 가르는 유일한 근거가 stdoutTail 안의
산문이었다는 것 — 그리고 stdoutTail마지막 4000자라, 삭제 대상 목록이 길면 그 근거가
응답에서 밀려난다.

그래서 판정은 잘리기 전 전체 출력에서 하고, 구조화 필드로 낸다: cancelled: true +
원인(stdin 없음 → 기본 N)과 해법(그 서브커맨드의 확인 플래그 — 사용자 대신 붙이면 안 된다)을
담은 message.

두 마커를 모두 요구한다. Cancelled.만으로는 평범한 출력이고(sessions search cancelled
매칭 세션을 낸다), 프롬프트만으로는 응답됐을 수 있다(-y 실측: 같은 프롬프트 줄 뒤에 완료
메시지). Grok 검증 5케이스 중 실제로 뭔가 바꾼 두 케이스("Cleared 3 files", "Deleted 3
sessions")는 false로 나온다.

4. isError가 양방향으로 틀렸다 (A10)

grok_cli {"args":["dashboard"]}       isError false, status "blocked"
grok_build_status (api 모드, 키 없음)  isError true, 13개 필드가 전부 채워진 페이로드 옆에서

docs/07을 따르는 소비자는 첫 번째를 "출력 없는 성공"으로 읽고 — spawn조차 안 된 거부된
명령
이다 — 두 번째는 통째로 버린다. 하필 그 안에 지금 불평하고 있는 auth를 고치는
nextSteps가 들어 있다.

isError는 "호출이 실패했는가"가 아니라 **"요청한 일이 일어났는가"**에 답한다:

before after
blocked 명령 false true
취소된 확인 (A9) false true
api 모드 status, 키 없음 true false (13개 필드·nextSteps 보존)
정상 성공 false false

grok_auth_check의도적으로 !result.ok를 유지한다 — 출력 전체가 판정이라 소비자가
잃을 다른 필드가 없고, isError가 "Grok 준비됐나?"에 대한 가장 짧은 참된 답이다. 그 비대칭을
테스트로 박아 두었으니 이제 실수가 아니라 결정으로 읽힌다.

5. 한 단어 오타가 60초를 태웠다 (A11)

["sesions"]          status timeout, 예산 전부 소진, stderrTail = 읽을 수 없는 ANSI TUI 프레임
["sesions", "list"]  status error, exit 2, 793ms, "unexpected argument 'list' found"

grok의 usage는 grok [OPTIONS] [PROMPT] [COMMAND]다. 혼자 있는 미지의 토큰은 프롬프트이고,
-p 없는 프롬프트는 대화형 UI를 띄운다 — 버퍼드 spawn은 그걸 기다리는 수밖에 없다. import
이미 정확히 이 메커니즘 때문에 막혀 있었고, 이 수정은 손으로 고른 단어 하나를 특별 취급하는
대신 그것을 일반화한다.

20,729ms timeout → 611ms blocked. 대조군 재실행: sessions list·models·version·
--version 전부 여전히 spawn되고 exit 0.

이 목록은 allowlist라 낡는 방향이 위 denylist와 반대다 — 스냅샷 이후 grok이 추가한
서브커맨드는 여기서 거부된다. 두 가지가 그 비용을 낮춘다. (1) 서브커맨드 슬롯이 불확실하면
(미인식 플래그가 먼저 왔으면) 규칙이 물러선다 — 그 토큰은 플래그의 값일 수 있고, 동작하는
명령을 오차단하는 쪽이 느린 실패 한 번보다 나쁘다. (2) 거부 문구가 "최근 추가된 서브커맨드라면
이 래퍼가 모르는 것"이라고 직접 말하고 터미널로 안내한다.

6. 설정 실수가 스택 트레이스로 보고됐다 (A12)

GROK_BUILD_AUTH_MODE=nonsense: 9개 도구와 모든 /grok:*가 한꺼번에 사라지고 클라이언트에는
일반 연결 실패만 보인다. 그러니 그 로그가 오너의 유일한 단서인데, 로그가 먼저 보여주는 건
번들 라인 번호(dist/index.js:13327)와 6개 프레임이었다. 무엇을 고쳐야 하는지 말하는 한 줄은
그 맨 위에 묻혀 있었다.

빠르게 실패하는 것은 바뀌지 않았고 바뀌면 안 된다 — 조용한 기본값은 잘못된 과금 모드로
돌게 만들고, 그것이 이 서버가 절대 만들면 안 되는 결과다. 바뀐 것은 보고뿐이다.

grok-build MCP server did not start: Invalid GROK_BUILD_AUTH_MODE: "nonsense". Expected "subscription" or "api".
exit 1, 한 줄, 프레임 없음

그 외의 에러는 스택을 그대로 낸다 — 거기서는 프레임이 곧 진단이다. 포매터는 throw 바로 옆
config.ts에 두어 프로세스 없이 테스트 가능하고, index.ts는 자기 docstring이 약속한 대로
배선만 남는다.

7. api 모드가 아무 문자열이나 "준비됨"이라고 했다 (A13)

XAI_API_KEY="not-a-real-key-at-all"ok: true, "API 키 인증 준비됨." 이 경로는 xAI에
접속하지 않는다. 즉 "준비됨"은 검사가 할 수 없는 주장이었다. 검사하는 것이 존재 여부이니
존재 여부를 말한다
— "키가 설정돼 있습니다 — 유효성은 검증하지 않았습니다."

ok는 일부러 true로 남긴다. 존재는 이 서버가 공짜로 확인할 수 있는 마지막 사실이고, 근거 없이
false로 뒤집으면 멀쩡한 키를 막는다. 검증 호출도 넣지 않았다 — 이 probe는 게이트되는 모든 호출
앞에서 돌고, 하필 사용자가 과금에 신경 쓰는 그 모드에서 네트워크 왕복은 청구 대상이다.

여기에 감사의 더 날카로운 발견을 덧붙였다: 죽은 키면 grok이 구독 세션으로 넘어간다. 그러면
billingmetered_api인데 실제로는 종량제로 청구되지 않는다(billing은 설계상 설정된 mode의
파생값이지 관측값이 아니다 — 절대 원칙 #1). 세션 파일이 바로 거기 있으면 그렇게 말해주는 데
AuthDeps에 이미 있는 stat 한 번이면 된다.

8. plan이 스키마가 거부하겠다고 한 필드를 조용히 버렸다 (A14)

plan은 { prompt, cwd, timeout_ms }additionalProperties: false를 광고했고 delegate는 10개를
광고했다. 그리고 zod는 나머지를 거부하는 대신 벗겨냈다. worktree: true
model: "grok-code"를 실은 호출이 isError false · status completed · worktreePath 없음으로
돌아왔다. 계약을 양방향으로 동시에 깬다 — 스키마는 거부를 약속하고, 런타임은 거부도 동작도 주지
않으므로 호출자는 아무 말도 듣지 못한다.

이제 verify와 같은 필드 집합을 받고 전부 전달한다.

같은 페이로드 before after
worktree: true 버려짐 worktreePath 반환, completed, planWroteFiles: false
model: "grok-code" 버려짐 grok_error (grok은 grok-4.6 / grok-4.5를 제공 — 값이 정말 틀렸고, 이제 호출자가 그걸 안다)

두 번째 줄이 첫 번째만큼 중요하다. 오타 난 모델은 전에는 조용히 기본 모델로 돌면서 성공처럼
보였다.

worktree가 특히 중요한 이유: --permission-mode plan읽기 전용이 아니다(1.0.13이
무시한다 — 계약 §6, planWroteFiles가 존재하는 이유). 격리가 plan의 실제 봉쇄 수단이다.
docs/04는 자기 경고 두 줄 위에서 "worktree/sandbox 없음 — 편집 안 함"이라고 말하고 있었다.

9. inspect가 의미 있는 쪽을 버렸다 (A15)

grok inspect는 7,269자를 냈고 우리는 마지막 4,000자를 남겼다 — 잘린 조각은 플러그인 명령
목록 한가운데에서 시작한다. inspect가 존재하는 이유(grok home·버전·cwd·git root·모델·auth·각
설정의 출처)는 전부 앞에 찍히고, 정확히 그게 버려졌다. help도 같은 모양이다 —
이 결함을 고친 바로 그 세션에서 grok --help가 같은 규칙에 머리를 잃었다.

이제 inspect와 help는 head를, 나머지는 tail을 남긴다(명령 로그는 결과로 끝나니까).
어느 쪽을 받았는지는 stdoutKept로 보고하며, 실제로 잘렸을 때만 붙는다.

before: "rok [claude] [collides with /plan → /grok:plan]…"
after:  "Environment / Version: 1.0.13 [stable] / CWD: … / Git root: …"

A15의 나머지 절반은 inspect --json(~81 KB)이었다 — 어느 쪽 4,000자를 남겨도 JSON으로 파싱되지
않는다. max_chars(상한 100,000)가 문서 전체가 정말 필요한 호출자에게 토큰 비용을 지불하고
가져갈 길을 준다. 실측: max_chars: 20000 → 7,269자 전부, stdoutTruncated 없음. 기본 예산은
그대로다.

10. 빈 범위 결과가 빈 이력으로 읽혔다 (A17)

디스크에 1,858행이 있는 상태에서:

grok_build_usage {"cwd":"…/nowhere-at-all"}
→ total 0, "아직 위임 이력이 없습니다. `/grok:setup` 후 샘플 위임으로 첫 성공을 만들어 보세요."

필터 아래의 0과 전체의 0은 다른 사실인데 이 문장은 두 번째를 말한다. 데이터 손실처럼 읽히고,
멀쩡히 동작하는 설치에 setup을 다시 돌리라고 보낸다. 이제 디렉터리를 이름으로 말하고 나머지
이력이 그대로임을 밝히며, tip은 실제 원인(cwd 필터 없이 호출 / 경로 정확 일치)을 가리킨다.
status 대시보드도 같은 headline을 물려받는 것을 확인했다. 범위 없는 출력은 그대로:
"위임 1859건 · 성공률 71.1% · …".

11. 문서 (A16 · A18 · A19 · A20)

  • A16filesChanged는 git porcelain 차집합이라 저장소 밖에서는 성공해도 항상 비어
    있다.
    새 사용자가 그 빈 목록을 처음 만나는 곳이 tour의 첫 성공이라, tour.md·setup.md
    이제 그렇게 말한다. (이 세션이 반복해서 겪었다 — git이 아닌 스크래치에서 돌린 Grok 리뷰는
    verdict.md를 쓰면서도 filesChanged: []였고, git 저장소 안에서 돌린 한 번만 파일을 보고했다.)
  • A18 — logout은 플러그인 안에서 일방통행이다. /grok:login은 설계상 거부되므로
    돌아오는 길은 사용자가 지금 앉아 있지 않을 수도 있는 터미널이다. logout.md가 그것부터
    말하고, 같은 턴에서의 명시적 확인을 요구하며, 더 큰 작업의 한 단계로 돌리거나 /grok:status
    제대로 진단해 줄 문제를 "리셋"하려고 돌리는 것을 금지한다.
  • A19 (오너 판단) — 배포 표면 6곳의 맨 상대 경로를 **${CLAUDE_PLUGIN_ROOT}/docs/…**로
    바꿨다. .mcp.json·hooks/hooks.json이 이미 쓰는 메커니즘이고, 설치된 플러그인 캐시 안에서
    해석되므로 마켓플레이스 공개 여부에 의존하지 않고 main을 향해 썩지도 않는다.
  • A20 — 드리프트한 커맨드 문서 3곳. 각각 1.0.13에 다시 물어보고 고쳤다: mcp에는 정말
    enable/disable이 있고(문서는 6개 중 4개만 적고 있었다), sessions.md는 1.0.5에 고정돼 있었고,
    plan.md·verify.md는 서버가 절대 경로를 요구하고 상대 경로를 spawn 전에 거부하는데
    "현재 작업 디렉터리"라고 적고 있었다.

Grok의 반증 (6/9 항목)

각 수정마다 정적 분석 한 건씩, 루트 CLAUDE.md의 조리법대로(verdict.md에 쓰게 하고, 셸 명령
금지, 주장 하나, 빈 디렉터리에 함수 하나). A7의 두 번째 패스는 내 주장을 반증했다 — "한
번의 조치가 항상 게이트를 연다"는 것. grok 미설치 그리고 세션 없음이면 설치 후 로그인까지
두 단계가 필요하니 문자 그대로 맞는 지적이다. 번들로 그 사슬을 실측했고(deny install → deny
login → allow) 전제조건이 둘이면 조치도 둘인 게 맞으며 각 메시지가 자기 것을 정확히 말한다 —
결함이 아니므로 코드는 바꾸지 않았다. 나머지는 전부 확인(CLAIM_CONFIRMED)이었다.

프롬프트가 넓으면 답이 오지 않는다는 것도 다시 실측됐다: 세 번의 timeout은 전부 행이 많거나
칸당 두 가지를 물은 표였고, 줄이자 1분 안에 돌아왔다.

이 릴리스가 손대지 않은 것

docs/10 C 섹션(실측으로 견고함)은 그대로다 — 과금 보장, 자동 커밋 금지, spawn 이전 검증,
worktree baseDir 가드, timeout 처리, 집계 산술. B 섹션(측정 불가 5건)도 그대로이며, 그중 B4는
이 머신 설치본이 갱신된 뒤에야 닫힌다.