Skip to content

Releases: xzawed/claude-grok-build-plugin

v0.2.23 — 붙여 쓴 프롬프트도 프롬프트다 (A24·A25)

Choose a tag to compare

@xzawed xzawed released this 06 Sep 11:13
8ea1cf3

v0.2.23 — 붙여 쓴 프롬프트도 프롬프트다 (전체 코드 감사 A24·A25)

2026-09-06. 릴리스된 산출물과 소스 전체(19모듈 3,930줄)를 Grok과 함께 검증한 결과다.
채점 대상은 사용자가 실제로 실행하는 것이었다 — 태그 v0.2.22distorigin/main
dist가 바이트 동일함을 먼저 확인했으므로, 캐시 번들 채점이 곧 릴리스 채점이다.

두 결함 모두 배포 번들에서 재현한 뒤 고쳤고, 같은 페이로드로 다시 쟀다.


A24 — 인증 게이트를 통과하고 이력에도 남지 않는 유료 턴

-p는 값을 붙여서도 받는다. clap은 -p2+2-p 2+2와 똑같이 읽는다. 그런데 이 래퍼의
짧은 플래그 판정은 /^-[A-Za-z][A-Za-z]+$/토큰 전체가 알파벳이어야 했다. 그건 붙인 값이
우연히 알파벳일 때만 참이므로, 숫자·슬래시·점을 포함한 값은 기록기와 인증 게이트 양쪽에서
동시에 사라졌다.

grok_cli에는 서버 측 checkAuth가 없다 — hook.ts가 스스로 적어둔 대로 PreToolUse hook이
유일한 게이트
다. 그 게이트가 열려 있었다.

실측 (배포 번들 + 배포 dist/hook.js):

확인 수정 전 수정 후
hook, 로그아웃 · ["-p","2+2"] deny deny
hook, 로그아웃 · ["-p2+2"] allow deny
hook, 로그아웃 · ["-p/tmp/x"] · ["-pfoo.txt"] allow deny
hook · ["sessions","list"] · ["--version"] · ["-wq","name"] allow allow (유지)
번들 · ["-p2+2","--output-format","json"] status: ok, grok이 text:"**4**" · stopReason: end_turn · 실제 sessionId — 유료 턴 동일하게 성공
그 턴의 promptRun / 이력 부재 / 행 없음 (2→2) true / 행 기록 (promptPreview: "2+2")

즉 A2가 고친 피해(위임이 /grok:usage·/grok:status에서 사라짐)가 다른 문으로 재발
있었고, 이번에는 인증 게이트까지 함께 열려 있었다.

수정: 판정 대상을 "토큰 전체"에서 **"앞쪽 옵션 문자열"**로 바꿨다. 붙인 값은 옵션이 아니므로
판정에서 제외되고(-s01a0p619p는 데이터다), -p=VALUE도 clap과 같게 읽는다.

Grok의 몫: 게이트 구멍을 독립적으로 먼저 찾았다 — 실제 코드를 주고 반증을 시켰더니
-p0, -p/tmp/x, -pfoo.txtGAP_FOUND를 돌려줬다. 수정 후 다시 검토시켰을 때는 누락을
하나도 찾지 못했고
, 대신 -mp(= --model p)를 과잉 게이트라고 지적했다. 그건 기각했다 —
실측 결과 수정 전후가 동일하고(옛 규칙도 true), 모듈 주석과 기존 테스트가 이미 명시한
의도적 정책이다("과잉 게이트는 로그인한 사용자에게 비용이 없지만, 누락은 검사 없이 쿼터를 태운다").

A25 — cwd를 안 넘긴 grok_cli 이력이 cwd: ""로 기록

runGrokCli는 생략된 cwd를 process.cwd()로 채우는데, 이력을 쓰는 호출부는 그걸 알 수 없어
cwd ?? ''를 적었다. 같은 디렉터리에서 돈 위임 2건 중 1건만 그 디렉터리로 집계됐다.

질의 수정 전 수정 후
전체 2건 (recent[].cwd = ["D:/…", ""]) 2건, 둘 다 실제 경로
그 디렉터리로 필터 1건 2건

A17이 읽는 쪽에서 고친 문제를 쓰는 쪽에서 만들고 있었던 셈이다.

수정: 호출부에서 기본값을 다시 유도하지 않는다 — 실행이 자기가 쓴 디렉터리를 보고한다
(GrokCliResult.cwd, 필수 필드). 호출부에 cwd ?? process.cwd()를 적는 대안은 기각했다:
A7이 정확히 그것(하나의 기본값을 두 곳에서 유도하다 어긋남) 때문에 생겼다.

Grok 판정은 이 수정안에 FIX_SOUND였고, 유일한 공격(cwd:""을 넘기면 여전히 "")은
실측으로 반증했다 — 빈 문자열은 spawn 전에 거부되고 이력 행도 남지 않는다.


결함이 아닌 것 (감사가 확인하고 넘어간 것)

  • 레닥션은 사람이 외우는 비밀번호를 못 가린다 (password = Password1!, 12자 하한).
    Grok이 LEAK_FOUND를 냈으나 4건 중 2건은 검토용 발췌본이 실제 키 목록보다 짧아서 생긴
    오탐이었고(실제 코드는 OPENAI_API_KEY·GH_TOKEN 모두 마스킹), 남은 2건은
    CLAUDE.md 절대 원칙 4와 docs/04가 이미 **"마스킹은 완화이지 보장이 아니다"**로 명시한
    설계상 한계다. 하한을 낮추면 A6 라운드에서 Grok이 잡았던 과교정(45개 중 19개 훼손)이 그대로
    돌아온다.
  • 표면 일치: 도구 9개 전부 등록·전부 additionalProperties: false, 커맨드 27·스킬 2·
    에이전트 1이 참조하는 도구 이름 9개가 모두 실재, hooks/hooks.json 래핑 정상,
    accept-release 10/10.

유닛

572 → 578. 수치는 npm test가 원천이다.

v0.2.22 — 광고한 계약을 실제로 지키게 한다

Choose a tag to compare

@xzawed xzawed released this 06 Sep 03:12
dca344d

v0.2.22 — 광고한 계약을 실제로 지키게 한다

2026-09-06. 같은 날 v0.2.21이 기능 감사 큐(A7~A20)를 비운 뒤, 문서·코드 전수 감사를 돌렸다.
질문이 달랐다 — "각 기능이 동작하는가"가 아니라 "우리가 광고한 계약이 실제로 지켜지는가".
찾은 3건 중 A23은 발견과 동시에 닫혔고, 이 릴리스가 나머지 둘이다. 유닛 555 → 568.


1. 9개 중 8개 도구가 자기가 광고한 스키마를 지키지 않았다 (A21)

9개 도구 전부가 스키마에 additionalProperties: false를 싣는다. 실제로 거부하던 것은
grok_build_route 하나뿐이었다
— A1이 .strict()를 붙인 그 도구다. 나머지 8개는 미지의 키를
받아들이고 조용히 벗겨냈다. 즉 필드 이름을 오타 내면, 그 필드가 없는 채로 호출이 성공한다.

실사용 피해를 끝까지 쟀다. throwaway git repo에서 worktreee(e 3개)로 delegate:

before: isError false · status completed · worktreePath 없음 · filesChanged ["typo-probe.txt"]
        → 격리를 요청했는데 grok이 사용자의 작업 디렉터리를 직접 고쳤고, 응답은 성공이라 말한다
after : isError true · unrecognized_keys: ["worktreee"] · 작업 디렉터리에 아무것도 쓰이지 않음

안전 강등은 두 필드다. 처음엔 worktree뿐이라고 썼는데 Grok이 반증했다sandbox
같은 모양이다(오타 → --sandbox 미전달 → 프로파일 없이 실행). 확인해 보니 delegate.ts:391
실제로 플래그를 넘기고 Linux/macOS는 커널이 강제한다. 지적이 맞았고, 그래서 이 릴리스의 근거가
하나 더 늘었다. 나머지(timeout_ms·model·effort·resume·continue)는 취향이 무시될 뿐
보호가 사라지지는 않는다.

오차단 위험은 추측하지 않고 쟀다. MCP 스펙대로 params._meta에 실린 메타데이터는 이미
strict였던 route에서도 통과하고, arguments 안에 잘못 넣은 경우만 거부된다 — 스펙을 지키는
클라이언트는 영향이 없다. 배포 표면도 확인했다: examples/orchestrator-consumer.md(문서화된
기계 소비자)가 보내는 것은 task·signals·metered_billing·prompt·cwd·worktree뿐이고
전부 스키마 안에 있다.

Grok 정적 검증: .strict()가 바꾸는 것은 미지의 키 하나뿐이다. 음수 timeout_ms도, null
값도 전에도 후에도 똑같이 거부된다(BEFORE/AFTER 5행 대조, CLAIM_CONFIRMED).

새 도구를 추가할 때 .strict()를 빠뜨리지 말 것 — 광고하는 스키마와 강제하는 스키마는 같은
스키마여야 한다. 그 이유를 server.ts 등록부 맨 위 주석에 적어 두었다.

2. redactSecrets가 npm 토큰을 놓쳤다 (A22)

튜닝에 쓰지 않은 코퍼스로 재측정했다: 시크릿 18종 중 16종 마스킹, prose 12종 중 0종 훼손.
이전 라운드에서 Grok이 잡았던 과교정(api_key: required 류를 망가뜨리던 것)은 완전히 사라진 상태였다.
새는 것은 npm_... 하나였다.

절대 원칙 #4가 이미 "마스킹은 완화이지 보장이 아니다"라고 못박고 있으므로 계약 위반이 아니라
커버리지 한 칸이다. A6이 같은 종류를 같은 방식으로 닫았으므로 같은 방식으로 닫았다.

before: //registry.npmjs.org/:_authToken=npm_AbCdEf0123456789AbCdEf0123456789Ab
after : //registry.npmjs.org/:_authToken=<redacted>

레지스트리 호스트는 남는다 — 행을 통째로 지우는 마스킹은 누군가 결국 꺼 버린다(A6의 판단 그대로).

30자 하한이 이 규칙의 전부다. npm_은 흔한 식별자 접두사이고(npm_config_registry,
npm_package_version, npm_lifecycle_event), 하한이 없으면 A6 라운드의 과교정을 그대로
재현한다. 그 세 개를 회귀 테스트로 박았다. Grok 정적 검증도 같은 결론이다 — 실토큰 2행만
매치하고 환경변수 3행은 매치하지 않는다.

재측정 최종: leaked 0/17 · mangled 0/15.

⚠️ 처음엔 새는 것이 둘로 보였다. 다른 하나(ssh-rsa AAAA...)는 제 시험 케이스가 틀린
이었다 — 공개키는 시크릿이 아니고, 그걸 가리는 것이야말로 과교정이다. 나중에 누가
"고치지" 않도록 테스트로 못박았다.

3. 같은 감사에서 발견과 동시에 닫힌 것 (A23)

prompt-flags.ts가 CLAUDE.md 컴포넌트 지도에서 빠져 있었다(19개 중 18개만 적혀 있었다).
장식이 아니다 — hook.tsgrok-cli.ts가 둘 다 import 하고, hook 번들에 delegation 엔진이
딸려 들어오지 않게 하려고 일부러 떼어낸 리프 모듈
이다(v0.2.20). 지도만 보고 일하는 다음
세션은 그 의도를 못 본다. 한 줄이라 v0.2.21 감사 기록 커밋에서 바로 닫았다.


감사가 확인한 것 (다시 열지 않도록)

  • 도구 9개·커맨드 27개 수치가 주장되는 모든 곳에서 일치
  • 모든 live 입력 필드가 docs/04자기 도구 섹션에 문서화됨
  • 모든 /grok:<verb> 참조에 커맨드 파일 존재, 배포 표면에 없는 도구 이름 0건
  • 값 수준 검증은 원래 견고했다action은 진짜 enum(obliterate 거부), 타입 불일치와
    음수 limit도 거부. 새던 것은 미지의 키뿐이었다
  • 절대 원칙 10개가 소스와 번들 양쪽에서 성립(과금 env 정제 · --no-auto-update ·
    --always-approve/plan · exec 문자열 조립 없음 · 자격증명 미기록 · 자동 커밋 없음 ·
    hook 번들에 엔진 없음 · porcelain -z · 시간 제한)
  • 계약 §6을 오늘 다시 쟀다 — grok 1.0.13은 여전히 --permission-mode plan을 무시하고 파일을
    썼다. planWroteFiles 방어는 그대로 필요하다

자동 검사 1차 결과 24건은 전부 정규식 오탐이라 보고에서 뺐다 — grok_error·
grok_not_installed는 도구가 아니라 status 값이고, 끊긴 링크는 전부 "SHIPPED — historical
record"
배너가 붙은 문서 안에 있었다.

손대지 않은 것

docs/10 C 섹션(실측으로 견고함)과 B 섹션(측정 불가 5건)은 그대로다. B4(GUI 슬래시 커맨드
경로)는 오너가 Claude Code를 재시작한 뒤 /grok:status 한 번으로 닫힌다.

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

Choose a tag to compare

@xzawed xzawed released this 06 Sep 02:04
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 가드, timeo...

Read more

v0.2.20 — 감사 큐 A1~A6

Choose a tag to compare

@xzawed xzawed released this 06 Sep 02:00
ca024ca

v0.2.20 — 감사 큐의 상위 다섯 건

2026-09-05. docs/10-service-audit-queue.mdA1~A5. 전부 배포 번들을
.claude/tools/mcpcall.mjs로 직접 구동해 재현 → 수정 → 재측정했고, 수치는 전부 그 실행에서
나온 것이다.

이 다섯 건의 공통점: 아무 말도 하지 않는 실패였다. 위험 판정이 조용히 내려가고, 실제 편집이
이력에 남지 않고, 다른 디렉터리에 쓰고도 성공이라 보고하고, 되돌릴 수 없는 삭제가 ok:true
끝나고, 정리 도구가 정리 대상만 골라 건너뛰었다.


A1. 명시 signals 하나로 위험 게이트가 꺼졌다 (routing.ts)

호출 0.2.19 0.2.20
{task:"rotate the OAuth client secret and update the auth middleware"} HIGH HIGH
+ signals:{security:false} MEDIUM HIGH
+ 전 필드 채운 구조체 LOW HIGH

v0.2.19는 destructive·production만 막았고 나머지 다섯 HIGH 키는 그대로 껐다. omitempty 없는
Go 구조체나 Python asdict는 매 호출 security:false를 보내므로, 끄려는 의도조차 없이 세션
전체의 키워드 안전망이 꺼졌다.

RISK_RAISING이 이제 모든 위험 상승 키다. 방향은 여전히 한쪽이다 — 명시 true는 텍스트에 없는
위험을 켤 수 있고, LOW 신호는 여전히 자유롭게 끌 수 있다. 자기 저위험 주장을 낮추는 것은
정당하지만, 텍스트가 말하는 위험을 무장해제하는 것은 아니다.

Grok의 반증 시도(격리 worktree, 프로브 3종): 강등 페이로드 20종 → 0건 성공. __proto__·
constructor·배열 signals·타입 혼동(0/"false"/null)은 전부 거부. 명시 true 상승 8종은
전부 유지. Grok이 짚은 것은 반례가 아니라 트레이드오프였다 — "change the password field label,
docs only"는 이제 영원히 HIGH다. 그래서 탈출구를 못으로 박았다: task 없이 signals만 보내면
추론할 텍스트가 없으므로 신호가 그대로 쓰인다.
도구 설명에도 적었다.

곁들여, 이 도구는 additionalProperties: false광고하면서 zod가 미지 키를 조용히
버렸다 — meteredBilling(camelCase, 실제 이름은 metered_billing)이 수용·무시되고 종량제
엄격 기준은 적용되지 않았다. .strict()로 광고와 런타임을 일치시켰다.

A2. 프롬프트를 실은 grok_cli가 이력에도 hook에도 안 잡혔다

grok_cli {"args":["-p","Create a file named a2.txt …","--always-approve"]}
  → status ok, a2.txt 생성, 구독 턴 소모
  → ~/.grok-build/history.jsonl: 1790행 → 1790행

/grok:usage·/grok:status는 그 파일만 읽는다. 즉 passthrough 편집은 사용량 보고의 사각지대
였다. PreToolUse matcher도 delegate/plan/verify뿐이라 인증 게이트도 없었다.

게이트는 툴 이름이 아니라 프롬프트를 따라간다. matcher에 grok_cli를 그냥 넣으면
grok --version·grok sessions list가 "로그인 안 됨"으로 막힌다 — 로그인이 안 된 이유를
알아보려고 치는 바로 그 명령들
이다. 그래서 hook이 stdin 페이로드의 tool_input.args를 읽어
-p/--single/--prompt-file/--prompt-json일 때만 인증을 요구한다(grok --help 1.0.13로
이 넷이 단일턴 플래그의 전부임을 확인). grok 미설치 deny는 여전히 전부에 적용된다. 페이로드를
읽을 수 없으면 닫히는 쪽으로 실패한다 — runGrokCli에는 서버측 checkAuth가 없어 이 hook이
passthrough의 유일한 인증 게이트다.

기록이 이려면 파일도 알아야 하므로, 프롬프트 런은 runDelegate와 같은 porcelain 델타를
잰다(읽기 전용 서브커맨드는 git을 아예 부르지 않는다). 이력 행에는 via: "grok_cli"가 붙는다 —
없으면 delegate라는 뜻이라 기존 이력은 마이그레이션이 필요 없다.

재측정: 1790 → 1791, filesChanged: ["a2.txt"], via: "grok_cli", grok_build_usage가 그 행을
보고. grok_cli --version은 행을 늘리지 않음.

Grok이 이 게이트에서도 구멍을 찾았다 — 짧은 플래그 뭉침

첫 판본은 토큰 전체로만 매치했다. 그래서 ["-vp","x"]에서 프롬프트를 못 찾았는데,
grok의 clap은 그걸 -v -p x로 읽는다. 실측(1.0.13):

grok -vp   → exit 2, "a value is required for '--single <PROMPT>' but none was supplied"
grok -sp   → "--session-id must be a valid UUID (got 'p')"     (즉 -s p)
grok -wp   → TUI가 떠서 행함 (-w p, worktree 이름 "p")

즉 뭉침은 실재하고, 게이트가 그걸 통과시키면 턴이 게이트 없이 소모된다.

수정하면서 두 질문을 분리했다 — 같은 함수가 답하기엔 서로 다른 질문이라서다:

  • extractPromptRun(기록용)은 확신할 때만 프롬프트를 말한다. -vp x는 확신한다
    (v는 값을 안 받으므로 p가 다음 토큰의 주인). -mp x는 아니다 — -m이 값을 받으므로
    그건 --model p이고 프롬프트가 아예 없다. 이력에 허구를 적느니 아무 말도 안 하는 게 낫다.
  • mayRunTurn(인증 게이트용)은 더 넓다. 해석되지 않는 뭉침에 p가 있으면 게이트한다.
    과잉 게이트는 로그인한 사용자에게 아무 비용이 없고, 과소 게이트는 확인 없이 쿼터를 쓴다.

곁들여, 이 파서를 prompt-flags.ts(임포트 없는 리프 모듈)로 옮겼다. hook이 이걸 얻으려고
grok-cli.tsdelegate.tsworktree.ts를 통째로 인라인해 dist/hook.js
6,612 → 9,660바이트가 됐었다. 지금은 8,587바이트이고 두 파일 다 들어 있지 않다.

A3. resume이 caller의 cwd를 조용히 무시했다 (delegate.ts)

delegate {prompt:"Create b.txt …", cwd:<dirB>}            → completed, b.txt in dirB, sessionId S
delegate {prompt:"Create a.txt …", cwd:<dirA>, resume:S}  → completed, filesChanged: []
                                                          → a.txt는 dirB에 생성됨

grok의 --resume--cwd를 덮어쓴다 — 세션은 자기가 태어난 디렉터리에 묶인다(계약 §12).
응답에도 이력에도 신호가 없었고, 이력의 cwd는 dirA로 잘못 남았다.

grok은 세션 위치를 파일시스템에 드러낸다(<grokHome>/sessions/<url-encoded cwd>/<id>). resume
주어지면 spawn 전에 소유 디렉터리를 찾아, 다르면 그쪽 델타도 함께 잰다. 결과에 resumedCwd
경고가 붙고 filesChanged는 두 디렉터리의 합집합이다. --continue는 사전에 세션을 특정할 수 없어
경고와 resumedCwd만 붙고 파일 주장은 하지 않는다.

이 조회는 이 레포가 소유하지 않은 레이아웃을 읽고, grok은 스스로 업데이트된다. 그래서 못 찾으면
아무 주장도 하지 않고 이전과 동일하게 지나간다 — 틀린 주장보다 무주장이 낫다.

재측정: filesChanged: ["a2.txt"], resumedCwd가 dirB를 지목, 메시지가 덮어쓰기를 설명.

A4. worktree remove가 미적용 산출물을 복구 불가하게 지웠다

--force가 무조건이었다. 이 플러그인은 커밋하지 않으므로 worktree의 grok 산출물은 정의상
미커밋이다 — apply 전에 remove하면 blob·reflog·브랜치 어디에도 남지 않고, 응답은 ok:true뿐이었다.
같은 데이터에 대해 prune은 "깨끗하다고 증명되지 않으면 안 지운다"였다. 두 명령, 반대 기본값.

이제 먼저 상태를 재고, 모르는 것은 dirty로 친다(prune이 이미 문서화한 이유 그대로 — 답 없는
상태를 "안전"으로 읽는 것이 작업물이 사라지는 경로다). 거부 메시지는 위태로운 파일을 이름으로
말한다. force: true는 호출자가 소리 내어 말하는 것이다.

실측: 감사가 남긴 SCAManager 소유 dirty 트리에 remove를 걸면 ok:false와 함께
src/analyzer/io/tools/throwaway_axis_a.py를 지목하며 거부한다. 트리는 그대로 남는다.

A5. prune이 진짜 고아를 절대 수집하지 못했다

소유 repo가 삭제되면 git -C <tree> status가 실패 → dirtyundefineddirty !== false
가드가 skippedDirty로 보낸다. prune을 돌리는 유일한 이유가 매번 후보로 뜨고 매번 건너뛰어졌다.
반대로 실제로 rmSync되던 유일한 경로는 baseDir 아래 우연히 놓인 독립 repo의 커밋된 이력이었다.

고아를 이제 분류로 찾는다: git이 답하지 않고, .git 파일이 없거나 그 파일이 가리키는 repo가
사라졌으면 고아다. 소유 repo가 살아 있는데 git이 답하지 않는 트리는 여전히 "판단 불가"로 보호된다
(2026-09-02 사고의 방어를 조용히 되돌리지 않기 위해). 그리고 removedOrphan이 래퍼가 지은
이름
(grok-<base36>-<base36>)에만 적용된다 — 감사가 글로벌 base dir에서 오너의 체크아웃을 발견한
적이 있고, git이 모른다는 것이 우리 것이라는 뜻은 아니다. 실패한 remove를 rmSync로 흘려보내던
경로는 제거했다.

실측: 진짜 고아를 하나 만들어(worktree 생성 → 소유 repo 삭제) dry run을 돌리면
orphan: true·dirty: undefined로 분류되고 "apply하면 삭제됩니다"라고 예고한다. 이전에는
skippedDirty였다. 이 머신에 남아 있던 9~10개는 전부 소유 repo가 살아 있어 고아가 아니다
안 지우는 게 맞다.

Grok이 이 수정의 첫 판본에서 구멍을 찾았다

A5의 첫 판본은 파괴 경로를 좁혔을 뿐 닫지 못했다. Grok이 pruneGrokWorktrees를 읽고 짚은 것:

  • hasGitFilereadFileSync('<tree>/.git')에서 왔는데, 그건 .git디렉터리
    EISDIR로 던진다 — 즉 평범한 체크아웃이 "git이 모르는 디렉터리"와 구별되지 않았다.
  • /^grok-[0-9a-z]+-[0-9a-z]+$/grok-build-plugin·grok-my-project에도 매치한다 —
    사람이 충분히 지을 법한 이름이다.

둘이 겹치면: PATH에 git이 없을 때 모든 status 프로브가 실패하므로, base dir에 있던 진짜
저장소들이 한꺼번에 고아로 분류돼 삭제된다. A5가 끝내려던 바로 그 파괴가 좁은 모양으로
되살아난 것이다. 둘 다 실측으로 확인했다(readFileSync.git 디렉터리 → EISDIR;
정규식 → 네 이름 전부 매치).

수정: .git 엔트리의 종류를 직접 본다(file/dir/none) — .git 디렉터리는
저장소다, 끝
. 그리고 이름 패턴을 worktreeName()이 실제로 만드는 모양으로 좁혔다
(/^grok-[0-9a-z]{7,10}-[0-9a-z]{1,8}$/ — 가운데는 base36 밀리초 타임스탬프라 이 소프트웨어가
돌 수 있는 모든 날짜에서 7~10자다). grok-build-plugin·grok-my-project는 이제 거부된다.

재측정: 진짜 고아는 여전히 orphan: true로 수집 예고되고, base dir에 놓인 커밋 이력이 있는
진짜 저장소
(wrapper 모양 이름)는 orphan: false로 보호된다.

A6. promptPreview 마스킹 구멍 (history.ts)

프롬프트에 붙여넣은 시크릿은 ~/.grok-build/history.jsonl에 200자 미리보기로 남고,
grok_build_usage.recent[]·grok_build_status.lastSession이 그걸 대시보드 호출마다 Claude
컨텍스트로 되돌려준다. 출하 중인 정규식에 8가지 실제 형태를 넣어 봤더니 8/8 전부 원문으로
통과
했다:

DATABASE_URL=postgres://app:PW@db.internal:5432/prod     LEAKED
DB_URL=mysql://admin:PW@10.0.0.5/app                      LEAKED
CONNECTION_STRING: mongodb+srv://root:PW@cluster0…        LEAKED
https://user:ghp_…@github.com/org/repo.git                LEAKED
Authorization: Basic YWRtaW46…                            LEAKED
sk_live_51Qx…                                             LEAKED
AIzaSyD-…                                                 LEAKED
-----BEGIN RSA PRIVATE KEY----- (종결 마커 없음)          LEAKED

마지막 것이 특히 나쁘다 — 미리보기는 200자에서 잘리므로 종결 마커가 없는 쪽이 오히려
평범한 경우인데, 정규식이 -----END-----를 요구해서 흔한 경우가 새는 쪽이었다.

수정: URL 자격증명(비밀번호만 치환, 호스트·경로는 남긴다 — 행 전체를 지우는 마스킹은 결국
꺼진다), DATABASE_URL류 키 8개를 NAMED_KEYS에, BearerBasic 추가, 종결 마커 없는 PEM은
그 뒤 전부, Stripe·Google 프리픽스 2종.

Grok이 여기서도 하나 더 찾았다: URL 정규식의 사용자 부분이 +여서 사용자명이 없는
redis://:password@cache:6379가 통과했다 — 그게 Redis URL의 표준 형태다. *로 고쳤다.
(같은 패스에서 오탐 후보 3종 — postgres://app@db, 3://412:30@lunch, DATABASE_URL: string
— 은 전부 통과 확인.)

실사용 트래픽으로 검증했다 — 감사가 "한 번도 안 했다"고 지적한 그것

새 규칙을 실제 이력 3,114개 필드에 돌렸다. 결과: 오탐 0건. 즉 이 규칙들은 3천 건이 넘는
진짜 프롬프트·요약을 하나도 망가뜨리지 않는다 — 마스킹이 꺼지게 만드는 실패 모드가 바로 그것이라
이게 확보할 수 있는 가장 강한 증거다.

⚠️ 동시에, 진짜 시크릿을 잡는 능력은 여전히 합성 테스트로만 증명돼 있다. 3,114개 중 새로
걸린 것이 0건이라는 말은 이 사용자의 이력에 그런 형태가 없다는 뜻이기도 하다. 유일한
<redacted> 1건은 감사가 직접 주입한 시험 행(f:/tmp/grok-service-audit/…)이고 —
docs/10 D가 3행을 지웠다고 적었는데 1행이 남아 있다 — 즉 실사용에서 이 규칙이 발동한 적은
아직 없다.
그건 실제 유출이 일어나야만 닫히는 종류의 검증이라 열어 둔다.


남은 것

docs/10에 14건(A7~A20)이 열려 있다. 다음은 A7 — PreToolUse hook의 subscription deny 분기가
출하 상태에서 무장되지 않는다.

유닛 테스트 460 (+40), tsc --noEmit clean, 두 번들 재빌드.

v0.2.19 — 서비스 감사가 찾은 네 가지 실패

Choose a tag to compare

@xzawed xzawed released this 05 Sep 12:57
4960ae7

v0.2.19 — 서비스 감사가 찾은 네 가지 실패

2026-09-05. 배포 번들(0.2.18)을 stdio로 직접 구동해 53개 기능 항목을 실행하고 각 항목을
독립 재실행자가 재측정했다. PASS 29 · DEGRADED 20 · FAIL 4. 이 릴리스는 그 FAIL 4건이다.

핵심 안전 계약(구독 과금 보장, 자동 커밋 금지, worktree baseDir 가드, spawn 이전 검증)은
메커니즘 수준에서 전부 통과했다 — 실패는 그 바깥에 있었다.


1. /grok:plan이 read-only가 아니었다

빈 디렉터리에 plan을 돌리면 파일이 생성됐고, 응답은 filesChanged: []였다. 쓰고서 안
썼다고 보고한 것이다.

귀속을 갈랐다. 플러그인은 --permission-mode plan을 정확히 넘긴다(delegate.ts).
플러그인 없이 grok을 직접 돌려도 파일이 생겼다 — grok CLI 1.0.13이 그 플래그를
무시한다.
계약 §6이 1.0.3에서 기록한 동작의 회귀다. 이어서 측정한 결과:

시도 결과
--permission-mode plan 파일 생성
--sandbox read-only 파일 생성
--sandbox strict 파일 생성
--always-approve 생략 파일 생성 (Grok 확인)

즉 1.0.13에는 쓰기를 막는 플래그가 없다. 봉쇄 수단은 격리뿐이다.

플러그인은 막을 수 없으므로 숨기지 않기로 했다(오너 결정). plan 런도 delegate와 같은
before/after porcelain 차집합으로 filesChanged를 채운다. 그리고 그것만으로는 부족하다 —
Grok이 설계 리뷰에서 반박했듯, 경로 차집합은 이미 더티했던 파일의 추가 편집을 놓친다
(before === after). plan-before-delegate가 정확히 그 케이스다. 그래서 git diff HEAD 해시를
함께 비교한다.

응답에 planWroteFiles가 붙는다 — true(변경됨, 경고 message 동반) / false(변경 없음 확인) /
생략(cwd가 git 저장소가 아니라 확인 불가). 마지막 경우를 false로 적지 않는 것이 요점이다.
tool 설명에서 "WITHOUT editing any files" 약속도 내렸다.

2. 라우터의 보안 판정이 영어에만 있었다

9개 규칙 중 security 한국어 대체어가 없었다. UI 전체가 한국어인 제품에서, 하필
최고 위험 신호가.

입력
운영 서버의 인증 토큰 발급 로직을 바꿔라 MEDIUM HIGH / claude
(같은 뜻 영어) HIGH HIGH
운영 데이터베이스의 비밀번호 해시를 argon2로 마이그레이션 LOW / delegate HIGH / claude

3. 맨 bulk 동사가 파괴적 운영 작업을 통과시켰다

migrate the production customer database … and drop the old columnsLOW /
grok_build_delegate
, 인간 게이트 없음. migratebulk를 켜고, bulk 신호는 약한 LOW
강등을 건너뛴다. productiondrop도 신호 집합에 아예 없었다.

destructive·production 두 신호를 추가했다:

  • 둘 다 → HIGH. worktree 격리는 파일 편집만 되돌린다 — 삭제된 컬럼은 돌아오지 않는다.
  • 하나만 → MEDIUM 바닥. bulk 신호가 있어도 내려가지 않는다.
  • 명시 signalsfalse를 보내도 본문이 그렇게 말하면 꺼지지 않는다 — 모든 필드를 채우는
    구조체 직렬화(omitempty 없는 Go, Python asdict)가 세션 내내 안전망을 끄는 것을 막는다.

Grok이 첫 초안을 반박했고, 양방향 모두 실측으로 재현됐다.

  • 너무 넓었다. 드롭드롭다운 안에서, 초기화가 "폼 상태 초기화" 안에서, 맨
    purge가 "purge unused CSS" 안에서 발동했다. 한국어는 단어 경계가 없어 짧은 어간은 구조적으로
    부분일치 함정이다 — 짧은 어간을 버리고 목적어를 요구하는 형태로 바꿨다
    (테이블·디비·DB·데이터베이스·버킷·인덱스 뒤에 오는 삭제/드롭만 잡고,
    초기화는 2차 라운드에서 데이터베이스 목적어로 더 좁혔다).
  • 너무 좁았다. 실제 인프라 파괴가 취하는 형태 — terraform destroy, kubectl delete namespace, aws s3 rb, dropdb — 가 하나도 안 잡혔다. migrate 40 files then terraform destroy against the prod workspaceLOW/delegate였다. 그리고 운영 리소스는 산문이 아니라
    식별자로 등장한다(prod-customer-backups, customer_live) — 그것도 추가했다.

그리고 2차로 다시 반박했다 — "출하 부적합" 판정. 이번엔 더 현실적인 반례였다:
목차 테이블 드롭다운(테이블 + 드롭다운), zod 스키마 초기화, 비프로덕션(= NON-production인데
프로덕션을 포함), go-live checklist, 그리고 delete all unused imports — 마지막은 흔한
정리 작업이다. 미탐 쪽도 운영 DB(디비가 아님)·테이블은(조사 은/는)·pulumi destroy·
rails db:drop·rm -fr가 남아 있었다.

고친 방식은 키워드 추가가 아니라 문법을 요구하는 것이다:
초기화는 데이터베이스에만, 삭제 동사는 데이터 목적어를 요구(코드 삭제는 제외),
드롭(?!다운) 부정 전방탐색, (?<!비)프로덕션·(?<!non-)production 부정 후방탐색,
하이픈 식별자(go-live)는 제외하고 prod- 접두·_live 접미만, 조사 은/는/도와 DB 약어 추가.

최종 실측: Grok의 2라운드 반례 13건이 전부 올바른 등급이 됐고 기존 케이스는 불변이다.

4. 같은 디렉터리가 대시보드에서 여러 프로젝트로 갈렸다

cwd 필터가 정확한 문자열 일치였다. 실제 1779행 이력에서 386행(21.7%)이 가려졌다.

f:\DEVELOPMENT\SOURCE\CLAUDE\SCAManager   377건
F:\DEVELOPMENT\SOURCE\CLAUDE\SCAManager    79건
f:/DEVELOPMENT/SOURCE/CLAUDE/SCAManager    37건
                                    → 493건 (수정 후, 네 표기 모두 동일)

성공률도 72.7% / 36.7% / 40.5%로 갈렸다. 구분자·후행 슬래시는 어디서도 의미가 없고,
대소문자는 win32에서만 접는다 — POSIX에서 접으면 /srv/A/srv/a가 합쳐진다.


바뀐 것

파일 변경
src/routing.ts 한국어 보안 키워드, destructive·production 신호, HIGH/MEDIUM 게이트, 위험 신호 OR 병합
src/delegate.ts defaultGitDirtyFingerprint, plan 런의 before/after 스냅샷, planWroteFiles
src/usage.ts normalizeCwd + 두 필터 지점
src/types.ts · src/server.ts planWroteFiles 필드, plan tool 설명 정정
docs/specs/grok-cli-contract.md §6 — plan 모드 쓰기 방지 실패 (§6 행 2026-09-05)
docs/04 · docs/05 · commands/plan.md plan 계약·위험 조건 갱신

검증

  • npm test 407 passed / 1 skipped (v0.2.18 시점 382 — 회귀 25건 추가)
  • npm run typecheck, npm run build — 번들 2개 재생성
  • 라이브 재현: 수정 전 filesChanged: [] 침묵 → 수정 후 planWroteFiles: true + 경고
  • cwd 4가지 표기 → 전부 493건 / 64.5%로 일치
  • 라우팅 스윕 2라운드 — Grok 반례 포함 전 케이스에서 위험 LOW 0건, 정상 오탐 0건

v0.2.18 — 세션이 만료되면 grok은 기다리지 않는다

Choose a tag to compare

@xzawed xzawed released this 05 Sep 00:52
80d2aed

v0.2.18 — 세션이 만료되면 grok은 기다리지 않는다, 그리고 우리는 정반대를 안내했다

2026-09-05. 오래 이월되던 마지막 두 항목은 오너 입력 대기로 적혀 있었다. 둘 다 물어보지
않고 실측으로 닫았고, 그 과정에서 사용자에게 보이는 실제 결함 하나가 나왔다.


1. GUI 슬래시 커맨드 칸 — serverVersion: 0.2.17 (닫힘)

재시작된 세션에서 플러그인이 띄운 MCP 서버의 grok_build_status(= /grok:status의 구동부)가
ready: true · billing: subscription · serverVersion: 0.2.17 을 반환했다. 마켓플레이스
설치본이 실제로 로드돼 돌고 있다는 뜻이고, 갱신 시점에 물고 있던 0.2.11 프로세스는 재시작으로
사라졌다. docs/09 §5에 열린 칸은 이제 없다.

2. 만료 세션 — 답은 "폐기"다 (닫힘, 사람 캡처 불필요)

문서는 "만료면 grok이 device-OAuth를 stderr로 내고 블록하므로 래퍼 timeout이 된다"고
적고 있었다 (2026-07-13 관측). 오너가 만료되는 순간을 붙잡아 캡처해 줘야 닫히는 항목이었다.

그럴 필요가 없었다. 만료는 세션의 부재가 아니라 auth.json이 있는데 거부되는 경우이고,
그건 격리 GROK_HOME합성 auth.json을 써서 재현할 수 있다 (실 ~/.grok은 읽지도 쓰지도
않는다). npm run probe:expired가 그것이고, 3회 연속 같은 결과를 냈다 — 1.0.13, win32:

변형 auth.json 결과
C1 expires_at 과거 + 갱신 실패 exit 1 · Not signed in.
C2 expires_at 미래 + 서버가 거부 exit 1 · Unauthorized (401) … Invalid or expired credentials
B (대조군) 파일 없음 exit 1 · Not signed in.

어떤 변형도 기다리지 않는다. device-OAuth 마커는 어디에도 없고, 첫 출력까지 10~20초, 전부
exit 1이다. 옛 A 경로는 1.0.13에서 재현되지 않았다(신호는 보험으로 남겼다).

3. 그래서 발견된 결함 — 만료 순간 우리는 "로그인할 필요 없다"고 안내했다

C2 봉투에는 AUTH_ERROR_SIGNALSDEVICE_AUTH_SIGNALS하나도 매칭되지 않는다.
not signed ingrok login도 없기 때문이다. 대신 xAI의 상용구가 이렇게 끝난다:

Your session is still signed in and will recover automatically — retry in a few seconds;
no need to run /login.

그래서 runDelegate는 이 실행을 auth_error가 아니라 grok_error 로 분류했고, 그 문장이
그대로 사용자 안내가 됐다. 즉 구독 세션이 죽는 바로 그 순간에, 플러그인은 사용자에게
"재로그인할 필요 없다"고 말하고 있었다. 실측으로 고정한 관측값:

r.status  = "grok_error"
AUTH_ERROR_SIGNALS hits   = []
DEVICE_AUTH_SIGNALS hits  = []
message contains "no need to run /login" = true

이 관측은 Grok이 독립적으로 작성한 테스트로 재현했다 — 같은 값이 나왔다.

고친 것. AUTH_ERROR_SIGNALS/invalid or expired credentials/i 한 줄. 상태코드가 아니라
자격증명 문구를 매칭한다 — "광범위한 401/403은 제외한다"는 기존 결정은 그대로다. 회귀 테스트
3건이 붙었다: 401 봉투 → auth_error + grok login 안내 + 그 상용구가 안내로 새지 않을 것,
자격증명 문구 없는 맨 401은 여전히 grok_error.

4. 액세스 토큰 수명은 7일이 아니라 6시간이다

docs/02는 "공개 문서상 약 7일로 추정 — 미검증"이라고 적고 있었다. 값이 아니라 시간 차이만
계산해 실측했다: expires_at − create_time = JWT exp − iat = 6.00시간, 그리고 expires_at
그 JWT의 exp와 같다. 사용자가 6시간마다 로그인하지 않는 이유는 별도의 refresh_token이 조용히
갱신하기 때문이다. 리프레시 토큰 수명은 여전히 미측정이고, grok login을 부르는 것은 그쪽이다.

5. 프로브가 조용히 무의미해질 뻔한 지점

처음 커밋한 probe:expired는 세 변형이 전부 B("Not signed in")로 무너졌다 — 즉
probe:unauth의 사본이었다. 원인은 auth.json 항목 키를 익명화한 것이다. 그 키는
<oidc_issuer>::<oidc_client_id>이고, UUID는 항목의 oidc_client_id와 같다 — 사용자 id가
아니라 grok CLI의 OAuth 클라이언트 id
(모든 설치본에서 동일, 공개값)다. 다른 UUID를 쓰면 CLI가
자기 클라이언트의 항목을 찾지 못한다. 실제 파일의 필드와 대조해서야 잡혔고, 스크립트에 주석으로
박아뒀다. 프로브는 "돌았다"가 아니라 대조군과 달라야 의미가 있다.


바뀐 것

영역 변경
src/delegate.ts AUTH_ERROR_SIGNALSinvalid or expired credentials 추가 + 실측 근거 주석
test/delegate.test.ts 회귀 3건 (401 봉투 분류 · 자격증명 문구 · 맨 401 비매칭)
scripts/probe-expired-session.mjs 신규. npm run probe:expired
docs/specs/grok-cli-contract.md §7 경로 C 신설, §7 행 2026-09-05로 갱신
docs/02-auth-strategy.md 만료 체크박스 닫음, 토큰 수명 6시간으로 정정, 신호 목록 갱신
docs/09-scope-and-residuals.md §5 마지막 칸 닫음
CLAUDE.md 컴포넌트 지도의 "만료 = 블록 → timeout" 서술 정정, 핸드오프 갱신

grok v0.2.17 — 게이트를 지키던 것이 아무것도 없던 자리

Choose a tag to compare

@xzawed xzawed released this 04 Sep 08:48
29f2236

v0.2.17 — 게이트를 지키던 것이 아무것도 없던 자리

2026-09-04. 세 번째 스윕이 남긴 새 기능 4건(E1~E4) 을 오너 승인 후 전부 구현했다. 넷 다
"기능이 없다"가 아니라 "검사가 없다" 였고, 착수 전에 각 항목을 다시 실측하고 Grok으로
반증까지 시킨 뒤 done 정의를 먼저 썼다.

동작은 하나도 바뀌지 않았다. 이 릴리스가 범프인 이유는 index.ts가 쪼개져 번들 바이트가
달라졌기 때문이다 — 캐시는 버전 키다.

E1 — MCP 툴 핸들러를 아무 테스트도 실행하지 않았다

실측(착수 전). isError: result.status !== 'completed'를 delegate·plan·verify 세 반환
지점에서 isError: false로 바꿨다. tsc --noEmit 통과, 352개 테스트 전부 녹색.
isError는 MCP 클라이언트가 "이 위임이 실패했는가"를 아는 유일한 신호다. 뒤집힌 계약은
grok_error로 끝난 실행을 Claude에게 성공이라고 말한다.

원인은 핸들러가 main() 안의 익명 클로저라 호출할 방법 자체가 없었다는 것이다. Grok도
같은 결론을 냈다 — 테스트 트리에 ../src/index import도, MCP 클라이언트도, callTool도 없고,
tool-surface.test.ts는 번들을 텍스트로 스캔할 뿐이다.

한 일. 등록과 핸들러를 src/server.ts로 옮기고 index.ts는 stdio 진입점만 남겼다 —
hook-entry.ts/hook.ts가 이미 쓰던 분리다. 핸들러가 손대는 모든 부작용은 ServerDeps
지나며 기본값은 실제 구현이다. 그래서 테스트가 인메모리 전송으로 진짜 등록된 tool을
클라이언트처럼 호출한다 — 등록·스키마·핸들러 본문·반환 봉투가 한 번에 덮인다. grok 프로세스도
홈 디렉터리도 건드리지 않는다.

test/server-tools.test.ts가 고정하는 것:

  • 9개 tool이 그대로 노출되는지 (listTools)
  • delegate/plan/verify: completedisError: false, 그 외 → true
  • auth 사전 검사 실패 시 grok을 부르지 않고 즉시 중단
  • 세 tool 모두 recordDelegation을 타이밍과 함께 호출
  • plan은 plan: true, verify는 check: true를 입력에 실어 보냄
  • grok_auth_check·grok_build_statusauth.ok를, grok_clierror/timeout을,
    worktree 5개 액션은 각각 result.ok를 반영
  • worktree_path 없는 diff/apply/remove는 아무 작업도 실행하지 않고 fail closed

done 검증. 같은 뮤테이션을 다시 넣으니 이번엔 3건 실패한다. 원복 후 전부 녹색.
이 항목만으로 테스트 수 352 → 376.

E2 — 배포되는 skills/agents 프론트매터를 읽지 못하던 검사

plugin-surface.test.ts는 내부용 .claude/ 스킬 두 개에는 name 일치와 description 길이
40자 초과를 요구하면서, 실제로 배포되는 skills/grok-first-mile·skills/grok-routing·
agents/grok-worker에는 existsSync 세 줄만 걸어두고 있었다.

원인이 흥미롭다. 세 파일 모두 description: > 폴드 스칼라를 쓰는데, 기존 헬퍼의
/^([a-zA-Z0-9_-]+):\s*(.*)$/는 그 줄에서 ">" 하나만 잡고 들여쓴 본문은 버린다. 즉 기존
헬퍼로 길이 검사를 걸었어도 ">"의 길이를 재고 통과했을 것이다. Grok이 실제로 값을
측정해 description === ">" 임을 확인했다.

폴드 스칼라(>/|/>-/|-)를 이해하는 파서를 더하고, 세 파일의 name·description
검증한다. 파서 자체가 접힌 값을 펴는지도 픽스처로 고정했다 — ">"를 검사하는 가드는
아무것도 지키지 못하기 때문이다.

done 검증. grok-routing의 description을 짧게 만들면 RED.

E3 — 선언된 버전에 태그·릴리스가 있는지 아무도 보지 않았다

v0.2.12는 매니페스트 두 곳에 선언된 채 태그도 릴리스도 없이 main에 있었다. 마켓플레이스
소스가 ./이고 캐시가 버전 키라, 그 사이 설치자는 그 번호로 옛 번들을 캐시한다. 그래서
번호를 재사용하지 못하고 버렸다.

.github/workflows/ci.ymlpush/pull_request에만 걸려 있고, 태그나 릴리스를 보는 검사는
저장소 어디에도 없었다(Grok 확인).

한 일. mcp-server/scripts/check-release-tag.mjs — 선언 버전의 git 태그와 GitHub 릴리스를
모두 확인하고, 실패 시 캐시 규칙을 인용해 무엇을 하라고 말한다. release-tag-check.yml
이를 실행한다.

⚠️ 일부러 push/PR에 걸지 않았다. 버전을 선언하는 커밋은 태그보다 먼저 머지된다
(CONTRIBUTING: "머지 직후 바로 태그"). PR 시점에 검사하면 모든 릴리스 PR이 오탐으로 빨개진다.
schedule(매일) + workflow_dispatch에서는 "선언된 지 하루가 지났는데 아직 태그가 없다"가
모호하지 않다. 체크아웃은 fetch-depth: 0 — 얕은 클론에는 태그가 없어 잘못된 이유로
통과하거나 실패한다.

done 검증. 스크립트를 로컬에서 세 번 돌렸다: 0.2.16(태그+릴리스) → 통과 /
0.2.12(소급 태그는 있으나 릴리스 없음) → 실패, 정확히 역사적 사고를 잡는다 /
9.9.9 → 둘 다 없다고 실패.

E4 — marketplace.json을 파싱하는 것이 하나도 없었다

모든 설치가 이 파일을 통해 플러그인을 찾는데, 테스트도 CI도 이 파일을 열지 않았다. JSON이
깨지거나 엔트리 이름이 plugin.json과 어긋나도 전부 녹색으로 배포된다.

유효한 JSON인지, namegrok-marketplace인지, 엔트리가 정확히 하나이고 그 이름이
plugin.json의 이름과 같은지, source./인지(버전 키 캐시 규칙이 여기에 의존한다)를
고정했다.

done 검증. 마켓플레이스 이름 변경 → RED. 엔트리 이름 드리프트 → RED.

검증

  • npm ci → SDK 1.29.0 = lockfile 일치
  • npm test 379 passed / 1 skipped (v0.2.16 시점 352 — E1이 +24, E2·E4가 +3)
  • npm run typecheck, npm run build — 번들 2개 재생성
  • 뮤테이션 3종을 실제로 넣었다 뺐다 하며 RED→GREEN 확인 (E1·E2·E4)
  • check-release-tag.mjs 3케이스 실측 (E3)
  • Grok 적대적 교차검증 4건 — E1·E2·E3·E4 주장 전부 CONFIRMED, 파일 무변경

남은 것

이 레포에 열린 코드 항목은 없다. 남은 것은 사람 손이 필요한 것뿐이다 — 설치본 갱신,
GUI 수동 수락 1회, 만료 세션 실측(실계정+시간), SCAManager 토큰의 발급처 revoke 확인.
분류는 docs/09-scope-and-residuals.md.

grok v0.2.16 — 문서가 코드와 반대로 말하던 곳들

Choose a tag to compare

@xzawed xzawed released this 03 Sep 02:22
e661560

v0.2.16 — 문서가 코드와 반대로 말하던 곳들

2026-09-03. 세 번째 스윕은 결함이 아니라 드리프트를 찾았다. 7개 렌즈(로드맵 · 범위문서 ·
릴리스노트 · 계약 스펙 · 코드 · 테스트/CI · 문서와 소비자 키트)로 전수 스윕한 뒤, 후보 33건을
"이미 닫혔다고 가정하고 반증하라"는 검증에 통과시켜 23건을 남겼다. 그중 코드 동작을 바꾸는
것은 하나도 없었다
— 대신 사용자가 따라 하면 안 되는 안내와, 서로 다른 이야기를 하는 문서
세 벌이 나왔다.

이 릴리스가 범프인 이유는 배포되는 실행 표면 하나(commands/cli.md)가 바뀌었기 때문이다.
번들 쪽은 아니다 — esbuild가 주석을 제거하므로 소스 주석 재실측은 바이트를 바꾸지 않았고,
재빌드 후 실제 dist 델타는 두 파일 각각 버전 문자열 1행뿐이었다(git diff --numstat
실측). 캐시가 버전 키이므로, 실행 표면이 바뀐 이상 같은 번호로 재배포하지 않는다.

따라 하면 격리가 안 되던 안내 (README 두 벌)

두 README가 위험한 작업의 격리 방법으로 --worktree / --sandbox 플래그를 안내하고
있었다. 그런데 계약 문서(§6)와 delegate.ts:445-447 주석이 이미 적어둔 대로, grok 자신의
--worktree헤드리스 -p에서 no-op이고 이 래퍼는 그 플래그를 넘기지도 않는다
(실제로 붙는 것은 --sandbox 하나뿐이다). 격리는 tool 필드 worktree: true / sandbox: "…"로만
일어난다. 사용자가 실제로 읽는 표면이 또 한 번 뒤처져 있었다 — v0.2.13의 auth 우선순위 사고와
같은 유형이다.

코드와 반대로 적혀 있던 스니펫

  • docs/01 · docs/04의 spawn 예시가 아직 -p prompt였다. 코드는 v0.2.13에서
    --single=${prompt}로 바뀌었다 — clap이 bare 옵션 값으로 -로 시작하는 문자열을 거부해
    "- Refactor …" 같은 프롬프트가 exit 2로 죽었기 때문이다.
  • docs/04의 이력 마스킹 서술이 v0.2.13 시절(“xAI 키 대입문만”)에 머물러 있었다. 실제
    redactSecrets는 PEM 블록 → Bearer → password: 류 대입 → xAI·AWS·GitHub·Slack·JWT
    형태 순으로 덮는다. 완화이지 보장이 아니라는 단서도 함께 넣었다.
  • docs/04GrokCliResult에 v0.2.14가 추가한 stdoutTruncated/stdoutTotalChars가 없었다.
  • docs/03plugin.json 예시 버전이 0.2.11이었다. 이 스니펫은 버전 일치 테스트 밖이라
    구조적으로 낡는다는 사실을 문서에 명시했다.
  • docs/06[~] check-auth 체크박스는 삭제된 커맨드를 가리키고 있었다. 그 커맨드는
    /grok:setup에 흡수됐고 done 정의의 네 실패 모드는 전부 코드에 있다.

같은 사실을 세 방향으로 말하던 문서

세션 만료 신호가 "왜 미검증인가"에 대해 docs/06개발 머신 keyring 폴백 탓,
docs/02정밀 앵커는 추후, docs/06의 다른 줄은 앵커 완료라고 적고 있었다.
2026-09-02 실측(계약 §8)은 그중 어느 것도 아니라고 말한다 — 격리 홈(GROK_HOME) 아래에는
폴백이 없다. 이번에 1.0.13에서 재확인했다: 유효한 ~/.grok/auth.json이 그대로 있는
상태에서 GROK_HOME=<tmp> grok models는 "You are not authenticated."를 낸다.

그래서 진짜 이유로 고쳤다: 재현되지 않는 것은 만료(≠부재)이고, 그것은 실계정과 시간
경과가 필요하다. 2026-07-25-auth-unauth-signals.md에도 그 노트의 HOME/USERPROFILE 방식이
격리가 아니라는 포인터를 달았다.

새로 실측한 것 — resume × sandbox (계약 §11)

tool은 resumesandbox를 각각 옵셔널로 받아 같은 argv에 싣는다. 그 조합을 재본 적이
없어서 재봤다(1.0.13):

grok --resume <id> --sandbox read-only     → exit 1, stdout 0바이트, stderr:
  "cannot resume this session under sandbox profile 'read-only' — it was created with 'workspace'."
grok --resume <id> --sandbox workspace     → end_turn   (같은 프로필은 허용)
grok --resume <id>                         → end_turn   (저장된 프로필로 재개)

세션의 sandbox 프로필은 수명 동안 고정이다. 래퍼에서는 빈 stdout이 파싱 실패로 이어져
grok_error가 되지만, grok의 안내 문구가 rawStderrTail에 그대로 실린다(191자, 500자 컷
안). 호출자가 무엇을 고쳐야 하는지 받는다 — 그래서 별도 가드를 넣지 않았다. 재현 절차와
근거는 계약 §11과 sandbox 스펙에 남겼다.

실행 표면 — /grok:cli의 잘림 분기

commands/inspect.md는 v0.2.14부터 stdoutTruncated를 다루지만, 같은 크기의 출력을
통과시키는 /grok:cli 패스스루
는 여전히 stdoutTail을 전체로 제시하라고 지시하고 있었다.
컷은 4,000자이고 inspect --json은 ~81 KB로 실측됐다. 잘렸을 때는 꼬리임을 밝히고
stdoutTotalChars를 인용하며 통째로 파싱하지 말라는 분기를 넣었다. 길이 실측이 없는 나머지
커맨드는 근거가 없어 건드리지 않았다.

소스 주석 — 1.0.5 스탬프를 1.0.13에서 재측정

세 곳이 "measured on 1.0.5"라고 적혀 있었지만 계약 문서의 해당 절은 1.0.13이었다. 세션 도중
CLI가 스스로 갱신된 흔적이다. 역사적 귀속을 추측하는 대신 오늘 다시 쟀다 — 세 주장 모두
1.0.13에서 그대로였다.

  • env.tsGROK_HOME 전면 이전 + 폴백 없음: 재확인.
  • delegate.ts — 확인 프롬프트와 stdin: 격리 홈에서 memory clear가 열린 stdin 아래서는
    10초 상한까지 매달렸고, stdin이 EOF면 프롬프트를 찍은 뒤 "Cancelled."로 1초 만에 exit 0,
    파일은 그대로였다. 헤드리스에서 stdin을 닫아두는 이유가 그대로 성립한다.
  • grok-cli.ts — 값-플래그 목록: 사라진 항목 없음. 더해서 -r/--resume·-w/--worktree
    옵셔널 값을 받는다는 것을 명시했다. 목록에 넣으면 자기 것이 아닌 토큰을 삼킬 수 있어
    일부러 빼둔 것이고, 빠져 있으면 파싱이 "불확실"로 판정돼 fail CLOSED 된다.

복구 — v0.2.13이 가리키던 "세션 보고서"

docs/releases/v0.2.13.md는 감사 방법과 전체 목록의 원천으로 "세션 보고서"를 가리켰는데,
그런 파일은 레포에 없었다. 실제 원천은 그 세션이 발행한 감사 리포트 아티팩트였고,
CHANGELOG.md가 "반증 4건도 기록에 남겼다"고 적은 그 4건도 레포 어디에도 열거돼 있지
않았다. 아티팩트에서 되살려 릴리스 노트에 표로 옮겼다 — filesChanged: [] 계약, 중복 probe,
hook 모드 불일치, security: false 우선순위. 이제 레포만 읽고도 자립한다.

닫힌 채로 열려 있던 항목

CLAUDE.md가 "사람이 해야 할 미해결 2건"으로 이월하던 것 중 Dependabot 경보는 이미
닫혀 있었다
state=open 0건, 전체 21건이 fixed. 남은 SCAManager 토큰 항목은 서술
자체가 CHANGELOG.md(PR #53)와 충돌했다: 그쪽은 발급처 revoke로 모든 사본이 무력해졌다
기록한다. 사실 관계를 CHANGELOG에 맞추고, 귀속 오류(v0.2.13이 아니라 PR #53, 그리고 v0.2.13이
손댄 것은 토큰이 없는 install-hook.sh)도 정정했다. 남은 것은 사람이 발급처에서 한 번
확인
하는 일뿐이다.

검증

  • npm test · npm run typecheck · npm run build — 번들 2개 재생성
  • 실측: grok 1.0.13 (5e9a58528b76), Windows 11. --help 전수, GROK_HOME 격리 probe,
    memory clear stdin 두 경로, resume × sandbox 4조합
  • Grok 교차검증: 아래 "반증된 것도 남긴다" 참고

반증된 것도 남긴다

스윕이 올린 후보 중 7건은 검증에서 죽었다. 되살리려면 이 근거부터 반박해야 한다.

죽은 후보 왜 아닌가
오케스트레이터 실배선이 이 레포의 열린 잔여다 계약·grok_build_route·픽스처·소비자 키트가 전부 존재한다. 남은 것은 소비자 레포의 일이며 docs/09 §4.A가 이미 그렇게 분류했다 — 이 레포 PR로 닫히지 않는다.
docs/09가 "버전을 박지 말라"면서 v0.2.x를 박아둔 자기모순 제안된 수정이 handoff-version.test.ts를 깨뜨린다. 15행은 게이트가 강제하는 자리, 19행은 가드가 없는 자리로 역할이 다르다.
이력 마스킹 패턴을 Google·Stripe·Azure 등으로 확대 v0.2.14가 근거와 함께 "하지 않는다"를 결정했다(과잉교정 전례: 프롬프트 45개 중 19개 훼손).
CHANGELOG.md를 버전 드리프트 가드에 포함 현재 드리프트 0, 그 가드가 막았을 실패 사례 0. repo-scope의 red flag에 정면으로 해당한다.
계약 §10의 "유효한 실제 API 키 미측정"이 미해결 항목 문서가 안전상 미측정을 의도적 선택으로 기록했고, 그 결과와 무관하게 절대 원칙 #1이 성립한다고 명시했다. 추적 체크박스도 없다.
resume × sandbox 불일치에 코드 가드가 필요하다 실측 결과 grok의 안내 문구가 rawStderrTail로 그대로 전달된다. 문서화로 충분하다.
2차 감사에서 반증된 6건(CI 부동 태그·히스토리 읽기 비용 등) v0.2.14가 측정치와 함께 이미 반증했다(13.6 ms 대 362.8 ms 등).

grok v0.2.15 — six advisories, four of which were shipping

Choose a tag to compare

@xzawed xzawed released this 02 Sep 22:39
c9b9f5f

A dependency patch. No plugin logic changed — but the interesting part was not the count of advisories, it was working out which of them actually reached anyone.

This repository ships its bundles, and esbuild inlines runtime dependencies into them. So an advisory against a transitive package is either live code in what users execute, or it is nowhere near them, and the only way to know is to look:

grep -c "node_modules/fast-uri/" dist/index.js  ->  6   inlined, shipping
grep -c "node_modules/qs/"       dist/index.js  ->  0   outside the bundle
grep -c "node_modules/express/"  dist/index.js  ->  0   outside the bundle
package advisories severity patched reached users
fast-uri 4 — two host-confusion, two SSRF high 3.1.5 → 3.1.7 yes
qs 2 — array-limit bypass, DoS moderate 6.15.3 → 6.16.0 no

fast-uri arrives through ajv from the MCP SDK, and it was inlined — those four highs were sitting in the code installed users run. qs and express belong to the SDK's HTTP transports, which this stdio server never loads; patched anyway, since there is no reason to leave an alert open.

Verified in the artifact, not the lockfile

A lockfile says what should have been built. The bundle is what ships. URI host is malformed. — a string that exists only in fast-uri 3.1.7 — is present at dist/index.js:4128 and absent from the previous bundle. Of 54 changed hunks, 53 land inside fast-uri and one is the version string; no plugin code moved.

Verified at this commit

npm ci, npm audit0 vulnerabilities, 352 tests passed / 1 skipped, typecheck clean, and both committed bundles rebuild byte-identical from the committed lockfile. The MCP SDK stayed at 1.29.0 — checked deliberately, because a past npm i --no-save in this repo silently raised a runtime dependency and nearly shipped a bundle that did not match its source.

Full detail: docs/releases/v0.2.15.md

grok v0.2.14 — the areas nobody had opened

Choose a tag to compare

@xzawed xzawed released this 02 Sep 16:35
624f1b9

The first audit left a list of areas it had never opened. This is that list, audited the same way: six areas, 18 findings, two independent checks each, Grok on the ones that mattered.

Ten reached cross-verification and two survived both. That thinness is the honest result — it is what a repository looks like after its serious defects have just been fixed.

The documents promised more than the code did

The plugin's own absolute principle says the server does not log credentials; the logging design spec says "No credentials, ever." In practice only the two xAI billing keys were masked. Measured: a Bearer JWT, an AWS secret, a GitHub personal access token and a bare password: line pasted into a delegation prompt were all written verbatim into ~/.grok-build/history.jsonl — and replayed back to Claude by /grok:usage and /grok:status.

Redaction now covers the common shapes. But the more important repair is to the sentences: the principle scopes itself to the credentials the server holds, states that your prompt preview is outside it, and says plainly that pattern matching is mitigation rather than a guarantee.

And no user-facing document mentioned the log at all. Both READMEs and /grok:usage now say what it records, that it is replayed into later sessions, and that deleting it is safe.

A first attempt at that fix was wrong, and was caught

Grok's adversarial pass ran 45 realistic prompts through the broadened redaction and found 19 of them mangledapi_key: required, DATABASE_URL: string, Bearer authentication-middleware. A history that eats ordinary task descriptions is worse than no history. The rule is now two-tier, separators and quotes survive, and all 19 are pinned as a regression corpus.

A gate that could not protect what it guarded

The packaging test checked that each tool name appeared somewhere in the bundle — and the routing module embeds those names as advice literals. Exactly the three tools the approval hook gates were the three the gate could not see. Deleting an entire tool registration and rebuilding left the whole suite green. It now checks the actual registration calls, as an exact set.

Smaller, measured

/grok:cli now declares when it truncated output instead of silently returning the last 4 KB of an 81 KB document — which is why /grok:inspect could never do what its own instructions said. A non-existent working directory is refused before spawning, rather than surfacing as "check your grok install". The shell-injection ban now covers every source file instead of one. And the orchestrator docs no longer tell consumers to re-call the planning tool after their human approval gate, which never delegated.

Recorded, but refuted

Floating CI action tags, non-strict status checks, the whole-file history read (measured at 3.6% of what the status call already waits on), and "four releases shipped untagged" — which died to its own reproduction command sorting version tags as strings.

Verified at this commit: npm ci, 352 tests passed / 1 skipped (up from 304), typecheck clean, both committed bundles rebuild byte-identical.

Full detail: docs/releases/v0.2.14.md