
Claude Code의 settings.json을 정리했더니 허가 규칙의 90%를 삭제할 수 있었다
요약
Claude Code의 설정 파일인 settings.json의 구조와 권한 관리 메커니즘을 분석합니다. 불필요하게 쌓인 허가 규칙을 정리하는 방법과 보안을 위한 레이어별 설정 전략을 다룹니다.
핵심 포인트
- settings.local.json은 사용자의 허가 클릭 이력이 누적된 파일임
- 설정은 프로젝트 로컬, 프로젝트 공유, 글로벌, 글로벌 로컬의 4개 레이어로 구성됨
- deny 규칙은 모든 레이어에서 최우선 순위를 가지므로 보안 설정에 활용 권장
- 정기적인 규칙 정리를 통해 불필요한 권한 남용과 설정 부패를 방지해야 함
수개월간 사용해 온 Claude Code의 프로젝트 설정을 열어보니, settings.local.json에 허가 규칙이 45개나 쌓여 있었다. 특정 논문 PDF를 다운로드하는 curl 명령의 완전 일치 허가. 더 이상 존재하지 않는 프로세스 ID(PID)에 대한 kill. 따옴표가 누락되어 깨진 패턴. 기억에 없는 규칙들만 나열되어 있었다.
정리해 보니 45개 중 32개는 삭제해도 아무런 문제가 없는 쓰레기였다. 덤으로 설정 파일의 계층 구조와 자동 허가 메커니즘, deny의 허점까지 한 차례 정리했더니 권한 관련 해상도가 상당히 높아졌기에, 알게 된 사실들을 남겨둔다.
전제 조건이 하나 있다. settings.json은 Claude Code의 동작을 결정하는 설정 파일로, 권한 관련 사항은 allow, deny, ask의 세 가지 리스트에 Bash(git add:*)와 같은 패턴을 나열하여 작성한다. allow는 확인 없이 실행, deny는 실행하지 않음, 둘 다 아니면 사용자에게 확인을 요청하는 판정이 된다. 기본적인 작성법은 다음 기사에 잘 정리되어 있으므로, 본고에서는 실제로 운용하며 처음으로 보이게 되는 측면을 다룬다.
먼저 결론을 한 장으로 요약한다.
| 알게 된 것 | 대처 |
|---|---|
| settings.local.json은 허가 프롬프트의 클릭 이력. 방치하면 부패함 | 정기적으로 정리하여 삭제 |
Bash(python3 *)는 임의 코드 실행의 전면 허가와 동일 | 사용할 경우 그 대가를 자각할 것 |
| deny의 .env만으로는 기밀 파일을 보호할 수 없음 | .ssh, .aws, *.key까지 범위를 넓힐 것 |
settings는 4개의 레이어로 구성되어 있다
Claude Code의 권한은 하나의 파일이 아니라, 4개의 레이어가 겹쳐져 결정된다.
| 파일 | 스코프 | git·공유 |
|---|---|---|
<project>/.claude/settings.local.json | 해당 프로젝트 × 해당 머신 | 공유하지 않음. 자동 생성됨 |
<project>/.claude/settings.json | 해당 프로젝트 | git 관리하여 팀에서 공유 |
~/.claude/settings.json | 모든 프로젝트 | 개인의 글로벌 방침 |
~/.claude/settings.local.json | 모든 프로젝트 × 해당 머신 | 공유하지 않음 |
평가는 프로젝트 로컬이 최우선이다. 단, deny는 어느 레이어에 있더라도 우선한다. 글로벌 settings.json에 Read(**/.env) deny를 하나 써두면, 어떤 프로젝트의 allow로도 덮어쓸 수 없다. 안전한 설정은 글로벌 deny에 두는 것이 기본 형태가 된다.
settings.local.json의 정체는 클릭 이력
첫 번째 의문은 누가 이 45개의 규칙을 작성했는가였다. 답은 바로 자신이다.
Claude Code가 허가 규칙 외의 명령을 실행하려고 하면 확인 프롬프트가 나타난다. 선택지에는 '이번만 허가'와 '앞으로 확인하지 않음'이 있으며, 후자를 선택하면 Claude Code가 그 자리에서 settings.local.json에 allow 규칙을 추가하여 저장한다. 즉, 그 파일은 설정이라기보다 과거의 "앞으로는 묻지 마"라는 클릭 로그다.
질이 나쁜 것은 안전하게 일반화할 수 없는 명령이다. 특정 URL의 curl을 Bash(curl *)로 넓힐 수는 없기 때문에, 이 완전 일치 명령을 앞으로 허가한다는 형태로 저장된다. 다시는 실행되지 않을 명령의 허가가 한 줄 남게 된다. 작업 중에 템포를 맞추기 위해 Enter나 2 키를 연타하다 보면, 이번 한 번만 하려던 것이 앞으로 확인하지 않는 쪽을 선택하게 될 때가 있다. 그것이 수개월 치 쌓이면 45개의 규칙이 된다.
삭제 가능한 규칙의 5가지 분류
실제로 삭제한 것들을 분류하면 다음과 같았다. 같은 정리를 하는 사람은 이 5가지 패턴을 찾으면 좋다.
일회성 허가
특정 URL의 curl, 특정 PID의 kill, /tmp에 둔 일회용 스크립트 실행. 그 당시에는 의미가 있었지만 다시는 매칭되지 않는다. 이 유형이 가장 많았다.
죽은 규칙
이전 머신의 절대 경로를 포함하는 규칙. 사용자 이름이 다르기 때문에 새 환경에서는 절대 매칭되지 않는데, 머신 이전 시 그대로 옮겨져 온다.
효력이 상실된 참조
MCP 서버의 도구 이름은 mcp__<서버ID>__<도구이름>
형식으로 저장되지만, 커넥터를 다시 연결하면 서버 ID가 변경될 수 있다. 이전 ID 규칙이 남아 있더라도 무엇과도 매치되지 않는다.
깨진 규칙 (Broken Rules)
Bash(python3 -c ':*)
와 같이 따옴표가 어중간하게 끊긴 패턴. 복잡한 원라이너(one-liner)를 앞으로 확인하지 않겠다고 답하면, 이런 식으로 깨지는 경우가 있다.
중복 규칙 (Redundant Rules)
Bash(python3 *)가 있는데 Bash(python3)가 공존하는 케이스. 넓은 범위의 규칙에 포함되는 좁은 규칙은 삭제해도 된다.
이 5가지 분류를 통해 45개가 13개까지 줄었다. 삭제해서 곤란했던 적은 현재까지 없다. allow를 삭제했을 때 최악의 상황은 다시 프롬프트가 뜨는 것뿐이며, 그 이상의 피해는 없다. 망설여진다면 삭제하는 쪽을 택할 수 있다.
써도 의미 없는 규칙이 있다
트랜스크립트(transcript)를 집계해 보면, Claude가 실행한 명령어 상위는 cd, echo, head, tail, grep, ls였다. 그렇다면 이것들을 allow에 추가하면 프롬프트가 줄어들까? 그렇지 않다. 처음부터 자동으로 허가되어 있기 때문이다.
Claude Code는 읽기 전용(read-only)이라고 판단할 수 있는 명령어를 내장하여 자동으로 허가한다. cat나 grep 같은 기본 명령어는 물론, git status나 git log 같은 git의 읽기 계열 서브 커맨드, gh pr view, docker ps 정도까지 커버된다. 이를 모르면 Bash(ls:*)와 같은 의미 없는 allow를 작성하게 된다. 인터넷의 설정 예시에는 이런 종류의 불필요한 규칙이 꽤 섞여 있다.
cd를 매번 물어본다는 것은 착각이다
자동 허가를 알고 나면 모순을 발견하게 된다. cd는 자동 허가 대상이어야 하는데, 체감상으로는 매번 허가를 요청받고 있다. deny에 넣었는지 설정을 다시 확인했지만 어디에도 없었다.
정체는 복합 명령어(composite command)였다. Claude는 cd /path/to/dir && python3 build.py와 같이 cd와 본래 명령어를 &&로 연결하여 실행하는 경우가 많다. 허가 판정은 명령어 전체에 대해 이루어지므로, 후반부의 python3 build.py가 미허가 상태라면 프롬프트가 뜬다. 그리고 확인 다이얼로그에 표시되는 것은 cd로 시작하는 줄 전체다. 사람의 눈에는 cd를 묻는 것처럼 보이는 것이다.
따라서 대책은 cd 쪽이 아니라 후반부 쪽에 세워야 한다. 프롬프트가 뜨면 && 뒷부분을 보고, 거기에 있는 명령어를 허가할지 판단한다.
인터프리터의 와일드카드(wildcard)는 전면 허가와 동일하다
가장 중요한 이야기다. Bash(python3 *)를 allow에 넣으면 프롬프트는 극적으로 줄어든다. Claude는 스크립트 처리를 대량으로 Python으로 수행하기 때문이다. 하지만 이는 임의 코드 실행(arbitrary code execution)을 전면 허가하는 것과 동일하다. -c는 파일 대신 인수로 전달된 문자열을 그대로 프로그램으로 실행하는 옵션이다. Python으로 작성할 수 있는 것은 파일 삭제든 네트워크 통신이든 전부 그곳에 적을 수 있으므로, python3 -c가 통과되는 시점에서 무엇이든 할 수 있다. 파일에 작성하여 python3 script.py로 실행하는 경우도 내용은 허가 규칙에서 보이지 않으므로 구조는 같다.
같은 이유로 위험한 것은 많다. node나 ruby 등의 인터프리터, bash -c나 eval, npx나 uvx 같은 패키지 러너(package runner), npm run *나 make * 같은 태스크 러너(task runner)의 와일드카드. npm run test와 같이 개별적으로 지정한다면 안전하다.
나는 이해한 상태에서 Bash(python3 *)를 사용하고 있다. 매번 확인해야 하는 번거로움과 저울질하여 내린 선택이며, 이것 자체를 부정할 생각은 없다. 위험한 것은 동일성을 모른 채 넣는 것과, 그것을 deny로 지키고 있다고 착각하는 것이다. Bash(rm -rf:*)를 deny에 넣어 두어도 python3 -c "import shutil; shutil.rmtree(...)"는 막지 못한다. allow를 넓히는 순간, deny의 그물은 그대로 통과될 수 있다.
deny는 .env만으로는 부족하다
deny의 정석은 .env 읽기 금지이지만, 다시 살펴보니 허점이 있었다. before와 after로 나누어 기재한다.
// before: 정석 세트
"deny": [
"Read(**/.env)", "Read(**/.env.*)",
...
언뜻 보면 철저해 보이지만, SSH 키는 id_rsa와 id_ed25519라는 기본 파일명만 커버하고 있다. 별칭으로 만든 키는 그대로 통과된다. 또 하나 놓치기 쉬운 것이 Claude Code 자신의 로그인 토큰인데, ~/.claude/.credentials.json에 평문으로 놓여 있다.
// after: 추가한 4가지
"Read(**/.ssh/**)", // 키 이름과 상관없이 .ssh 디렉토리 전체
"Read(**/.aws/**)", // AWS 인증 정보
...
범위를 넓힐 때 주의할 점은 오탐(誤爆)이다. Read(**/*token*)까지 설정하면 tokenizer.py와 같은 일반적인 파일을 읽을 수 없게 된다. *.key 역시 테스트용 인증서를 다루는 개발 환경에서는 걸려들게 된다. deny는 걸렸을 때 인지할 수 있으므로, 우선 넓게 설정해 두었다가 오탐이 발생하면 좁혀가는 방식으로 운영해도 좋다.
allow와 deny 사이에 ask가 있다
소개 기사에서는 allow와 deny의 두 가지 선택지로 설명하는 경우가 많지만, 실제로는 ask라는 매번 확인을 거치는 리스트가 있다.
"ask": [
"Bash(rm:*)",
"Bash(curl:*)",
...
위험해서 금지하고 싶지만, 가끔은 필요하다. 그런 명령어를 두는 곳이 바로 여기다. rm을 deny에 넣으면 정말 삭제하고 싶을 때 불편하고, allow에 넣는 것은 무섭다. ask라면 실행할 때마다 육안 확인 단계가 삽입된다. deny만큼 경직되지 않으면서도, allow만큼 무방비하지도 않다. 이 계층을 구분해서 사용하면 설계가 훨씬 수월해진다.
동기화한다면 settings.local.json만은 제외할 것
설정을 여러 대의 Mac에서 맞추고 싶어서, 실체를 Dropbox에 두고 ~/.claude/settings.json에서 심볼릭 링크 (Symbolic Link)를 거는 구성으로 만들었다. dotfiles의 정석대로 읽기와 쓰기 모두 링크를 투과하여 실체에 전달된다. 여기서 빠지기 쉬운 함정은 두 가지였다.
첫 번째. settings.local.json을 동기화 대상으로 삼아서는 안 된다. 이 파일은 권한 프롬프트가 나타날 때마다 자동으로 쓰여지기 때문에, 두 대에서 동시에 세션을 실행하면 클라우드 스토리지 상에서 충돌 복사본(Conflict Copy)이 생성되어 허가 규칙이 사라지거나 JSON이 깨질 수 있다. 실제로 동기화 대상에 포함되어 있던 시기의 파일에는 두 대 분량의 사용자 이름 절대 경로를 포함한 규칙이 혼재되어 있었다. 머신 고유(Machine-specific)라는 이름 그대로 취급하고, 공통화하고 싶은 규칙만 settings.json 쪽으로 승격시켜야 한다.
두 번째. 심볼릭 링크는 다른 파일에 작성한 뒤 이름을 바꾸어 교체하는 저장 방식을 만나면 소리 없이 해제된다. 실체에 쓰는 것이 아니라, 링크라는 파일 자체가 새로운 실제 파일로 덮어씌워지기 때문이다. 쓰기는 성공하기 때문에 알아채지 못한다. 그 순간부터 동기화는 조용히 멈춘다. 설정을 변경하는 작업을 했다면 ls -la로 링크가 살아있는지 확인하는 습관을 들이는 것이 좋다.
몇 달에 한 번 정도의 청소가 딱 적당하다
권한 설정은 한 번 만들고 끝나는 것이 아니라, 사용 이력이 계속 쌓이는 생물과 같았다. allow의 삭제는 프롬프트가 한 번 늘어날 뿐이므로 망설여진다면 지운다. 읽기 계열은 써봤자 무의미하다. 인터프리터(Interpreter) 허가는 의식적으로 관리한다. deny의 그물은 .env 너머까지 펼친다. 이 네 가지를 몇 달에 한 번씩 검토하는 정도가 딱 적당하다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기