로드 중

빌드 단계에서는 후크를 가드레일로 사용하여 사람의 개입 없이 작업을 허용하거나 차단했습니다(3단계: 빌드). 후크는 질문(ask)을 할 수도 있어서, 세션을 실행하는 사람이 확인할 때까지 작업을 일시 중지합니다. 승인자가 릴리스 관리자와 같은 다른 사람인 경우, 후크는 기록된 승인을 찾고 그것이 존재할 때까지 차단합니다.

이 플레이는 릴리스 게이트가 가장 명확한 사례이기 때문에 5단계: 배포에 속하지만, 후크는 배포에만 한정되지 않습니다. 후크는 Claude가 작동하는 모든 곳에서 실행됩니다. 예를 들어, 후크는 3단계: 빌드 중에 변경 티켓 없이 마이그레이션과 인프라를 수정하는 것을 차단할 수 있고, 4단계: 테스트에서 수정 작업 중 에이전트가 테스트 파일을 편집하는 것을 막을 수 있습니다.

시작하기

  • 전제 조건: 없음.
  • 인프라: 변경 프로세스에 필요한 승인 목록을 문서화한 것.

실행 방법

  1. 엔지니어링 리더십은 변경 관리 및 컴플라이언스 팀과 함께, 변경 관리 승인, 릴리스 권한 부여, 보호된 경로에 대한 수정과 같이 반드시 유지되어야 하는 사람의 승인 게이트를 나열합니다.
  2. 플랫폼 엔지니어는 각 게이트를 후크로 표현합니다. 후크는 Claude가 작동하기 전에 실행되는 스크립트로, 허용(allow), 질문(ask), 또는 차단(block)을 할 수 있습니다.
  3. 팀 후크는 Git의 .claude/settings.json에 들어가고, 협상 불가능한 후크는 플랫폼 또는 IT 관리자가 소유한 관리 설정(managed settings)에 들어가며, 이곳에서는 개별 엔지니어가 이를 끌 수 없습니다.
  4. 차단은 스스로를 설명해야 하므로, 후크가 작업을 중지할 때 그 이유와 승인 경로가 Claude의 출력에 나타납니다.

실제 모습

이 예시는 개발(dev), 스테이징(staging), 프로덕션(production)에 릴리스되는 보험사의 고객 포털입니다. 배포는 MCP 도구를 통해 실행되며, 환경마다 서버가 하나씩 있고 각 서버에는 release 도구가 있습니다.

아래 항목은 프로젝트의 .claude/settings.json에 있는 독립적인 예시입니다. 이 매처(matcher)는 모든 서버에서 release를 포착합니다. 빈 args 목록은 Claude Code가 셸을 통하지 않고 스크립트를 직접 실행하도록 합니다:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__deploy-.*__release",
        "hooks": [
          { "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/release-gate.sh",
            "args": [] }
        ]
      }
    ]
  }
}

게이트 자체, .claude/hooks/release-gate.sh:

bash
#!/bin/bash
# 릴리스 게이트. dev: 허용. staging: 확인 요청. production: 이 버전에 대한 승인된 변경 티켓이 있으면 허용, 없으면 차단.
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
version=$(jq -r '.tool_input.version' <<<"$input")
ticket=$(jq -r '.tool_input.ticket // empty' <<<"$input")

decide() {   # decide <allow|ask|deny> <reason>
  jq -n --arg decision "$1" --arg reason "$2" '{hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: $decision,
    permissionDecisionReason: $reason}}'
  exit 0
}

case "$tool" in
  mcp__deploy-dev__release)
    decide allow "dev needs no approval" ;;
  mcp__deploy-staging__release)
    [ "$GITHUB_REF" = "refs/heads/main" ] && decide allow "pipeline release from main"
    decide ask "This ships $version to staging. Approve only if you own this release." ;;
  mcp__deploy-prod__release)
    route="Open a change ticket at https://change.example.com/new for version $version, \
wait for the release manager to approve it, then call release again with that ticket number."
    [[ "$ticket" =~ ^CHG-[0-9]+$ && "$version" =~ ^[0-9a-f]{7,40}$ ]] ||
      decide deny "Production needs an approved change ticket. $route"
    if approver=$(/usr/local/bin/change-ticket show --ticket "$ticket" --format json |
        jq -er --arg v "$version" 'select(.state == "approved" and .version == $v) | .approver' 2>/dev/null); then
      decide allow "$ticket approved by $approver"
    fi
    decide deny "$ticket is not approved for version $version. $route" ;;
  *)
    decide deny "No release rule for $tool. Add one to release-gate.sh first." ;;
esac

스크립트에서 version과 ticket은 Claude가 release 도구에 전달하는 입력입니다. change-ticket은 여러분의 변경 시스템 자체의 명령줄 도구를 나타냅니다. 이 게이트는 에이전트가 읽는 기록에 쓸 방법이 없다는 것에 의존합니다.

비대화형(non-interactive) 실행에서는 아무도 ask에 응답할 수 없으므로 Claude Code는 호출을 거부합니다. 이것이 바로 게이트가 main의 파이프라인에서 스테이징을 허용하는 이유이며, 여기서는 승인된 병합(merge)이 곧 승인입니다.

이 게이트에는 한계가 있습니다. 엔지니어가 자신의 세션에서 직접 GITHUB_REF를 설정할 수 있지만, 그렇게 해도 본인이 직접 답했을 프롬프트만 건너뛰게 됩니다.

ask에 대한 이유는 사람에게 표시됩니다. deny에 대한 이유는 Claude에게 표시되므로, 차단 메시지는 Claude가 전달할 승인 경로를 명시합니다. 스테이징 결과가 먼저 나오고, 그다음 티켓이 없는 프로덕션이 나옵니다:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "This ships 4f2c9e1 to staging. Approve only if you own this release."
  }
}
json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Production needs an approved change ticket. Open a change ticket at https://change.example.com/new for version 4f2c9e1, wait for the release manager to approve it, then call release again with that ticket number."
  }
}

거버넌스 고려 사항

후크가 바로 승인 게이트입니다. 게이트 조건은 모든 사람에게 매번 적용됩니다. 허용 및 차단 결정은 타임스탬프와 함께 기록됩니다. 게이트는 또한 승인된 변경 티켓이든 릴리스 관리자의 승인이든, 무엇이 승인으로 간주되는지를 정의합니다.

규제 대상 기업을 위한 관리 설정

규제 대상 기업을 위한 관리 설정으로, 플랫폼 팀이 모바일 기기 관리(MDM) 또는 관리자 콘솔을 통해 배포합니다. 엔지니어는 그 안의 어떤 설정도 편집하거나 재정의할 수 없습니다. 아래를 참조하세요:

json
{
  "permissions": {
    "deny": [
      "Read(.env*)", "Read(./secrets/**)",
      "WebFetch", "Bash(curl *)", "Bash(wget *)"
    ],
    "allow": [
      "Bash(git *)", "Bash(make build)",
      "Bash(make test)", "Bash(make lint)"
    ],
    "disableBypassPermissionsMode": "disable"
  },
  "allowManagedPermissionRulesOnly": true,
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "network": { "allowedDomains": ["git.internal.example.com", "registry.npmjs.org"] },
    "credentials": {
      "files": [
        { "path": "~/.ssh", "mode": "deny" },
        { "path": "~/.aws/credentials", "mode": "deny" }
      ],
      "envVars": [ { "name": "GITHUB_TOKEN", "mode": "deny" } ]
    }
  },
  "allowManagedHooksOnly": true,
  "disableSideloadFlags": true,
  "allowManagedMcpServersOnly": true,
  "strictKnownMarketplaces": [
    { "source": "github", "repo": "example-corp/approved-plugins" }
  ],
  "requiredMinimumVersion": "2.1.193"
}

이 설정이 통제 측면에서 하는 일:

  • permissions.deny는 비밀 정보가 에이전트의 컨텍스트에 들어가지 않도록 하고 도구를 통한 임의의 네트워크 아웃바운드(egress)를 차단합니다. permissions.allow는 안전한 내부 루프를 사전 승인하여 거부 목록이 프롬프트 피로(prompt fatigue)로 이어지지 않도록 합니다.
  • disableBypassPermissionsMode와 allowManagedPermissionRulesOnly를 함께 사용하면 어떤 엔지니어, 프로젝트 파일, 또는 명령줄 플래그도 규칙을 확장할 수 없습니다.
  • sandbox는 권한이 다룰 수 없는 부분을 다룹니다. WebFetch에 대한 도구 수준의 거부는 셸 명령이 네트워크에 접근하는 것을 막지 못하지만, OS 수준의 도메인 허용 목록(allowlist)은 아웃바운드를 완전히 차단하므로, 이 둘은 서로 다른 계층에서 하나의 목표를 강제합니다. failIfUnavailable과 allowUnsandboxedCommands는 샌드박스를 전제 조건으로 만들어, 샌드박스가 초기화될 수 없을 때 Claude Code가 시작을 거부하고, 샌드박스 내부에서 실패한 명령은 샌드박스 외부에서 재시도할 수 없게 합니다.
  • credentials는 거부 규칙이 놓치는 경우를 처리합니다. permissions.deny는 Claude의 파일 도구를 통제하지만, 샌드박스화된 셸 명령은 기본적으로 여전히 ~/.ssh나 ~/.aws/credentials를 읽을 수 있습니다. 이 블록은 그러한 읽기를 거부하고 나열된 비밀 정보를 샌드박스화된 명령의 환경에서 제거합니다.
  • allowManagedHooksOnly는 관리 설정에 정의된 후크만 실행됨을 의미합니다. 사용자, 프로젝트, 로컬 설정의 후크는 위에서 본 독립적인 .claude/settings.json 예시를 포함하여 차단됩니다. 이 플레이의 승인 게이트가 계속 적용되도록 하려면, 관리 파일 자체의 hooks 블록에 이를 정의하세요.
  • disableSideloadFlags와 strictKnownMarketplaces는 엔지니어의 머신에 있는 모든 스킬, 에이전트, 후크, 또는 MCP 서버가 홈 디렉터리가 아니라 조직이 승인한 플러그인 마켓플레이스를 통해 들어왔음을 의미합니다. 마켓플레이스 허용 목록은 설치할 수 있는 항목을 통제하며, 단일 실행을 위해 플러그인, 에이전트, 또는 MCP 구성을 사이드로드하려는 플래그는 시작 시 거부됩니다.
  • allowManagedMcpServersOnly는 에이전트의 도구 범위를 플랫폼 팀이 소유한 허용 목록으로 만듭니다.
  • requiredMinimumVersion은 승인된 최저 버전보다 낮은 버전에서는 시작을 거부하므로, 통제는 조직이 실제로 평가한 빌드에 의해 강제됩니다.

이 예시를 여러분의 환경에 맞게 커스터마이즈하기 위한 출발점으로 삼으세요. 각 거부 규칙은 일부 기능을 제거하며, 적절한 균형은 저장소의 데이터 분류에 따라 달라집니다. 설정 참조에는 관리 전용 키를 포함한 모든 키가 문서화되어 있습니다.

측정 방법

후크 자체에 대해서는:

  • 선행 지표: 각 승인 게이트에서 대기하는 데 소요된 시간. 모든 후크 결정은 타임스탬프와 허용 또는 차단 판정과 함께 OpenTelemetry 내보내기(export)에 기록되므로, 게이트별 대기 시간을 확인할 수 있습니다.
  • 후행 지표: 후크 도입 전후로 프로덕션에 도달한 게이트 위반 건수, 인시던트 트래커 기준.