본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 26. 10:47

Codex가 SKILL.md를 220행에서 끊어 읽고 있었던 이야기

요약

Codex CLI를 사용하여 코딩 에이전트용 skill 파일을 읽을 때, 모델이 파일 전체를 읽지 않고 특정 행(220행)에서 멈추는 현상을 분석했습니다. 조사 결과, 이는 도구의 버그가 아니라 모델이 스스로 sed 명령어를 생성하여 파일의 일부만 읽도록 구성한 결과임을 밝혀냈습니다.

핵심 포인트

  • Codex 모델이 파일 읽기 시 sed 명령어를 직접 생성함
  • 대부분의 태스크에서 220행 부근에서 읽기가 중단됨
  • 중요한 지시 사항(stop condition 등)이 누락되는 문제 발생
  • 모델이 tool_call 인자로 직접 명령어를 구성하는 특성 확인

Quaere라는 skill 집합을 만들고 있다. 코딩 에이전트(Coding Agent)가 대충 처리하기 쉬운 부분에 게이트를 설치하는 5개의 skill이다.

만들다 보니 확인해두고 싶은 것이 생겼다. 작성한 skill을 에이전트가 정말 마지막까지 읽고 있는 것일까?

Terminal-Bench를 돌렸을 때의 로그를 보고, Codex CLI가 skill 파일을 어떻게 읽고 있는지, 그 실제 명령어를 전부 수집해 보았다. 결과는 꽤나 허탈했다.

quaere-evidence라는 skill은 441행이다. Codex는 그중 220행을 읽었고, 나머지 221행은 단 한 번도 읽지 않았다.

정확히 220행에서 멈춘다

Codex가 로컬 파일을 읽을 때는 셸 명령어(Shell Command)를 사용한다. skill 파일도 마찬가지로, 로그를 보면 다음과 같은 명령어가 나열되어 있다.

sed -n '1,220p' ~/.agents/skills/quaere-evidence/SKILL.md

sed -n '1,220p'는 "1행부터 220행까지 출력하라"는 의미다. 221행 이후는 이 명령어의 출력에 포함되지 않는다.

단 한 번뿐이라면 우연히 그런 명령어를 선택했다고 치부할 수 있다. 하지만 로그를 통해 살펴보니 그렇지 않았다. skill을 읽은 41개의 태스크 중, 39개의 태스크가 읽기 깊이가 정확히 220행에 맞춰져 있었다.

만약을 위해 보충하자. 이것은 "첫 번째 시도가 우연히 220이었다"는 이야기가 아니다. 어떤 태스크에서도 221행부터 가져오려는 두 번째 시도── sed -n '221,441p'와 같은 명령어──는 나오지 않았다. 읽기는 220에서 끝났다. 남은 2개의 태스크도 딱 220은 아니었지만, 역시 200행대 어딘가에서 멈춰 있었고 skill의 끝까지는 도달하지 못했다. 120행도 441행도 아닌, 매번 200행대 초반. 우연히 이런 일치가 발생할 수는 없다.

이 측정은 Quaere 리포지토리의 evals/에 있는 runner로 Terminal-Bench를 돌리고, 각 태스크에 남는 sessions/agent.log를 확인하면 검증할 수 있다. skill을 읽는 sed 명령어는 그곳에 그대로 기록되어 있다.

Quaere의 skill은 현재 모두 220행을 초과한다. 즉, 전부 후반부가 구조적으로 읽히지 않은 채 실행되고 있었다는 뜻이다.

skill행수읽히지 않는 비율
quaere-semantic25614%
...

게다가 220행보다 뒤에 배치되어 있었던 것은── anti-patterns, 예상되는 drift 패턴, stop condition, 다른 skill과의 연계 절차였다. 요컨대 "이런 식으로 대충 하지 마라", "여기까지 오면 멈춰라"라고 적어둔, 가장 지시 강도가 높은 부분이었다. Quaere는 에이전트를 멈추기 위한 skill인데, 그 "멈춰라"라는 부분이 읽히지 않고 있었던 것이다.

이것은 Codex의 버그인가

처음에는 Codex CLI가 잘못된 것이라고 생각했다. sed -n '1,220p'라는 끊기 처리를 Codex의 파일 읽기 프로세스가 고정적으로 수행하고 있는 것이라고.

하지만 아니었다.

Codex CLI에는 파일 읽기 전용 도구가 없다. 파일을 읽는다는 것은 셸 명령어를 생성하여 실행하는 것과 같다. 따라서 에이전트가 스스로 skill을 가져오는 경로에서는 "SKILL.md 본문을 읽기"가 "sed를 입력하기"가 된다. 그리고 sed -n '1,220p'라는 문자열은 Codex CLI가 내보낸 것이 아니라, 모델이 tool_call의 arguments로서 구성한 것이었다. 만약 하네스(Harness) 측에서 220행에서 자르고 있다면, 파일은 하네스가 직접 전달했을 것이고 tool_call의 명령어 문자열에 sed 같은 것은 등장하지 않았을 것이다. sed가 등장했다는 시점에서 명령어를 작성하고 있는 것은 모델 쪽이다.

즉, 220이라는 숫자는 Codex 어딘가에 적힌 설정값이 아니라, 모델의 동작으로부터 나오는 것이다.

다른 각도에서의 확인도 있다. skill

도구 호출형 하네스 (tool-calling harness), 즉 Claude Code나 OpenCode를 통해 동일한 skill을 사용하면 220 문제는 발생하지 않는다. 이러한 하네스에서는 skill 본문을 하네스 측에서 사전에 통째로 읽어 모델에게 전달한다. Claude Code의 transcript 57건을 전수 검증했을 때 (441행의 skill도 매번 441행으로 전달됨), OpenCode의 바이너리 구현에도 skill 본문을 <skill_content> 블록으로 전체를 반환하는 코드가 그대로 심겨 있다. 이러한 경로에서는 sed를 사용할 일이 없으므로, cap이 표면으로 드러날 여지도 없다. Codex CLI와 같이 "파일을 읽으려면 모델 스스로 쉘 명령어를 작성한다"는 아키텍처 안에서만 220이라는 상한선이 보인다.

여기서 한 가지 유보해 둘 점이 있다. "동일한 Codex CLI 하네스에서 구동하는 모델만 Claude 등으로 교체하면 220 문제가 사라지는가"는 이번에 측정하지 못했다. Codex CLI는 구현상 OpenAI의 모델에 종속되어 있으며, 다른 모델을 강제로 올려 비교한 데이터도 수중에 없다. 여기까지 말할 수 있는 것은 "sed 문자열은 모델이 내뱉고 있다", "skill 도구 경로에서는 발생하지 않는다"까지이며, "Codex CLI 자체는 무죄다"라고까지 단정 지을 수는 없다.

역방향 교체 ── 모델은 gpt-5.5인 상태로 유지하고, 하네스만 Claude Code 측으로 교체하는 것 ── 은 claudex를 통해 측정할 수 있었다. claudex는 Claude Code를 OpenAI 호환 엔드포인트 (endpoint)로 향하게 하여 실행하는 래퍼 (wrapper)로, 도구 (tool) 형태와 프롬프트 (prompt)는 Claude Code를 유지하면서 추론은 gpt-5.5가 담당한다. 3번의 세션을 실행한 결과, 3건 모두 첫 번째 동작(first move)은 Skill tool 호출이었다.

sed -n '1,Np' SKILL.md

은 0건이었다. 나아가 나머지 2건에서는 Skill(...) 직후에 SKILL.md 전문 (frontmatter만 제외한 439 / 259 / 353행)이 user-message envelope 형태로 다음 턴에 삽입되었고, 모델은 그것을 읽은 뒤 구체적인 작업에 들어갔다. 남은 1건은 Skill 실행이 claudex 측에서 에러 (error)를 반환했으나, 그럼에도 모델은 sed로 넘어가지 않고 Glob/Bash/Agent 방식으로 진행했다. 즉, 220은 "gpt-5.5가 무조건적으로 하는 일"이 아니라, Codex CLI가 skill 전용 도구를 가지고 있지 않기 때문에 모델이 쉘 (shell)로 넘어가는 것이며, 그 쉘 이디엄 (shell idiom)이 220행에서 나타나는 것 ── 즉 하네스 형태 (harness-shape) 측의 동작이라고 말할 수 있을 것 같다. n=3인 작은 관찰이므로 단정하지는 않겠다.

왜 220인가

이를 이해하려면 Codex의 skill 메커니즘을 조금 살펴볼 필요가 있다.

Codex의 skill은 점진적 공개 (progressive disclosure) 방식으로 동작한다. 기동 시에 읽히는 것은 각 skill의 이름, 설명, 파일 경로뿐이다. 본문 (SKILL.md의 내용)은 에이전트 (agent)가 해당 skill을 사용하기로 결정했을 때 비로소 읽힌다.

이때 "본문을 어떻게 읽는가"가 호출 방식에 따라 달라진다. codex-rs의 소스를 분석한 takiko 씨의 기사에 따르면, 사용자가 skill을 명시적으로 지정한 경우에는 본문이 하네스 측으로부터 자동으로 주입된다. 반면, 자연어로 요청하여 에이전트가 스스로 skill을 선택한 경우에는 ── LLM이 스스로 본문을 읽으러 간다.

그리고 Codex에는 파일 읽기 전용 도구가 없다. 파일을 읽는다는 것은 쉘 명령어를 생성하여 실행하는 것과 같다. 따라서 자연어 호출 경로에서는 "SKILL.md의 본문을 읽는 것"이 "sed를 치는 것"이 된다. 나의 평가 (eval)는 에이전트가 skill을 스스로 찾아내는 쪽 ── 즉 후자의 경로였다. 그래서 sed가 나왔던 것이다.

그렇다면 왜 그 sed는 항상 220인가.

skill을 읽는 상황이 매번 거의 동일하기 때문이라고 생각한다. 일반적인 코드 파일을 읽을 때는 120행이거나 240행인 것처럼 숫자가 들쭉날쭉하다. 태스크(Task)도 이유도 그때마다 다르기 때문이다. 하지만 skill을 읽을 때의 모델 상황은 한 종류뿐이다 ── "skill 시스템이 이 경로의 skill을 사용하라고 명령하고 있다". 동일한 상황이 입력되면 동일한 명령이 나온다. 학습된 "skill을 읽는 명령"의 최빈값이 매번 그대로 나오는 것이다.

그리고 여기가 가장 중요한 지점이다. sed -n '1,220p'

는 모델이 "220행에서 끊어라"라고 상한선을 두고 있는 것이 아니다. 그것은 모델에게 있어서 "SKILL.md를 통째로 읽기" 위한 이디엄(Idiom)이라고 생각한다. 220 ≈ 모델이 생각하는 "SKILL.md의 길이"다.

Agent Skills의 표준은 원래 본문을 짧게 유지하도록 설계되어 있다. 긴 사양(Specification)이나 샘플은 references/로 넘기고, SKILL.md 본체에는 "언제 사용하는가", "어떻게 진행하는가"만을 적는다. 그러한 점진적 공개 (Progressive Disclosure) 원칙에 따라 작성된 SKILL.md는 대략 200행 이내에 들어온다. 따라서 220은 "정상적인 skill의 길이 + 약간의 여백" 정도의 숫자가 된다.

모델이 221행째를 읽으러 가지 않는 이유는, 자기 내부에서는 이미 skill을 전부 다 읽었다고 판단했기 때문이다. 사양대로 작성된 skill이라면 실제로 그것만으로 전문을 읽을 수 있다. 문제가 발생하는 것은 모델이 상정하고 있는 길이를 초과한 skill ── 즉, Quaere와 같은 skill뿐이다.

솔직히 선을 긋자면, "왜 200도 250도 아닌 220인가"라는 마지막 한 자릿수까지는 나도 근거를 확인할 수 없다. 학습 데이터상의 최빈값이 220으로 쏠려 있다는 정도까지만 말할 수 있다. 다만 "220은 모델이 상정하는 skill의 길이"라는 방향성은 측정의 안정성(39/41)과도, 사양의 구조와도 일치한다.

즉, 사양대로 작성하면 된다

이 이야기의 결론은 "Codex에게 더 많이 읽히는 방법"이 아니다.

220행에서의 끊김은 해서는 안 될 버그를 밟고 있었던 것이 아니었다. 모델이 "skill은 짧은 것이다"라고 ── Agent Skills의 사양대로 ── 전제하고 있었고, 그것을 초과한 skill을 작성한 쪽이 그 전제에서 벗어나 있었을 뿐이다. 고쳐야 할 방향은 Codex를 바꾸는 것이 아니라, 모델의 전제에 맞추는 것이 된다.

측정값 자체는 220이었다. 다만 본문에서 썼듯이 220은 마진을 포함한 숫자이며, 모델이 순수하게 상정하는 길이는 그보다 조금 앞선 ── 대략 200행 전후라고 생각한다. 그러므로 스스로 상한선을 정한다면 200으로 두는 것이 안전하다.

  • SKILL.md 본체는 200행 이내로 유지할 것
  • Iron Law, 사용 상황, 핵심 절차, 최소한의 예시는 첫 200행 안에 넣을 것
  • 긴 예시, 안티 패턴 (Anti-patterns), 참조 문서는 references/로 넘길 것

이것은 새로운 이야기가 아니다. "SKILL.md는 짧게"는 OpenAI의 공식 문서에도 적혀 있다. 다만 그것이 "예의 바른 작성법"이 아니라 "지키지 않으면 뒷부분이 물리적으로 읽히지 않는다"라는 제약이었다는 사실은, 나에게 있어 실제 로그를 보기 전까지는 체감되지 않는 부분이었다.

그리고 이것은 Quaere 자신에게 되돌아온다.

Quaere의 skill은 256~441행에 달한다. 리포지토리의 README에 있는 eval 수치 ── in-tree eval에서 +37.7pp, Terminal-Bench에서도 clean 69-task subset에서 +8.7pp ── 는 모두 codex exec를 통해 측정한 것이므로, 본문에서 언급한 220 cap이 그대로 적용된다. 지금 돌이켜보면, 그것은 "Codex가 뒷부분을 읽지 않은 채 실행한 skill"을 측정한 수치였다. 게다가 읽히지 않은 부분은 stop condition이나 anti-patterns와 같이 가장 "멈춰야 한다"라고 적어둔 부분이었다. Codex는 브레이크 설명서의 뒷부분을 가린 채 Quaere를 주행시키고 있었던 셈이다.

이것이 "전문을 다 읽었다면 수치가 더 좋았을 것"인지, 아니면 "처음 200행만으로 실질적으로 충분했던 것"인지는 현재의 데이터로는 구분할 수 없다. skill을 200행 이내로 맞춘 버전으로 다시 측정하는 것이 다음에 할 일이다. 그때까지 저 수치는 "Codex가 잘라낸 Quaere"의 수치라는 단서 조항이 필요하다.

직접 작성한 SKILL.md와 에이전트가 실제로 읽는 SKILL.md는, 같은 길이인 한 서로 다른 문서가 된다. 221행 이후는 자신이 썼다고 생각하더라도 상대에게는 전달되지 않는다. skill을 짧게 유지하라는 것은 아마도 그런 의미일 것이다.

마치며

이 기사는 takiko 님의 "Codex CLI는 Agent Skills를 어떻게 구현하고 있는가"── loader의 구현 자체를 추적한 기사──의, 동작 측면에서의 후속편으로 작성했다. 구현 편에 대한 동작 편이다.

측정에 사용된 생 데이터(raw data)는 별도의 노트에 정리되어 있다:

측정에 사용한 skill 집합 Quaere는 이쪽.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0