[AI] Codex를 기존 .claude 하네스에 태우기
규칙을 두 벌로 만들면 무슨 일이 생기는가
코딩 에이전트를 오래 쓰면 규칙과 스킬과 에이전트와 훅(Hook)이 특정 런타임 하나의 디렉토리 구조에 맞춰 쌓인다. 같은 레포에서 Codex도 돌리기 시작하면 그 규칙을 다시 만들어야 하는가라는 문제가 생긴다.
가장 손쉬운 방법은 .claude/ 를 통째로 복사해 Codex가 읽는 자리에 옮기는 것이다. 이 방법은 복사한 그날에만 동작한다. Claude 쪽 규칙 한 곳을 고치고 사본에 반영하지 않으면 두 런타임이 서로 다른 기준으로 판단하기 시작한다.
find .claude/skills -maxdepth 1 -type d ! -name skills | wc -l # 15
find .claude/agents -maxdepth 1 -name '*.md' ! -name README.md | wc -l # 16
find .claude/rules -maxdepth 1 -name '*.md' ! -name README.md | wc -l # 11
find .claude/hooks -maxdepth 1 -name '*.sh' | wc -l # 13| 표면 | 개수 | 무엇을 정의하는가 |
|---|---|---|
| 스킬 | 15 | 자연어 요청을 절차로 바꾸는 진입점 |
| 에이전트 | 16 | 특정 역할을 맡는 서브에이전트 정의 |
| 규칙 | 11 | 파일 경로별로 적용하는 작성과 리뷰 규칙 |
| 훅 | 13 | 도구 호출 전후에 끼어드는 게이트 |
| 문서와 의존 지도와 템플릿과 작업 원칙 | 16 | 워크플로 절차, 의존 관계 지도, 산출물 양식 |
복제가 만드는 두 벌의 원본
복사본을 만든 순간 원본이 두 개가 된다. 어느 쪽이 최신인지 알려주는 장치가 없어서, 규칙이 갈라졌다는 것은 두 런타임이 다르게 판단한 뒤에야 드러난다. 갈라진 지점을 찾는 비용은 사본이 오래 살아 있을수록 커진다.
어댑터(Adapter) 패턴은 이미 존재하는 인터페이스를 다른 인터페이스처럼 쓰게 만드는 구조다. 기존 대상을 고치지 않는다는 점이 핵심이라, 원본을 그대로 두고 사이에 얇은 해석층만 하나 둔다. 이 레포에서는 세 파일이 그 층을 맡는다.
| 층 | 파일 | 무엇을 옮기는가 |
|---|---|---|
| 컨텍스트 | AGENTS.md | 규칙과 스킬과 에이전트와 문서 표면을 Codex가 같은 의미로 읽게 한다 |
| 훅 등록 | .codex/hooks.json | 훅 13개를 Codex 훅 이벤트에 매핑한다 |
| 훅 규격 | .codex/hooks/claude-hook-bridge.py | 입력 형태와 응답 규격의 차이를 흡수한다 |
세 파일 어디에도 원본과 사본을 잇는 심볼릭 링크(Symbolic Link)가 없고, 실행할 때마다 문서를 다시 써 주는 생성기도 없다. 어댑터는 원본을 읽어 옮길 뿐 복사본을 남기지 않는다.
# 잘못된 패턴: .claude 를 새 런타임 전용 디렉토리로 복사한다
cp -r .claude .codex-context
# 이 순간 원본이 두 개가 되고, 한쪽만 고치면 그 즉시 어긋난다
# 올바른 패턴: 원본은 그대로 두고 얇은 해석층이 가리키게 한다
# AGENTS.md 는 .claude/** 를 매 세션 다시 읽고,
# .codex/hooks.json 은 .claude/hooks/*.sh 를 경로로 참조한다
find . -type l | wc -l # 0 (심볼릭 링크 없음)
find . -iname 'build.py' # (생성기 없음)AGENTS.md, 원본을 가리키는 진입 문서
Codex는 레포 루트의 AGENTS.md 를 세션마다 자동으로 읽는다. 그래서 이 파일은 규칙을 새로 쓰는 자리가 아니라, 원본을 어떤 순서로 어떻게 읽을지만 적는 자리가 된다.
# AGENTS.md 첫 문단 (원문 그대로)
이 파일은 Codex 자동 로드용 어댑터다. 프로젝트 운영 규칙의 단일 진실
공급원은 계속 CLAUDE.md 와 .claude/ 이며, 여기에는 Codex가 같은
컨텍스트를 읽고 같은 절차로 움직이게 하는 연결 규칙만 둔다.먼저 읽히는 문서를 이름으로 배제한다
해석층에는 자기 앞에 무엇이 먼저 붙는지 아는 일도 포함된다. codex debug prompt-input 으로 모델에 전달되는 입력을 보면, 레포 AGENTS.md 보다 앞에 전역 홈의 AGENTS.md 블록이 들어와 있다. 레포 어댑터가 이를 명시로 덮지 않으면 먼저 읽힌 쪽이 그대로 작업 컨텍스트에 남는다.
# AGENTS.md Scope Boundary 발췌 (경로 일반화)
Codex 는 이 파일보다 먼저 전역 홈의 AGENTS.md 블록을 프롬프트에
주입한다. 그 블록은 단일 진실 공급원과 게이트를 자기 기준으로
선언하지만, 이 레포 작업에는 적용하지 않는다.상위 워크스페이스 디렉토리든 전역 홈이든, 먼저 읽히는 문서가 있으면 배제 대상을 이름으로 적어야 한다는 규칙은 같다.
자동 호출이 없는 자리를 메우는 라우팅 표
Claude Code에서는 자연어 요청이 각 스킬 문서의 설명과 매칭되어 자동으로 호출된다. Codex 쪽 스킬 목록에는 이 레포의 스킬 15개가 뜨지 않아서, 어댑터가 자연어 트리거를 스킬 이름으로 직접 잇는 표를 대신 둔다.
| 요청 형태 | 라우팅되는 스킬 |
|---|---|
| 모듈 위치나 실행 디렉토리 질문 | 모듈 라우터 스킬 |
| 티켓 키와 구현 의도가 함께 온 요청 | 개발 오케스트레이터 스킬 |
| push 직전 자가 점검 요청 | 셀프 코드 리뷰 스킬 |
| PR(Pull Request) 본문 작성 요청 | PR 작성 스킬 |
| PR 리뷰 요청 | PR 리뷰 스킬 |
처리 순서도 함께 적는다. 요청에 스킬명이 직접 적혀 있으면 그것을 먼저 찾고, 없으면 표의 자연어 트리거를 적용한다. 둘 이상이 매칭되면 오케스트레이터 스킬을 우선하고, 오케스트레이터가 하위 스킬을 호출하는 절차라면 개별 스킬을 임의로 먼저 실행하지 않는다.
.codex/hooks.json, 훅을 문서가 아니라 등록으로 옮긴다
Claude Code에서는 이 등록이 곧 강제라서, 훅이 차단하면 도구 자체가 실행되지 않는다.
| 시점 | 등록 횟수 | 예 |
|---|---|---|
| 세션 시작 | 1 | 모듈과 JDK와 시크릿 안내 |
| 도구 호출 직전 | 9 | 위험한 셸 명령과 민감 파일 읽기 차단 |
| 도구 호출 직후 | 3 | 코드 변경 후 정적 분석 안내 |
Codex에도 훅이 있고 레포 로컬 .codex/hooks.json 이 공식 탐색 경로다. 훅만큼은 문서로 옮겨 적는 대신 등록할 수 있다는 뜻이다. 어댑터는 스크립트를 복제하지 않고 경로로 가리키기만 한다.
{
"matcher": "^Bash$",
"hooks": [
{ "type": "command",
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/claude-hook-bridge.py\" .claude/hooks/custom-block-git-push.sh" }
]
}브리지가 흡수하는 세 가지 규격 차이
.claude/hooks/*.sh 는 한 줄도 고치지 않는다. 앞 대조표에서 형태가 다른 것으로 분류한 항목은 사이에 낀 브리지가 흡수한다. 어댑터 패턴의 정의에 가장 가까운 조각이 이 파일이다.
| 차이 | Claude 쪽 전제 | Codex 쪽 현실 | 브리지 처리 |
|---|---|---|---|
| 파일 편집 입력 | tool_input.file_path 에 대상 경로가 온다 | apply_patch 는 패치 본문 전체가 문자열로 온다 | 본문에서 경로를 뽑아 file_path 를 채우고, 여러 파일이면 파일마다 훅을 반복 호출한다 |
| 승인 프롬프트 | permissionDecision: ask 로 사용자에게 묻는다 | 파싱만 되고 아직 동작하지 않는다 | 차단으로 내리고 사람이 할 일을 사유에 적는다 |
| 도구 이름 | Bash, Read, Edit, Write | Bash, apply_patch, mcp__* 뿐이고 Read 가 없다 | 파일 읽기 차단을 Bash 매처 훅으로 옮긴다 |
Claude 설정을 그대로 옮겨 적으면 아래 그룹은 문법상 멀쩡한 채로 영원히 발화하지 않는다.
{
"matcher": "^Read$",
"hooks": [
{ "type": "command", "command": "python3 .codex/hooks/claude-hook-bridge.py .claude/hooks/custom-secrets-read-guard.sh" }
]
}Codex는 파일을 셸로 읽으므로 같은 목적의 게이트를 Bash 매처로 옮겨야 한다. 다음과 같이 claude의 훅을 사용할 수 있다.
{
"matcher": "^Bash$",
"hooks": [
{ "type": "command", "command": "python3 .codex/hooks/claude-hook-bridge.py .claude/hooks/custom-block-env-read.sh" }
]
}요청 한 건이 어댑터를 지나는 순서
지금까지 붙인 조각을 한 요청의 흐름으로 이으면 다음과 같다. 사용자가 티켓 키와 구현 의도를 함께 보내면 Codex는 라우팅 표를 거쳐 오케스트레이터 스킬을 찾고, 구현 승인 게이트 앞뒤로 다른 경로를 탄다.
sequenceDiagram autonumber participant U as 사용자 participant C as Codex participant A as AGENTS.md participant S as 스킬문서 participant Ag as 에이전트정의 participant H as 훅게이트 U->>C: 티켓 키와 구현 의도 C->>A: 자연어 트리거 매칭 A-->>C: 오케스트레이터 스킬 지정 C->>S: 전체 절차 읽기 alt 구현 승인 게이트 이전 C->>U: 요구사항만 확인, 코드 편집 보류 else 구현 승인 이후 C->>Ag: 역할 정의 조회 Ag-->>C: 범위 경계와 절차 적용 C->>H: 도구 호출 전 게이트 통과 alt 훅이 승인됨 H-->>C: 브리지 경유 실행 결과 else 훅이 미승인 H-->>C: 조용히 스킵, 수동 규칙 적용 end C-->>U: 산출물 반환 end
스킬 선택과 게이트 판단과 역할 위임까지는 AGENTS.md 한 장이 끌고 가지만, 마지막 훅 구간만 문서가 아닌 프로세스가 판정한다.
어댑터가 붙었는지 확인하는 세 가지 명령
어댑터를 붙이는 일과 같게 도는지 확인하는 일은 별개다. 확인할 것이 세 가지이고 각각 다른 명령으로 본다.
첫째는 어댑터가 원본과 어긋나지 않았는지다. validate-harness.sh 는 원래 README 카운트와 실제 파일 수 일치, 훅 파일의 양방향 등록, 문서의 하드코딩 카운트 부재, 위키링크 무결성 네 가지를 검사하던 스크립트다.
bash .claude/scripts/validate-harness.sh== 6. Codex hooks.json ↔ Claude hooks 어댑터 ==
✓ .codex/hooks.json: JSON 유효
✓ .codex/hooks.json: Claude hook bridge 사용
✓ .codex/hooks.json: 모든 Claude hook 등록
✓ .codex/hooks.json: SessionStart 가 clear/compact 트리거까지 커버
✓ .codex/hooks.json: Codex 미존재 tool 만 매칭하는 죽은 그룹 없음
✓ claude-hook-bridge.py: Codex 미지원 ask fail-closed 처리
✓ claude-hook-bridge.py: apply_patch 파일 경로 보정
✓ harness 정합: 경고 0둘째는 컨텍스트가 세션에 붙었는지다. 문서가 정합해도 Codex가 읽지 않으면 소용이 없으므로, 모델에 전달되는 입력을 그대로 뽑아 확인한다.
codex debug prompt-input "test"출력에서 project-doc 표지 뒤에 레포 AGENTS.md 본문이 통째로 들어가 있으면 컨텍스트 층은 붙은 것이다. 이 명령은 그 앞에 무엇이 먼저 주입되는지도 함께 보여주므로, 배제 대상을 정하는 근거로도 쓴다.
셋째는 훅이 지금 사용되는지이다.
grep -c '<레포경로>/.codex/hooks.json' "$CODEX_HOME/config.toml"이 값이 0이면 훅은 하나도 돌지 않는다. 경고 0건과 게이트 0개가 동시에 성립한다는 뜻이라 붙었는가와 지금 도는가를 각각 확인해야 한다.
어댑터를 만들 상황과 만들지 않을 상황
| 상황 | 선택 | 근거 |
|---|---|---|
| 런타임이 하나뿐이다 | 어댑터를 만들지 않는다 | 해석층은 같은 정책을 다른 형태로 읽는 두 번째 호출자가 있을 때만 값어치가 있다 |
| 원본 규칙이 자주 바뀌지 않는다 | 손으로 쓴 해석 문서로 시작한다 | 생성기를 먼저 만드는 비용이 표면을 손으로 옮기는 비용보다 크다 |
| 원본 규칙이 자주 늘어난다 | 전수 대조 검증 스크립트를 함께 둔다 | 검증이 없으면 해석 문서가 조용히 낡는다 |
| 새 런타임에도 훅 기능이 있다 | 문서 규칙에 그치지 말고 훅을 등록한다 | 지시는 지켜지길 바라는 것이고 등록은 프로세스가 가로채는 것이다 |
| 훅을 등록했다 | 발화 여부를 정합성 검증과 별도 명령으로 확인한다 | 미승인 훅은 경고 없이 스킵되어 검증 경고와 게이트가 함께 0이 된다 |
| 두 런타임의 도구 이름이 다르다 | 상대에게 없는 도구를 매칭하는 그룹을 검증으로 잡는다 | 발화하지 않는 매처는 이름만 남아 보호받고 있다고 착각하게 만든다 |
| 먼저 읽히는 상위 문서가 있다 | 배제 대상을 이름으로 적는다 | 선언하지 않으면 먼저 읽힌 문서가 작업 컨텍스트에 그대로 남는다 |
| 상대에게 없는 능력을 만났다 | 열어 두지 말고 막는 쪽으로 정한다 | 승인 판정을 통과로 처리하면 게이트가 조용히 사라진다 |