ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AI가 한 일을 어떻게 믿나 - PostToolUse 훅으로 만드는 Audit 시스템
    IT 2026. 3. 13. 21:00

    AI가 코드를 짰다. 근데 제대로 됐나?

    AI 코딩 에이전트를 쓰다 보면 불안한 순간이 온다. 에이전트가 파일 10개를 수정하고 명령어 5개를 실행했다. 겉보기엔 잘 된 것 같다. 빌드도 통과했다.

    그런데 이런 의문이 든다:

    • 어떤 파일이 어떻게 바뀐 거지?
    • 테스트 커버리지가 떨어진 건 아닌가?
    • 우리 팀 컨벤션을 지켰나?
    • 만약 나중에 문제가 생기면 AI가 뭘 했는지 추적할 수 있나?

    이 불안을 해소하는 것이 Audit(감사) 시스템이다. 그리고 AI 코딩 에이전트에서 이것을 구현하는 핵심 도구가 PostToolUse 훅이다.


    PostToolUse 훅이란

    Hook은 AI 에이전트가 특정 행동을 할 때 자동으로 실행되는 스크립트다. 이전 포스팅에서 Hook의 종류를 소개했는데, 그 중 PostToolUse는 도구 실행이 성공한 직후에 발화한다.

    Hook 발화 시점 주요 용도
    PreToolUse 실행 위험 명령 차단, 사전 검증
    PostToolUse 실행 결과 기록, 품질 검증, Audit
    PostToolUseFailure 실행 실패 시 오류 로깅, 알림

    PostToolUse가 Audit에 특히 적합한 이유가 있다.

    "실행 후 발화하기 때문에 '실제로 일어난 사실'을 기록하는 데 적합하다."

    그리고 한 가지 역설적인 특성이 있다. 실행을 되돌릴 수 없다는 것. 이 제약이 오히려 신뢰성을 만든다. PostToolUse가 기록한 것은 "실제로 일어났다"는 사실이 보장된다. 나중에 누군가 "그때 AI가 뭘 했는지" 물으면, 그 기록이 반박할 수 없는 증거가 된다.


    Audit의 두 가지 레이어

    PostToolUse로 구현하는 Audit는 두 개의 레이어로 나뉜다. 많은 팀이 첫 번째 레이어만 하고 멈추는데, 진짜 가치는 두 번째에 있다.

    레이어 1 - Observability: "무슨 일이 있었나"

    에이전트가 실행한 모든 행동을 기록하는 것이다.

    • 어떤 도구를 썼는가 (Write, Bash, Read...)
    • 대상이 무엇이었는가 (파일 경로, 실행 명령어)
    • 언제 일어났는가 (타임스탬프)
    • 어느 세션에서 일어났는가 (세션 ID)

    이것을 JSONL(JSON Lines) 포맷으로 쌓는다. JSONL은 한 줄이 하나의 JSON 레코드인 포맷이다. 대용량 로그에 적합하고, 스트리밍으로 읽을 수 있다.

    {"ts": "2026-03-12T14:20:00Z", "session": "abc-123", "tool": "Write", "target": "src/auth.py", "lines_changed": 42}
    {"ts": "2026-03-12T14:20:05Z", "session": "abc-123", "tool": "Bash", "command": "pytest tests/", "exit_code": 0}
    {"ts": "2026-03-12T14:20:08Z", "session": "abc-123", "tool": "Write", "target": "src/utils.py", "lines_changed": 7}

    이 로그를 HTTP 웹훅으로 팀 단위 중앙 서버에 보낼 수도 있다. Claude Code는 PostToolUse에서 HTTP 훅 타입을 지원하기 때문에, 각자의 로컬 에이전트가 하는 일을 팀 전체가 실시간으로 모니터링하는 대시보드를 만들 수 있다.

    {
      "hooks": {
        "PostToolUse": [
          {
            "type": "http",
            "url": "https://our-team-dashboard.internal/agent-audit",
            "matcher": "Write|Bash"
          }
        ]
      }
    }

    레이어 2 - Compliance Check: "의도한 대로 됐나"

    Observability가 "사실 기록"이라면, Compliance Check는 "기준 대비 판정"이다.

    AI가 파일을 수정한 직후, 자동으로 이런 것들을 검사한다:

    • 테스트 커버리지: 커버리지가 기준(예: 80%) 아래로 떨어졌는가
    • 코드 복잡도: 함수 복잡도가 임계값을 넘었는가 (Cyclomatic Complexity)
    • 컨벤션 준수: 린터 룰을 위반했는가, 파일명 규칙을 지켰는가
    • SLA: 응답 시간, 빌드 시간 등 성능 지표가 기준 이내인가

    중요한 것이 있다. 판정 결과 자체도 로그로 남겨야 한다.

    {"ts": "2026-03-12T14:20:09Z", "session": "abc-123", "tool": "Write", "target": "src/auth.py",
     "compliance": {
       "coverage": {"value": 76.3, "threshold": 80, "pass": false},
       "complexity": {"value": 8, "threshold": 10, "pass": true},
       "lint": {"violations": 0, "pass": true}
     },
     "verdict": "WARN"
    }

    이 레코드가 있어야 나중에 "그때 커버리지가 떨어졌는데 왜 아무도 몰랐지?"라는 질문에 답할 수 있다. 로그 + 판정 결과 = 완전한 audit trail.


    실제 구현 예시

    Claude Code 기준으로 PostToolUse 훅에 audit 스크립트를 연결하는 방법이다.

    .claude/hooks.json 설정

    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Write",
            "command": "bash .claude/hooks/post-write-audit.sh"
          },
          {
            "matcher": "Bash",
            "command": "bash .claude/hooks/post-bash-audit.sh"
          }
        ]
      }
    }

    .claude/hooks/post-write-audit.sh

    #!/bin/bash
    # PostToolUse: Write 이후 실행
    # 환경변수로 Claude Code가 컨텍스트를 전달함
    FILE="$CLAUDE_TOOL_INPUT_PATH"
    SESSION="$CLAUDE_SESSION_ID"
    TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    
    # 1. Observability 로그
    echo "{\"ts\":\"$TS\",\"session\":\"$SESSION\",\"tool\":\"Write\",\"target\":\"$FILE\"}" \
      >> ~/.claude/audit.jsonl
    
    # 2. Compliance Check - 린터
    LINT_RESULT=$(eslint "$FILE" --format json 2>/dev/null | jq '.[0].errorCount // 0')
    LINT_PASS=$([ "$LINT_RESULT" -eq 0 ] && echo true || echo false)
    
    # 3. 판정 결과도 로그에 기록
    echo "{\"ts\":\"$TS\",\"session\":\"$SESSION\",\"compliance\":{\"lint\":{\"violations\":$LINT_RESULT,\"pass\":$LINT_PASS}}}" \
      >> ~/.claude/audit.jsonl
    
    # 4. 위반 시 LLM 컨텍스트에 주입 (AI에게 알림)
    if [ "$LINT_PASS" = "false" ]; then
      echo "{\"contextModification\": \"WARNING: $FILE has $LINT_RESULT lint violations. Please fix before proceeding.\"}"
    fi

    마지막 줄이 중요하다. 린터 위반이 있으면 단순히 로그만 쌓는 게 아니라 AI에게도 알린다. contextModification을 반환하면 AI의 다음 응답 컨텍스트에 이 메시지가 주입되어, AI가 스스로 수정하도록 유도한다.


    Audit이 SUPERVISE 루프와 연결되는 방식

    이 구조가 Harness Engineering의 어디에 위치하는지 짚고 가자.

    Harness Engineering에서 개발자가 하는 일은 세 단계다: PREPARE(환경 설계) → SUPERVISE(루프 감시) → IMPROVE(개선).

    PostToolUse Audit은 SUPERVISE 단계의 자동화다.

    전통적 SUPERVISE PostToolUse Audit
    개발자가 AI의 변경을 직접 보고 판단 훅이 자동으로 기준 대비 판정
    매번 수동 검토 필요 기준 위반 시에만 알림
    기록이 남지 않음 JSONL로 모든 판정이 기록됨
    팀 규모에 제한됨 HTTP 훅으로 팀 전체 중앙 수집

    여기서 핵심 관점 하나를 다시 강조한다:

    "진짜 audit의 본질은 '기준 대비 현실의 비교'다."
    로그는 수단이고, 메트릭 충족 여부 판정이 목적에 더 가깝다.

    단순히 "AI가 이 파일을 수정했다"는 것을 아는 것(Observability)으로는 부족하다. "AI가 이 파일을 수정했고, 그 결과가 우리 기준을 충족했는가"(Compliance)까지 자동으로 판정해야 진짜 Audit이다.


    어디서부터 시작할까

    Audit 시스템을 한 번에 완성하려 하면 복잡해진다. 단계별로 쌓는 것을 추천한다.

    1. 로깅부터 (Observability)
      PostToolUse에 JSONL 로그만 쌓는 스크립트를 붙인다. 내용을 보지 않아도 된다. 일단 쌓기 시작하는 것이 중요하다.
    2. 가장 중요한 기준 하나 (Compliance)
      팀에서 가장 자주 위반되는 규칙 하나만 골라서 자동 검사를 붙인다. 린터, 파일명 규칙, 커버리지 중 하나.
    3. 컨텍스트 주입으로 AI 셀프 수정 유도
      위반이 감지되면 contextModification으로 AI에게 알린다. AI가 스스로 고치는 루프가 만들어진다.
    4. 팀 대시보드 (HTTP 훅)
      팀이 충분히 커지면 HTTP 훅으로 중앙 수집 시스템을 붙인다.

    마치며

    AI 에이전트를 믿는 방법은 두 가지다. 첫 번째는 AI를 무조건 믿는 것. 두 번째는 AI가 한 일을 자동으로 검증하는 시스템을 만드는 것.

    PostToolUse 훅 + JSONL 로그 + Compliance Check의 조합이 바로 그 두 번째 방법이다. AI를 믿는 것이 아니라, 기준이 통과됐다는 사실을 믿는 것. Proof of Work와 같은 철학이다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.