Cursor Hooks, 저장·커밋 전에 에이전트 가드를 걸려면

Cursor Hooks로 에이전트 루프에 가드를 걸려면, 프로젝트는 /.cursor/hooks.json, 사용자 전역은 ~/.cursor/hooks.json에 훅을 정의합니다. 공식 문서 기준 이벤트 이름은 afterFileEdit, beforeShellExecution처럼 단계별로 나뉘며, “pre-save”라는 단일 훅 이름은 없습니다.

이 글은 공식 Cursor Hooks 문서를 따라 어디에 두고, 편집 후 훅과 셸 훅을 어떻게 나누며, 실패 시 무엇을 막는지만 정리합니다. 요금·후기는 다루지 않습니다.

Hooks는 어디에 두나?

한 줄 답: 레포 공유는 .cursor/hooks.json, 내 머신 전역은 ~/.cursor/hooks.json에 둡니다.

공식 Quickstart는 두 위치를 구분합니다.

  • Project hooks: /.cursor/hooks.json — 해당 프로젝트만. 스크립트는 프로젝트 루트 기준으로 실행하므로 경로는 .cursor/hooks/format.sh처럼 씁니다.
  • User hooks: ~/.cursor/hooks.json — 모든 워크스페이스. 스크립트는 ~/.cursor/ 기준으로 ./hooks/format.sh 형태가 됩니다.

훅은 stdin/stdout으로 JSON을 주고받는 커맨드 훅(기본)과, 자연어 조건을 평가하는 프롬프트 훅이 있습니다. Agent Chat·Cmd+K용 Agent hooks, Tab 완성용 Tab hooks, 워크스페이스 오픈용 workspaceOpen이 서로 다른 표면입니다.

Cloud agent는 레포 루트 .cursor/hooks.json의 커맨드 훅을 로드합니다. ~/.cursor/hooks.json 사용자 훅은 클라우드 VM에 홈 디렉터리가 없어 적용되지 않습니다. Enterprise에서는 팀·엔터프라이즈 훅도 클라우드에서 실행될 수 있습니다.

최소 예(프로젝트):

{
  "version": 1,
  "hooks": {
    "afterFileEdit": [
      { "command": ".cursor/hooks/format.sh" }
    ],
    "beforeShellExecution": [
      {
        "command": ".cursor/hooks/approve-network.sh",
        "timeout": 30,
        "matcher": "curl|wget|nc"
      }
    ]
  }
}

스크립트는 실행 권한이 필요합니다(chmod +x). Cursor는 hooks.json 저장 시 다시 로드하며, 안 보이면 Customize의 Hooks 탭과 Hooks 출력 채널을 확인합니다.

pre-save와 shell 훅 차이는?

한 줄 답: 문서에 “pre-save” 이벤트는 없고, 편집 직후는 afterFileEdit, 셸 실행 전 차단은 beforeShellExecution을 씁니다.

검색어로 자주 나오는 “저장 전 가드”를 공식 이벤트에 대응하면 대략 다음입니다.

목적공식 훅역할
에이전트가 파일을 고친 뒤 포맷·린트afterFileEdit편집 후 관찰·후처리. stdin에 file_path, edits
위험한 셸 명령 승인/거부beforeShellExecution실행 전 permission: allow / deny / ask
MCP 도구 게이트beforeMCPExecution셸과 같은 permission 계약
민감 파일 읽기 차단beforeReadFilepermission으로 읽기 거부
Tab 완성과 Agent를 분리afterTabFileEdit / beforeTabFileReadTab 전용 표면

afterFileEdit는 포맷터를 돌리기에 적합하지만, 공식 스키마상 편집 자체를 되돌리거나 막는 출력 필드는 없습니다. 커밋·네트워크·삭제를 막으려면 beforeShellExecution(필요 시 matcher로 명령 문자열 필터) 또는 더 넓은 preToolUse를 사용합니다. 셸만 관심이면 preToolUse보다 beforeShellExecution이 문서가 권하는 좁은 선택입니다.

커맨드 훅은 JSON을 stdout으로 돌려줍니다. beforeShellExecution 예:

{
  "permission": "deny",
  "user_message": "네트워크 명령이 훅에 의해 차단되었습니다.",
  "agent_message": "curl/wget 대신 허용된 도구를 사용하세요."
}

실패 시 편집을 막나?

한 줄 답: afterFileEdit 실패로는 이미 적용된 편집을 막지 못하고, 실행 전 훅에서 permission: "deny", exit code 2, 또는 failClosed: true일 때 동작을 막습니다.

공식 exit code 동작은 다음과 같습니다.

  • exit 0: 훅 성공. permission 훅은 stdout JSON이 스키마에 맞아야 하며, 잘못된 JSON이면 해당 동작을 차단합니다.
  • exit 2: 동작을 차단(permission: "deny"와 동등).
  • 그 외 비정상 종료: 기본은 fail-open(로그만 남기고 동작 허용). 보안 훅은 정의에 "failClosed": true를 넣어 크래시·타임아웃·비정상 종료 때도 막습니다.

따라서 “포맷 스크립트가 실패하면 저장을 막는다”는 기대를 afterFileEdit에 걸면 문서와 맞지 않습니다. 편집 전 차단이 필요하면 beforeReadFile·도구 단계의 before 훅, 셸·MCP는 beforeShellExecution / beforeMCPExecution을 사용하고, 정책 강제 훅에는 failClosed: true를 검토합니다.

preToolUse의 permission: "ask"는 스키마에 있으나 현재 강제되지 않는다고 문서에 명시되어 있으므로, 승인 UI가 필요한 셸/MCP는 beforeShell/beforeMCP의 ask를 쓰는 편이 안전합니다.

마무리

저장·커밋 전 가드를 걸려면 ① 위치(.cursor vs ~/.cursor) → ② 이벤트 선택(afterFileEdit vs beforeShellExecution) → ③ 차단 계약(deny / exit 2 / failClosed) 순으로 맞춥니다. 상세 스키마와 예제는 Cursor Hooks 공식 문서를 기준으로 확인하시기 바랍니다.

출처

  • Hooks — 위치, 이벤트, exit code, failClosed, Cloud agent 범위