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 |
세 가지를 기억합니다.
*앞의 공백이 단어 경계입니다.Bash(ls *)는ls -la에 맞고lsof에는 안 맞습니다.Bash(ls*)는 둘 다 맞습니다.- 복합 명령은 쪼개서 각각 검사합니다.
&&,||,;,|,&, 줄바꿈이 구분자이며Bash(pnpm test *)가 있어도pnpm test && rm -rf dist는rm부분에서 다시 묻습니다. 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는 즉시 적용됩니다.
실제 검증에서 드러난 주의점
우리가 레포에 설정을 넣고 돌려 보며 확인한 것들입니다.
- "다시 묻지 않기"는 복합 명령을 통째로 저장하지 않습니다.
git status && pnpm test를 승인하면pnpm test규칙만 저장되고, 한 복합 명령에서 최대 5개까지 규칙이 생깁니다. - 기본 제공 읽기 전용 명령 집합은 바꿀 수 없습니다.
cat이 묻지 않고 도는 것이 불편하면ask규칙을 추가해야 합니다. find -delete,find -exec,watch,setsid같은 실행 래퍼는 접두사 규칙으로 자동 승인되지 않고 항상 묻습니다. 전체 문자열 정확 일치 규칙만 통합니다.cd와git을 한 명령에 묶어 다른 디렉터리로 이동하면 그 디렉터리의 훅이 돌 수 있어 묻습니다.- 훅이
allow를 돌려줘도 deny 규칙은 그 뒤에 다시 평가됩니다. 반대로 훅의 종료 코드 2 차단은 allow 규칙보다 먼저 적용됩니다. "전부 allow하고 훅으로 몇 개만 막기"가 가능한 이유입니다. "*"나"B*"같은 앵커 없는 allow glob은 경고와 함께 무시됩니다. allow는 항상 구체적으로 씁니다.
설정이 안 먹을 때 확인할 것
/permissions로 규칙과 출처 파일을 봅니다. 다른 층의 deny나 ask가 있으면 그쪽이 이깁니다.- 워크스페이스 신뢰 대화를 수락했는지 확인합니다. allow만 안 먹으면 거의 이것입니다.
- JSON 문법을 검증합니다. 깨진 파일은 통째로 무시됩니다.
- 도구 이름이 정규 이름인지 봅니다. 화면 표기와 다를 수 있으며, 모르는 이름의 deny·ask 규칙은 시작 시 경고가 뜹니다.
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 수준 차단이 필요하면 샌드박스의 파일 시스템 제한을 함께 켭니다.