퍼미션 설정 최적화: allow·deny·ask

퍼미션은 Claude Code가 어떤 조작을 자동 실행하고 어떤 조작을 확인·차단할지 정하는 설정입니다. allow·deny·ask 문법과 우선순위, defaultMode 비교, 복붙용 추천 설정, 실제 검증에서 드러난 주의점을 정리합니다.

25분 갱신

Claude Code를 처음 쓰면 권한 확인 창이 너무 자주 떠서 지치고, 익숙해지면 확인 없이 다 허용하고 싶어집니다. 둘 다 정답이 아닙니다. 퍼미션은 "무엇을 묻지 않고 실행하고, 무엇을 반드시 묻고, 무엇을 절대 못 하게 할지"를 파일에 적어 두는 설정입니다. 잘 짜 두면 하루 종일 확인 창 서너 번으로 끝나면서도 .env나 운영 DB는 어떤 경우에도 건드리지 못합니다.

이 글은 앞 글의 템플릿에 적어 둔 "주의" 항목을 CLAUDE.md 밖으로 꺼내 강제하는 방법입니다. 규칙 문법, 우선순위, 모드 비교, 복붙용 설정, 그리고 우리가 실제로 검증하며 만난 함정을 다룹니다. 다음 글 「CLAUDE.md @참조로 다른 파일 불러오기」로 이어집니다.

핵심 요약

  • 규칙은 도구 또는 도구(지정자) 형식이며 deny, ask, allow 순서로 첫 일치가 결정합니다. 더 구체적인 규칙이라고 해서 순서를 바꾸지 못합니다.
  • Bash 규칙의 *는 어디든 올 수 있고, * 앞의 공백이 단어 경계를 만듭니다. Bash(npm:*)는 Bash(npm *)와 같습니다.
  • Read·Edit 규칙은 gitignore 문법이며 //(절대), ~/(홈), /(설정 파일 기준), 그냥 경로(현재 디렉터리) 네 가지 앵커가 있습니다.
  • deny는 어느 층에 있든 다른 층이 뒤집지 못합니다. --dangerously-skip-permissions도 deny를 무시하지 않습니다.
  • Read deny는 Claude의 파일 도구와 Bash의 cat·head·tail·sed에 적용되지만 Python·Node 스크립트의 간접 읽기는 막지 못합니다. 그건 샌드박스의 몫입니다.

퍼미션은 어떤 도구를 제어하나

Claude Code의 도구는 세 부류로 나뉩니다.

부류 예 기본 동작
읽기 전용 Read, Grep, Glob 작업 디렉터리 안에서는 묻지 않음
Bash 명령 셸 실행 기본 제공 읽기 전용 명령(ls, cat, pwd, grep, git status 등) 외에는 물음
파일 수정 Edit, Write 물음

여기에 WebFetch(도메인), MCP 도구(mcp__서버__도구), 서브에이전트(Agent(이름)), Skill(Skill(이름))이 같은 문법으로 제어됩니다. 퍼미션은 모델이 아니라 Claude Code 클라이언트가 적용합니다. CLAUDE.md에 "하지 마라"고 적는 것과 근본적으로 다른 층입니다.

allow·deny·ask 세 규칙은 어떻게 다른가

  • allow: 묻지 않고 실행합니다.
  • ask: 매번 확인 창을 띄웁니다.
  • deny: 실행하지 못하게 막습니다. 도구 이름만 쓰면(Bash) 그 도구가 Claude의 컨텍스트에서 아예 사라지고, 지정자를 붙이면(Bash(rm *)) 도구는 남고 일치하는 호출만 막힙니다.

우선순위는 Deny > Ask > Allow

규칙은 deny, ask, allow 순서로 평가되고 첫 일치가 결과입니다. Bash(aws *)를 deny에 두면 Bash(aws s3 ls)를 allow에 넣어도 막힙니다. deny는 예외를 허용하지 않습니다. ask와 allow 사이도 같아서, Bash(git *)가 ask에 있으면 Bash(git status)를 allow에 넣어도 확인 창이 뜹니다. "넓게 막고 좁게 열기"는 되지 않으므로, 좁게 막고 넓게 여는 쪽으로 설계합니다.

Bash 패턴은 어떻게 쓰나

규칙 일치
Bash(pnpm test) 정확히 pnpm test
Bash(pnpm test *) pnpm test로 시작하는 명령
Bash(pnpm *) pnpm 로 시작하는 모든 명령
Bash(* --version) --version으로 끝나는 명령
Bash(git * main) git checkout main, git log main

세 가지를 기억합니다.

  1. * 앞의 공백이 단어 경계입니다. Bash(ls *)는 ls -la에 맞고 lsof에는 안 맞습니다. Bash(ls*)는 둘 다 맞습니다.
  2. 복합 명령은 쪼개서 각각 검사합니다. &&, ||, ;, |, &, 줄바꿈이 구분자이며 Bash(pnpm test *)가 있어도 pnpm test && rm -rf dist는 rm 부분에서 다시 묻습니다.
  3. timeout, time, nice, nohup, stdbuf 같은 래퍼는 벗겨진 뒤 비교됩니다. 반면 npx, docker exec, mise exec 같은 러너는 벗겨지지 않으므로 Bash(npx *)는 npx 뒤의 무엇이든 허용하는 위험한 규칙이 됩니다. 러너와 내부 명령을 함께 적습니다(Bash(npx prettier *)).

인수를 제한하려는 패턴은 취약합니다. Bash(curl https://github.com/ *)는 옵션 순서, 리다이렉트, 변수로 쉽게 우회됩니다. URL을 제한하려면 curl·wget을 deny하고 WebFetch(domain:github.com)을 allow하는 쪽이 안전합니다.

Read·Edit 경로는 어떻게 쓰나

Read와 Edit 규칙은 gitignore 문법을 따르며 앵커가 네 가지입니다.

패턴 기준 예
//경로 파일 시스템 루트 Read(//Users/me/secrets/**)
~/경로 홈 디렉터리 Read(~/.zshrc)
/경로 그 규칙이 적힌 설정 파일의 위치 프로젝트 설정의 Edit(/src/**)는 <프로젝트>/src/**
경로 또는 ./경로 현재 디렉터리 Read(.env)

/Users/me/file처럼 슬래시 하나로 시작하면 절대 경로가 아니라는 점이 가장 흔한 실수입니다. 사용자 설정(~/.claude/settings.json)에 Read(/secrets/**)를 적으면 ~/.claude/secrets/**가 막힐 뿐 프로젝트의 secrets 폴더는 열려 있습니다. 모든 프로젝트에서 통하게 하려면 // 또는 ~/로 씁니다.

파일명만 쓰면 어느 깊이에서든 맞습니다. Read(.env)와 Read(**/.env)는 같고, 현재 디렉터리 아래의 모든 .env를 막습니다. 상위 디렉터리나 다른 프로젝트까지 막으려면 Read(//**/.env)입니다. Windows에서는 C:\Users\me가 /c/Users/me로 정규화되므로 //c/**/.env로 씁니다.

Read deny는 같은 경로의 Edit도 막습니다(v2.1.208 이상). Write와 NotebookEdit은 포함되지 않으므로 절대 바꾸면 안 되는 경로에는 Edit deny를 따로 둡니다. 심볼릭 링크는 링크와 대상 둘 다 검사하며, allow는 둘 다 맞아야 통과하고 deny는 하나만 맞아도 막힙니다.

defaultMode 세 가지 비교

공식 문서의 모드는 default, acceptEdits, plan, auto, dontAsk, bypassPermissions 여섯입니다. 실무에서 고르게 되는 셋을 비교합니다.

모드 동작 언제
default 도구를 처음 쓸 때마다 물음. 화면에는 Manual로 표시 낯선 프로젝트, 결제·DB 작업
acceptEdits 작업 디렉터리 안의 파일 편집과 mkdir·mv·cp 같은 파일 명령은 자동 승인 내 코드 안에서 반복 편집하는 날
plan 읽기와 읽기 전용 셸만. 소스를 편집하지 않음 설계·조사, 처음 보는 저장소

bypassPermissions는 명시적 ask 규칙과 rm -rf /·rm -rf ~ 같은 회로 차단기를 제외한 모든 확인을 건너뜁니다. .git, .claude, .vscode 쓰기까지 통과하므로 컨테이너나 VM처럼 망가져도 되는 환경에서만 씁니다. 조직에서 막으려면 permissions.disableBypassPermissionsMode: "disable"을 관리 정책에 둡니다.

복붙해서 쓰는 균형형 설정

초급 4주차 "키 3규칙"과 우리 레포 규칙을 파일로 옮긴 형태입니다. .claude/settings.json에 둡니다.

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": [
      "Bash(pnpm install)",
      "Bash(pnpm dev)",
      "Bash(pnpm build)",
      "Bash(pnpm test *)",
      "Bash(pnpm lint *)",
      "Bash(git add *)",
      "Bash(git commit *)",
      "Bash(git checkout *)",
      "Bash(git diff *)",
      "Bash(* --version)",
      "WebFetch(domain:code.claude.com)"
    ],
    "ask": [
      "Bash(rm *)",
      "Bash(git push *)",
      "Bash(git reset *)",
      "Edit(/supabase/schema.sql)"
    ],
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(//**/.pem)",
      "Bash(curl *)",
      "Bash(wget *)",
      "Bash(git push * main)",
      "Bash(git push --force *)"
    ]
  }
}

설계 의도는 이렇습니다. 매일 수십 번 도는 빌드·테스트·커밋은 allow, 되돌리기 어려운 삭제·푸시·스키마 편집은 ask, 비밀값 읽기와 main 강제 푸시는 deny입니다. deny의 Bash(git push * main)은 git push origin main을 막고, ask의 Bash(git push *)는 다른 브랜치 푸시를 확인하게 합니다.

프로젝트별로 더하는 규칙

  • 결제 코드가 있는 레포: "ask": ["Edit(/app/api/orders/**)", "Edit(/lib/payments/**)"]. 우리 사이트 레포는 결제 코드 수정 시 교차 리뷰를 규칙으로 두므로 편집 자체를 한 번 더 묻게 합니다.
  • 운영 DB에 붙는 스크립트: "deny": ["Bash(* --env production *)", "Bash(psql *)"]. "운영 DB는 성역"을 파일로 옮긴 것입니다.
  • 비개발 프로젝트(문서·데이터 정리): "deny": ["Edit(/data/원본/**)"], "allow": ["Bash(python3 scripts/*)"].
  • 브랜치 규칙이 있는 팀: "deny": ["Bash(git push * main)", "Bash(git merge main *)"].
  • MCP를 쓰는 팀: "allow": ["mcp__github__get_*"], "deny": ["mcp__filesystem"]. allow의 도구 이름 glob은 mcp__서버__ 접두사 뒤에서만 허용됩니다.

설정 파일은 어디에 두나

파일 담을 규칙
.claude/settings.json 팀 공통 allow·ask·deny. git 커밋
.claude/settings.local.json 내 기기 전용 allow. "다시 묻지 않기"가 여기 쌓임. gitignore
~/.claude/settings.json 모든 프로젝트 공통. 경로 규칙은 //나 ~/로
관리 정책 조직 강제. disableBypassPermissionsMode, 도메인 차단

프로젝트 설정의 allow 규칙과 additionalDirectories는 워크스페이스 신뢰 대화를 수락한 뒤에만 효력이 생깁니다. 클론 직후 allow가 안 먹는다면 이 대화를 아직 안 지난 것입니다. deny와 ask는 즉시 적용됩니다.

실제 검증에서 드러난 주의점

우리가 레포에 설정을 넣고 돌려 보며 확인한 것들입니다.

  1. "다시 묻지 않기"는 복합 명령을 통째로 저장하지 않습니다. git status && pnpm test를 승인하면 pnpm test 규칙만 저장되고, 한 복합 명령에서 최대 5개까지 규칙이 생깁니다.
  2. 기본 제공 읽기 전용 명령 집합은 바꿀 수 없습니다. cat이 묻지 않고 도는 것이 불편하면 ask 규칙을 추가해야 합니다.
  3. find -delete, find -exec, watch, setsid 같은 실행 래퍼는 접두사 규칙으로 자동 승인되지 않고 항상 묻습니다. 전체 문자열 정확 일치 규칙만 통합니다.
  4. cd와 git을 한 명령에 묶어 다른 디렉터리로 이동하면 그 디렉터리의 훅이 돌 수 있어 묻습니다.
  5. 훅이 allow를 돌려줘도 deny 규칙은 그 뒤에 다시 평가됩니다. 반대로 훅의 종료 코드 2 차단은 allow 규칙보다 먼저 적용됩니다. "전부 allow하고 훅으로 몇 개만 막기"가 가능한 이유입니다.
  6. "*"나 "B*" 같은 앵커 없는 allow glob은 경고와 함께 무시됩니다. allow는 항상 구체적으로 씁니다.

설정이 안 먹을 때 확인할 것

  1. /permissions로 규칙과 출처 파일을 봅니다. 다른 층의 deny나 ask가 있으면 그쪽이 이깁니다.
  2. 워크스페이스 신뢰 대화를 수락했는지 확인합니다. allow만 안 먹으면 거의 이것입니다.
  3. JSON 문법을 검증합니다. 깨진 파일은 통째로 무시됩니다.
  4. 도구 이름이 정규 이름인지 봅니다. 화면 표기와 다를 수 있으며, 모르는 이름의 deny·ask 규칙은 시작 시 경고가 뜹니다.
  5. claude --verbose로 실제 호출된 도구와 매개변수를 확인합니다.

정리

  • deny, ask, allow 순서이며 deny는 예외를 두지 못합니다. 좁게 막고 넓게 엽니다.
  • Bash는 * 앞 공백과 복합 명령 분리를, Read·Edit는 네 가지 경로 앵커를 기억합니다.
  • 매일 도는 명령은 allow, 되돌리기 어려운 것은 ask, 비밀값과 운영 자원은 deny입니다.
  • Read deny는 스크립트의 간접 읽기를 못 막습니다. 완전한 차단은 샌드박스와 함께 씁니다.

자주 묻는 질문

allow에 넣은 명령인데 왜 매번 확인 창이 뜨나요?

세 가지를 봅니다. 프로젝트 설정의 allow는 워크스페이스 신뢰를 수락해야 효력이 생깁니다. 더 넓은 ask 규칙이 같은 명령에 걸리면 allow가 있어도 묻습니다. 복합 명령이면 각 부분이 전부 allow에 맞아야 하며, 하나라도 빠지면 그 부분에서 묻습니다.

Bash(npm:*) 와 Bash(npm *) 는 뭐가 다른가요?

같습니다. :*는 끝에 오는 *의 다른 표기이며 패턴 끝에서만 인식됩니다. 권한 창의 "다시 묻지 않기"는 공백 형식으로 저장합니다. Bash(git:* push)처럼 중간에 쓰면 콜론이 문자 그대로 취급되어 아무것도 맞지 않습니다.

--dangerously-skip-permissions 를 쓰면 deny 규칙도 무시되나요?

아닙니다. 이 플래그는 확인 창을 건너뛰는 것이지 deny를 지우지 않습니다. 명시적 ask 규칙과 루트·홈 삭제 회로 차단기도 여전히 묻습니다. 조직에서 이 모드 자체를 막으려면 disableBypassPermissionsMode를 관리 정책에 둡니다.

Bash(cat *) 를 deny 했는데 Claude가 파일을 읽었습니다. 왜죠?

Bash 규칙은 Bash 도구만 제어합니다. Claude는 Read 도구로 같은 파일을 읽을 수 있습니다. 파일 자체를 막으려면 Read(경로) deny를 씁니다. 그래도 Python이나 Node 스크립트가 파일을 여는 간접 읽기는 막지 못하므로, OS 수준 차단이 필요하면 샌드박스의 파일 시스템 제한을 함께 켭니다.

팀에 클로드코드를 도입하려면 실제 업무에 맞춘 설계가 필요합니다. 무료 상담 신청 →