Codex 작업 중 트러블슈팅 기록을 자동화한 이야기
부제: 오류는 지나가도 로그는 남는다

Codex로 개발을 하다 보면 작업 흐름이 꽤 빠르게 지나간다.
검색하고, 코드를 읽고, 수정하고, 빌드하고, 다시 고치고, 검증한다.
이 과정 자체는 편하다.
문제는 나도 같이 빠르게 까먹는다는 점이었다.
분명 방금 고쳤는데,
10분 뒤의 나는 이렇게 된다.

> “근데 이거 왜 고쳤더라?”
그래서 이번에는 Codex로 작업하면서 발생하는 오류와 트러블슈팅 과정을 놓치지 않기 위해,
작업 중 발생한 오류 후보를 자동으로 수집하고, 마지막에 정리할 수 있는 구조를 만들어봤다.
정확히 말하면 이건 “트러블슈팅 문서를 완전 자동 작성하는 구조”는 아니다.
핵심은 이것이다.
> 오류 후보 증거는 자동으로 모으고,
> 최종 기록은 작업이 끝난 뒤 판단한다.
즉, 자동화한 것은 “문서 작성”이라기보다
**기록할 근거를 잃지 않게 만드는 과정**에 가깝다.
---
1. 왜 이런 자동화가 필요했나
내가 작업하던 프로젝트는 WPF/C# 기반 프로젝트였다.
그래서 문제가 한 종류로만 나오지 않았다.
- dotnet 빌드 환경 문제
- XAML 바인딩 문제
- ViewModel 상태 관리 문제
- API 응답 shape 불일치
- mock 데이터와 real API 응답 차이
- mapper, renderer 쪽 데이터 처리 문제
이런 것들이 섞여서 발생했다.

이 정도면 오류가 발생한 게 아니라
오류들이 단체로 회식하러 온 수준이었다.
WPF 프로젝트 특:
> 하나 고치면 빌드가 울고,
> 빌드 고치면 바인딩이 울고,
> 바인딩 고치면 ViewModel이 조용히 고개를 든다.
개발 중에는 일단 고치는 게 급하다.
그런데 문제를 고치고 나면,
정작 나중에 회고나 MR 설명을 쓸 때 이런 상태가 된다.
> “이거 왜 고쳤지?”
> “어디서 터졌지?”
> “원인이 뭐였지?”
> “그때 로그 어디 갔지?”
그리고 결국 또 코드를 뒤진다.
이게 너무 비효율적이었다.
그래서 생각했다.
이 고통…
이 원인…
이 해결 과정…
전부 동결건조해서 미래의 나에게 보내야 한다.

> 미래의 나야, 이 정도면 많이 도와줬다.
---
## 2. 내가 만든 구조의 핵심 아이디어
전체 흐름은 이렇게 잡았다.
Codex 작업 수행
↓
Bash 명령 실행
↓
PostToolUse hook 실행
↓
Python 스크립트가 명령 결과 분석
↓
오류 후보를 raw-events.jsonl에 저장
↓
작업 완료 후 AGENTS.md 규칙에 따라 기록 여부 판단
↓
필요하면 HR-troubleshooting.md에 최종 정리
여기서 가장 중요한 점은
hook이 최종 트러블슈팅 문서를 직접 수정하지 않는다는 것이다.
처음에는 이렇게 생각할 수도 있다.
“오류 감지되면 바로 Markdown에 쓰면 되는 거 아닌가?”
하지만 실제로 해보면 그렇게 단순하지 않다.
검색 명령 결과에 error라는 문자열이 포함될 수도 있고,
rg 명령이 검색 결과를 못 찾아서 non-zero exit code를 반환할 수도 있다.
이런 것까지 전부 문서에 들어가면
트러블슈팅 문서는 금방 지저분해진다.
아무 오류나 문서에 쓰면
그건 기록이 아니라 낙서장이다.
아니, 더 정확히 말하면
개발자의 비명 아카이브가 된다.

그래서 역할을 나눴다.
- hook: 오류 후보 증거를 raw log로 수집
- AGENTS.md: 언제, 무엇을, 어떻게 기록할지 규칙 정의
- HR-troubleshooting.md: 실제 해결된 문제만 정리하는 최종 지식 저장소
즉, 자동화한 것은 “최종 문서 작성”이 아니라
기록할 근거를 잃지 않게 만드는 과정이었다.
3. Codex hook 설정
그럼 이걸 어떻게 자동화했냐면…
다음 구조가 궁금하다면
냐냐냥!!! 을 입력하세요.

Codex hook 설정은 .codex/config.toml에 둔다.
이 파일에서는 다음 역할을 한다.
- Codex hook 기능 활성화
- PostToolUse hook 등록
- Bash 도구 사용 직후 Python 스크립트 실행
- command evidence 수집 상태 메시지 표시
블로그용으로 경로는 익명화하면 대략 이런 형태다.
[features]
codex_hooks = true
[[hooks.PostToolUse]]
matcher = "^Bash$"
[[hooks.PostToolUse.hooks]]
type = "command"
command = "python <PROJECT_ROOT>/.codex/hooks/log_trouble_event.py"
timeout = 10
statusMessage = "Collecting command evidence"
위 코드는 실제 설정을 설명하기 위한 축약 예시다.
실제 프로젝트에서는 Windows 로컬 절대 경로가 들어가 있었지만,
공개 글에는 <PROJECT_ROOT>처럼 바꿔서 쓰는 것이 좋다.
여기서 핵심은 이 부분이다.
matcher = "^Bash$"
Codex가 Bash 도구를 실행한 직후 hook이 동작하고,
그 결과를 Python 스크립트가 받아 분석한다.
설정은 생각보다 단순하다.
Bash 도구가 실행된 직후,
Python 스크립트를 한 번 태운다.
말은 단순한데,
이때부터 나는 약간 이런 상태였다.
“어? 이거 잘못하면 모든 로그가 다 잡히는 거 아닌가?”

4. Hook 스크립트가 하는 일
hook 스크립트는 .codex/hooks/log_trouble_event.py에 있다.
이 스크립트는 대충 이런 일을 한다.
Codex가 명령을 실행한다.
뭔가 넘어진다.
그러면 옆에서 조용히 적는다.
“방금 넘어졌고요, 명령어는 이거였고요, 대충 이런 에러였습니다.”

조금 더 기술적으로 정리하면, 이 스크립트가 하는 일은 다음과 같다.
- stdin으로 들어온 JSON payload를 읽는다.
- 실행한 명령어를 추출한다.
- tool response를 문자열로 변환한다.
- exit code가 0이 아닌지 확인한다.
- 응답 텍스트에서 오류 패턴을 찾는다.
- 읽기/검색 명령과 빌드/검증 명령을 구분한다.
- dotnet sandbox, NuGet restore, code/feature 관련 오류를 분류한다.
- 후보 이벤트를 frontend/.troubleshooting/raw-events.jsonl에 append한다.
- hook 자체에서 예외가 나도 Codex 작업을 막지 않도록 조용히 종료한다.
핵심 로직을 단순화하면 이런 느낌이다.
def is_troubleshooting_candidate(tool_response, response_text, command):
if has_non_zero_exit_code(tool_response, response_text):
return True
if is_passive_inspection_command(command):
return False
if not is_validation_or_execution_command(command):
return False
return has_error_pattern(response_text)
실제 코드에서는 Get-Content, rg, ls 같은 명령을
passive inspection, 즉 읽기/검색성 명령으로 본다.
반대로 아래 같은 명령은 검증/실행 명령으로 본다.
- dotnet build
- dotnet test
- npm run
- pytest
- tsc
- eslint
그리고 오류 후보가 되면 다음과 같은 정보를 raw event로 저장한다.
- timestamp
- session_id
- turn_id
- tool_name
- command
- response_excerpt
- cwd
- event_hash
- category
- root_cause_key
- record_priority
여기서도 주의할 점이 있다.
raw log에는 command와 response excerpt가 들어간다.
그래서 API URL, 토큰, 로컬 경로 같은 민감 정보가 섞일 수 있다.
공개 글이나 외부 공유 자료에 그대로 올리면 안 된다.
로그는 소중하지만,
민감 정보까지 소중하게 공개하면 안 된다.

5. 왜 raw log와 최종 문서를 분리했나
이 구조에서 가장 중요하게 본 부분은
raw log와 최종 문서의 분리다.
처음에는 오류가 나면 바로 Markdown에 적어도 되지 않나 싶었다.
하지만 곧 깨달았다.
세상에는 오류가 있고,
오류처럼 보이는 것도 있고,
그냥 rg가 검색 결과를 못 찾은 것도 있다.
이 셋을 한 문서에 넣으면
그날로 트러블슈팅 문서는 대혼돈의 멀티버스가 된다.

예를 들어 검색 명령을 실행했는데 결과 안에 error라는 단어가 들어갈 수 있다.
이건 코드에 에러가 있다는 뜻이 아니다.
단순히 소스 코드 안의 문자열을 찾은 것일 수 있다.
또 rg로 무언가를 검색했는데 결과가 없으면 exit code가 1이 될 수 있다.
이 역시 실제 빌드 실패나 기능 오류가 아니다.
그냥 “검색 결과 없음”일 뿐이다.
그런 것까지 최종 문서에 들어가면
트러블슈팅 문서는 지식 저장소가 아니라 로그 쓰레기장이 된다.
그래서 나는 raw evidence를 먼저 모으고,
최종 문서에는 실제로 해결한 문제만 남기는 방식으로 정리했다.
정리하면 이렇게 볼 수 있다.
raw-events.jsonl
- 실패 명령
- response excerpt
- exit code
- cwd
- event hash
HR-troubleshooting.md
- Issue
- Root Cause
- Solution
- Note
raw log는 재료고,
troubleshooting 문서는 완성 요리다.
재료를 바로 상에 올리면 안 된다.

raw log는 재료고, troubleshooting 문서는 완성 요리다.
이 방식의 장점은 명확했다.
- 오류 후보는 놓치지 않는다.
- 하지만 최종 문서는 오탐으로 더럽히지 않는다.
- 작업이 끝난 뒤 맥락을 보고 기록 여부를 판단할 수 있다.
- 같은 환경 이슈를 반복해서 기록하지 않을 수 있다.
결국 트러블슈팅 문서는 로그 저장소가 아니라
지식 저장소여야 한다고 봤다.
raw log는 근거이고,
최종 문서는 정제된 기록이다.
6. 잠깐 쉬어가는 장시간 디버깅
여기서 잠깐.
장시간 디버깅입니다.
옆자리를 골라주세요.
[1] dotnet build 실패
[2] XAML Binding 오류
[3] API response shape 불일치
[4] mock에서는 됐는데 real에서 안 됨
[5] renderer가 조용히 이상함
[6] 원인은 모르겠지만 아무튼 안 됨

이런 문제들을 계속 만나면서 느낀 건 하나였다.
기록하지 않으면 미래의 내가 다시 맞는다.
그래서 기록 자동화가 필요했다.
정확히는,
기록할 수 있는 증거를 자동으로 남기는 구조가 필요했다.
7. AGENTS.md의 역할
여기서 AGENTS.md가 등장한다.
내 기준으로 AGENTS.md는 그냥 안내문이 아니라
Codex에게 붙여둔 개발 생활기록부에 가깝다.

AGENTS.md에는 트러블슈팅 기록을
언제, 무엇을, 어떻게 남길지 정의되어 있다.
가장 중요한 규칙은 이것이다.
트러블슈팅 검토와 최종 기록은
작업 완료 후 마지막 단계에서만 한다.
즉, 개발 작업을 시작하자마자 트러블슈팅 문서를 열어보거나 수정하지 않는다.
먼저 사용자가 요청한 작업을 끝내고,
검증까지 마친 뒤,
마지막에 기록할 만한 문제가 있었는지 판단한다.
이 규칙이 없으면 Codex가 작업 중간에 문서부터 건드릴 수 있다.
그러면 또 이런 일이 생긴다.
작업은 덜 끝났는데
트러블슈팅 문서만 열심히 쓰고 있음
그건 자동화가 아니라 산만함이다.
그래서 AGENTS.md에는 이런 식의 규칙을 둔다.
- 작업 중간에 문서부터 수정하지 않기
- 오류 후보와 실제 오류 구분하기
- 같은 root cause 또 쓰지 않기
- 검색 결과의 error 문자열 보고 호들갑 금지
거의 잔소리 모음집이다.
하지만 필요한 잔소리였다.
Codex야 Codex야~
왜요쌤~
작업 중간에 HR-troubleshooting.md 바로 수정하지 말랬지~
죄송해요쌤~

기록 대상으로 보는 문제는 다음과 같다.
- 실제 코드 또는 기능 문제를 고친 경우
- API response shape가 클라이언트 가정과 달랐던 경우
- mock 데이터와 real API 응답이 달랐던 경우
- ViewModel 상태 관리가 잘못된 경우
- XAML binding mode, source, target 동작이 잘못된 경우
- mapper, parser, renderer, branch selection 로직이 잘못된 경우
- UX 문구나 흐름이 사용자 오해를 유발해 수정한 경우
반대로 기록하지 않는 것도 정해두었다.
- harmless exploratory command 실패
- 코드 수정과 무관한 일시적인 sandbox 실패
- 단순 검색 결과에 포함된 error 문자열
- 이미 같은 root cause로 기록된 dotnet sandbox 권한 문제
특히 dotnet sandbox 문제처럼 반복해서 나올 수 있는 환경 이슈는
root cause 기준으로 중복 기록을 막도록 했다.
예를 들어 DOTNET_CLI_HOME 설정 문제나
System.UnauthorizedAccessException 같은 것은 같은 원인으로 보고,
이미 기록되어 있으면 새 항목을 만들지 않는다.
이 부분이 없으면 트러블슈팅 문서는 금방
“빌드 실패 로그 모음.zip”이 되어버린다.
8. HR-troubleshooting.md 문서 구조
최종 기록은 frontend/HR-troubleshooting.md에 남긴다.
문서는 크게 두 섹션으로 나뉜다.
# Troubleshooting Log
## 기능/코드 트러블슈팅
## 빌드/환경 트러블슈팅
기능/코드 트러블슈팅에는 실제 코드 수정으로 해결한 문제가 들어간다.
예를 들면 이런 것들이 여기에 해당한다.
- XAML 바인딩 문제
- ViewModel 상태 문제
- API 응답 매핑 문제
- renderer 문제
- mapper/parser 처리 문제
반대로 빌드/환경 트러블슈팅에는
로컬 환경이나 빌드 도구 문제가 들어간다.
예를 들면 이런 것들이다.
- NuGet restore 누락
- dotnet CLI first-time setup 권한 문제
- sandbox 권한 문제
기록 템플릿은 대략 이런 형태다.
## [YYYY-MM-DD HH:mm] 문제 제목
- Issue:
어떤 작업 중 어떤 문제가 발생했는지 정리한다.
- Root Cause:
근본 원인을 설명한다.
- Solution:
어떤 방식으로 해결했는지 적는다.
- Note:
재발 방지를 위해 기억할 점을 남긴다.

드디어 로그가 사람이 읽을 수 있는 문서가 되는 순간
이 구조를 유지하면 나중에 다시 볼 때 좋다.
단순히 “뭔가 안 됐다”가 아니라,
어떤 상황에서 문제가 발생했고,
왜 발생했고,
어떻게 해결했는지가 한눈에 들어온다.
또 기능 문제와 환경 문제를 분리하면 문서 품질도 좋아진다.
코드 버그를 찾으려는 사람이
dotnet sandbox 권한 이슈를 계속 보지 않아도 된다.
반대로 환경 세팅 문제를 찾는 사람은
빌드/환경 섹션만 보면 된다.
문서도 결국 독자가 있다.
미래의 나도 독자다.
그리고 미래의 나는 생각보다 인내심이 없다.
9. 실제로 얻은 장점
이 구조를 만들고 가장 크게 느낀 장점은
“나중에 다시 찾는 시간”이 줄었다는 점이다.
예전에는 오류가 해결되면 기뻐서 넘어갔다.
그리고 다음날 다시 만났다.
반갑지 않은 재회였다.

Codex로 작업하면 명령 실행과 수정이 빠르게 지나간다.
그런데 raw evidence가 남아 있으니,
어떤 명령에서 실패했는지,
어떤 응답이 있었는지 다시 확인할 수 있다.
그리고 최종 문서는 단순 로그가 아니라
해결된 문제 중심으로 남는다.
이게 생각보다 중요했다.
로그는 많을수록 좋은 것이 아니다.
너무 많으면 아무도 안 본다.
반면 해결된 문제를 원인과 해결책 중심으로 정리해두면
회고, MR 설명, 기술 블로그 소재로 재사용하기 좋다.
실제로 도움이 된 지점은 다음과 같다.
- 오류 원인을 다시 찾는 시간이 줄었다.
- Codex가 어떤 명령에서 실패했는지 근거가 남았다.
- 단순 raw log가 아니라 해결된 문제 중심으로 정리할 수 있었다.
- MR 설명이나 회고에 쓸 수 있는 맥락이 남았다.
- 팀원이 비슷한 문제를 만났을 때 참고할 수 있는 문서가 생겼다.
- 기능 문제와 환경 문제를 분리해 문서가 덜 지저분해졌다.
이쯤 되면 장항준적 사고가 필요하다.
오류가 났다?
오히려 좋아.
트러블슈팅 문서 하나 추가.
회고 소재 하나 추가.
블로그 글감 하나 추가.
개발자는 울고 있지만 콘텐츠는 풍년이다.

특히 WPF 프로젝트에서는
XAML 바인딩 문제와 ViewModel 상태 문제가 자주 얽힌다.
여기에 dotnet 환경 문제까지 섞이면
나중에 다시 추적하기가 어렵다.
이 구조는 그 맥락을 분리해서 남기는 데 도움이 됐다.
오늘 빌드 많이 볼 거야.
오늘 로그 많이 쌓일 거야.
스트롱 스트롱.

10. 한계와 개선할 점
물론 이 구조가 완벽한 것은 아니다.
raw-events는 계속 쌓이고,
중복 제거도 아직 완전하지 않고,
Bash matcher에 의존하고,
키워드 기반 분류라 놓치는 것도 있다.
그러니까 이 자동화는 완성형이라기보다
“일단 내가 안 까먹기 위한 1차 방어막”에 가깝다.

첫 번째 한계는 raw-events가 append-only라는 점이다.
후보 이벤트가 계속 쌓이기 때문에
오래 사용하면 파일이 커질 수 있다.
주기적으로 reviewed 상태를 관리하거나
오래된 이벤트를 아카이빙하는 방식이 필요하다.
두 번째는 hook 스크립트 자체에 완전한 중복 제거가 없다는 점이다.
event_hash는 만들지만,
같은 이벤트가 이미 있는지 확인하고 append를 막는 로직은 없다.
최종 문서 중복 방지는 AGENTS.md 규칙에 의존한다.
세 번째는 Bash matcher에 의존한다는 점이다.
설정이 matcher = "^Bash$"로 되어 있기 때문에,
도구 이름이 다르게 들어오는 환경에서는 hook이 실행되지 않을 수 있다.
네 번째는 키워드 기반 분류의 한계다.
ViewModel, XAML, Mapper, Renderer, API response 같은 키워드를 기준으로
code/feature 문제를 분류한다.
그런데 실제 기능 버그가 이런 키워드 없이 발생하면
unclassified로 남을 수 있다.
다섯 번째는 절대 경로 의존성이다.
실제 설정에는 로컬 Windows 경로가 들어가 있었다.
다른 PC에서 그대로 재사용하려면
<PROJECT_ROOT> 같은 방식으로 경로를 일반화하거나
환경변수를 쓰는 편이 좋다.
마지막으로 공개할 때 민감 정보 문제가 있다.
raw log에는 command, cwd, response excerpt, session_id, turn_id가 들어간다.
여기에 로컬 경로, 내부 프로젝트명, API URL, 토큰 같은 정보가 섞일 수 있다.
블로그에 raw log를 그대로 붙이는 것은 피해야 한다.
오탐 가능성?
또각또각.
중복 로그?
또각또각.
민감 정보 마스킹?
이건 못 걷어차고 제대로 해야 함.

11. 공개 전에 익명화해야 할 것
그리고 블로그에 올릴 때 제일 중요한 것.
절대 raw log를 그대로 올리면 안 된다.
로컬 경로, 프로젝트 키, session id, API URL이
생각보다 너무 자연스럽게 숨어 있다.
얘네는 숨어 있는 게 아니라
내 블로그 공개 처형 버튼 위에 앉아 있는 수준이다.

블로그에 이 내용을 정리할 때는 아래 항목을 꼭 확인해야 한다.
- 로컬 경로
- 사용자 이름
- Jira 키
- 내부 프로젝트명
- session_id / turn_id
- API URL
- 사내 도메인
- 팀원 이름
- command excerpt에 포함된 민감 정보
특히 command excerpt는 조심해야 한다.
겉으로는 단순한 로그처럼 보여도,
API 요청 URL이나 파일 경로, 내부 브랜치명, 프로젝트 키가 들어갈 수 있다.
그래서 블로그에는 실제 raw log 전체를 넣기보다,
구조를 설명하는 짧은 예시만 익명화해서 넣는 것이 안전하다.
추천 방식은 이렇다.
실제 경로:
C:/Users/사용자명/Desktop/프로젝트명/.codex/hooks/log_trouble_event.py
블로그용 경로:
<PROJECT_ROOT>/.codex/hooks/log_trouble_event.py
실제 Jira 키:
S14P31S306-000
블로그용 표현:
<PROJECT_KEY>-000
실제 session_id:
절대 공개하지 않기
블로그용 표현:
<SESSION_ID>
로그는 소중하다.
하지만 공개 블로그에 올릴 때는
소중한 만큼 잘 가려야 한다.
12. 마무리
이 자동화의 핵심은
모든 것을 자동으로 기록하는 것이 아니었다.
기록할 수 있는 근거를 잃지 않게 만드는 것이었다.
트러블슈팅 문서는 개발이 끝난 뒤 따로 쓰려고 하면
놓치는 부분이 많다.
어떤 명령에서 실패했는지,
왜 그 판단을 했는지,
어떤 가정이 틀렸는지 기억이 흐려진다.
그래서 작업 흐름 안에 evidence 수집을 넣어두는 방식이 효과적이었다.
Codex hook은 실패 명령과 응답을 raw evidence로 남기고,
AGENTS.md는 마지막에 기록 여부를 판단하는 기준이 된다.
그리고 HR-troubleshooting.md에는 실제로 해결된 문제만 정리한다.
앞으로 개선한다면 세 가지를 더 해보고 싶다.
첫째, raw event 중복 제거를 hook 단계에서 더 강화하는 것.
둘째, command나 response excerpt에 포함된 민감 정보를 자동 마스킹하는 것.
셋째, raw event를 바탕으로 트러블슈팅 초안을 자동 요약하되,
최종 기록은 여전히 사람이 확인하는 방식으로 유지하는 것.
결국 좋은 트러블슈팅 자동화는
“문서를 대신 써주는 도구”라기보다,
“나중에 제대로 쓸 수 있도록 맥락을 보존하는 장치”에 가깝다고 느꼈다.
오류는 지나가도,
로그는 남는다.
그리고 그 로그는
미래의 나를 살린다.
가끔은 블로그 글감도 살린다.

미래의 나야, 이번엔 내가 좀 했다.
'SSAFY 14기 > 자율프로젝트' 카테고리의 다른 글
| n8n + Gemini API로 GitLab MR 자동 리뷰봇 만들기: 503 high demand 에러를 시간대별 분기로 우회한 기록 (1) | 2026.04.28 |
|---|---|
| 자율프로젝트인데요? (0) | 2026.04.27 |