Claude hook 기반 daemon 개발하기
최근 claude code를 활용하여 개인 프로젝트를 열심히 돌리고 있는데, 이런 일이 있었다
- 배포하기 전에는 로컬에서 pre-deploy라는 작업을 거친 후 배포하는 플로우가 존재한다. (이것저것 테스트하는 명령어)
- 근데 클로드 이녀석이!! CLAUDE.md에 명시해놔도 냅다 무시한다!!!!!!
- 그렇다고 git push 자체를 hook으로 냅다 틀어막을 수는 없는 노릇…
시행착오
그래서 이것저것 시도해본 것들
- 앞서 언급했지만 CLAUDE.md에 명시도 했다.
→ NEVER나 MUST같은 걸 어느 정도 넣어서 시도해보긴 했지만 생각만큼 잘 되진 않았고, 더 넣자니 토큰 수 너무 까먹을 것 같고 강조/부정프롬을 너무 많이 넣으면 안좋다는 글을 어디서 본 것 같아서..
- hook으로 막지 않고 echo로 프롬프트 주입만 하기
→ 클로드가 가능하다고 알려준 방법인데, 그냥 무시하고 지 할 일을 한다;;
- pre-deploy를 실행하면 .pre-deploy-passed 파일을 생성하고, git push origin develop에 푸시할 때 hook으로 .pre-deploy-passed 파일 없으면 푸시 못하게끔 만들기
→ 근데 이녀석이 지 맘대로 .pre-deploy-passed 파일 touch해서 bypass해버린다
→ 화들짝 놀라서 인터럽트하고, 왜 그랬냐고 변명을 들어보니 변경사항이 trivial해서 그랬다고 하더라. 틀린 말은 아닌데…
아이디에이션
claude plugin 중에 이런 게 있다 (unknown mention type link_preview)
hook 내부에서 session id 기반으로 json 파일 생성하고, 이 안에 플래그를 저장하는 방식으로, 한 세션에서 이미 본 warning은 무시할 수 있다.
즉 클로드가 잠재적으로 위험성이 있는 명령어를 실행하려 하면 한 번 중단하면서 echo로 메세지를 적당히 출력. 컨텍스트를 주입하면서 에이전트가 적절한 다음 행동을 할 수 있도록 유도하는 방식이다.
좋은 아이디어같긴 한데, 몇 가지 아쉬운 점이 있었다.
- 션에서 한 번 warning 보고 나면 끝. 작업 길어지면 컨텍스트 길어지고 compact하고 할 텐데.. warning 내용도 까먹지 않을까
- 하드코딩되어 박혀 있는 rule 말고는 수정이 불가한데, 동적으로 설정할 수 있지 않을까
개발
그래서 만들었다 (unknown mention type link_preview)
Claude Code ──▶ gate.py ──▶ daemon
│
▼
매칭되는 룰 있나?
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
없음 1차 시도 2차 시도
(플래그 없음) (플래그 있음)
│ │ │
▼ ▼ ▼
통과 경고 + 차단 플래그 소비
│ → 통과
▼
에이전트 retry 핵심적인 컨셉은 매칭되는 명령어를 입력시 프롬프트 출력하면서 1회 차단 → 2회째에 실행을 허용하는 것
- 에이전트가 출력된 프롬프트 보고 자체적으로 판단이 가능하고
- 플래그는 30초 후 초기화되니 나중에 다시 실행하면 프롬프트 재주입이 가능하다
Claude Code
│
│ SessionStart
▼
init.py ──▶ daemon 살아있나?
│ │
YES NO
│ │
skip 백그라운드로 daemon 띄움
│
▼
daemon (대기)
Project A Project B
┌──────────┐ ┌──────────┐ ┌──────────┐
│ session1 │ │ session2 │ │ session3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└──────┬──────┘ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ daemon A │ │ daemon B │
│ │ │ │
│ rules: 공유 │ │ rules: 별개 │
│ flags: 세션 │ │ flags: 세션 │
│ 별 분리 │ │ 별 분리 │
└─────────────┘ └─────────────┘
같은 프로젝트 = 같은 daemon (rules 공유, flag 만 세션별 분리)
다른 프로젝트 = 별개 daemon 그럼 상태 관리는 어디에서 하느냐? 클로드 세션이 뜰 때, SessionStart 훅으로 별도의 daemon이 실행된다
- 프로젝트 경로를 해싱해서 /tmp 하위 경로 어딘가에 유닉스 소켓을 열고,
- PreToolUse 훅을 걸어서 이 유닉스 소켓으로 요청을 보내는 구조가 된다.
- 매 요청마다 session_id도 같이 보내서 플래그 상태가 세션별로 분리되게 하였다.
규칙은 아래와 같은 구조로 지정된다
{
"rules": [
{
"id": "no-force-push",
"matcher": "Bash",
"pattern": "git push.*(--force|-f)",
"message": "[clflmgr] Force push requires checking 'git status' first.\n\n- If you have not checked, please do so first.\n- If unnecessary, re-run the same command.\n\n(Auto-resets after 30s)",
"timeout": 30
}
]
}이렇게 하면 에이전트가 Bash에서 git push -f같은 명령어를 실행하려 할 때, daemon이 1회는 tool 사용을 거절하고 message에 입력된 프롬프트를 주입해준다.
그 이후 30초동안은 동일 명령어 재실행시 명령어를 허용하는 구조가 된다. daemon이 알아서 30초 후에 플래그를 리셋