
Claude Code를 사용하며 한 번 움찔한 뒤, settings.json에 추가한 설정
요약
Claude Code 사용 시 와일드카드(*) 설정의 위험성과 settings.json을 통한 권한 관리 방법을 다룹니다. 설정 파일의 우선순위와 deny 규칙의 특성을 이해하여 안전하게 도구를 사용하는 법을 설명합니다.
핵심 포인트
- Bash 와일드카드(*) 사용 시 의도치 않은 위험 명령어가 허용될 수 있음
- settings.json은 관리자, CLI, 로컬, 프로젝트, 사용자 설정 순으로 우선순위 적용
- deny(거부) 설정은 하위 단계에서 허가로 덮어쓸 수 없는 강력한 규칙임
- 버전 업데이트에 따라 설정 키 사양이 변경될 수 있으므로 직접 확인 권장
WordPress 플러그인 리포지토리(Repository)에서 Claude Code를 실행하며, git 명령어를 매번 일일이 허가하는 것에 지쳤습니다. 그래서 깊게 생각하지 않고 Bash(git *)를 allow(허용)에 넣어버렸습니다. 이 정도면 git 관련 명령어는 조용히 통과되겠지 하는 가벼운 마음이었습니다. 평소 WordPress 플러그인을 만들고 있고, Claude Code는 거의 매일 사용하는 도구이기에 확인 횟수를 단 하나라도 줄이고 싶었을 뿐이었습니다.
며칠이 지나, 문득 *이 실제로 무엇에 매치(Match)되는지 궁금해졌습니다. 찾아보니 *은 공백을 포함하여 임의의 문자열에 매치된다고 적혀 있었습니다. 즉, Bash(git *)는 git log --oneline뿐만 아니라, git push origin main도, git reset --hard HEAD~3도 전부 그대로 통과시키고 있었던 것입니다. 제가 '허가했다고 생각한' 범위와 실제로 열려 있던 범위가 처음부터 어긋나 있었습니다. 실행 중에는 전혀 눈치채지 못합니다. 허가 다이얼로그가 나오지 않는다는 것은 바로 그런 의미였습니다.
무언가 망가진 것은 아닙니다. 다만, git reset 행을 보았을 때 꽤 움찔했습니다. 플러그인의 작업 트리(Working tree)가 실수로 되돌려졌다면 꽤 뼈아픈 상황이 되었을 것입니다. 그 이후로 claude를 실행하기 전에 settings.json에 몇 줄을 더 적어 넣게 되었습니다. 이 기사는 그때 조사한 내용과 현재 제가 설정해 둔 내용을 그대로 나열한 것입니다. 실행한 뒤에 허가 문제와 씨름하는 것보다, 미리 적어 두는 것이 더 조용하게 시작할 수 있습니다.
설정 키의 이름과 동작은 버전에 따라 평범하게 변경됩니다. 아래에 적는 내용은 공식 문서의 Configure permissions와 설정 레퍼런스를 2026년 6월 5일에 다시 읽고 확인한 것입니다. 저는 다음 환경에서 사용하고 있습니다.
- Claude Code:
claude --version으로 나오는 번호 (공개 시점의 실제 값으로 대체) - Node.js: 실제 기기의
node -v값 - OS: 평소 사용하는 작업 머신
- 설정 파일: 사용자 전체의
~/.claude/settings.json과 프로젝트 개별의.claude/settings.json
키의 사양은 업데이트가 빈번한 부분이므로, 마지막에는 자신의 환경에서 /permissions와 /config를 열어 어떤 규칙이 어느 파일에서 읽히고 있는지 눈으로 직접 확인한 뒤 결정하시기 바랍니다. 아래 내용도 전부 제 머신에서 확인한 범위 내의 이야기로 읽어주시는 것이 안전합니다.
처음에 헤맸던 부분은 "설정을 적었는데도 아무것도 변하지 않는다"였습니다. 한동안 제 JSON 작성 방식을 의심했지만, 원인은 다른 곳에 있었습니다. settings.json은 하나가 아닙니다. 여러 개가 존재하며, 우선순위에 따라 덮어쓰기(Overwrite)됩니다. 높은 순서대로 나열하면 다음과 같습니다.
- 관리자가 배포하는 managed 설정 (조직의 정책. 개인은 거의 건드리지 않음)
- 실행 시의 커맨드 라인 인자 (Command line argument)
.claude/settings.local.json(해당 프로젝트의 개인 전용. git에는 포함되지 않음).claude/settings.json(해당 프로젝트 공유용. git에 포함됨)~/.claude/settings.json(사용자 전체. 모든 프로젝트에 적용됨)
위의 설정이 아래의 설정을 덮어씁니다. 여기까지는 단순합니다. 까다로운 것은 deny(거부)인데, 이것만은 예외입니다. 어느 한 단계에서라도 거부되면 아래 단계에서 다시 허가할 수 없습니다. "사용자 전체에서 허가했는데 이 프로젝트에서만 막힌다"라고 한다면, 대개 아래 단계에 오래된 deny 설정이 남아 있는 경우입니다. 제가 걸렸던 것도 이것이었으며, 프로젝트의 settings.local.json에 예전에 적어둔 deny를 잊어버렸을 뿐이었습니다.
구분 방법은 그렇게 어렵게 생각하지 않습니다. 안전한 설정은 사용자 전체에 두어 모든 프로젝트에 적용합니다. 해당 프로젝트에서만 사용하는 도메인 허가 같은 것만 프로젝트 측에 둡니다. 검증 중인 느슨한 허가는 settings.local.json에 빼두면 팀과 공유하는 파일을 더럽히지 않고 해결할 수 있습니다. 혼자 작업하더라도 이 세 가지 위치를 의식하는 것만으로 "적용되지 않음" 현상이 상당히 줄었습니다.
또 하나 머릿속을 정리하게 된 것은, 설정이 크게 두 개의 레이어(Layer)로 나뉘어 있다는 점이었습니다.
permissions는 "어떤 도구를 사용할 수 있는지, 어떤 파일이나 도메인에 접근할 수 있는지"를 결정합니다. Bash, Read, Edit, WebFetch 등 모든 도구가 대상입니다. 이와 대조적으로 sandbox는 Bash 명령어와 그 자식 프로세스(Child Process)만을 가두는 OS 레벨의 격리입니다. 보호 대상도, 보호하는 방식도 다릅니다.
이 두 가지를 별개의 것으로 이해하고 있으면, permissions만으로는 닿지 않는 상황이 보입니다. 예를 들어 Read(.env)를 deny에 넣었다고 가정해 봅시다. 이것이 막아주는 것은 Claude 본체의 파일 읽기 도구와, cat이나 head처럼 Claude Code가 "파일을 읽는 명령어"라고 인식하는 것까지입니다. Python 스크립트가 자체적으로 .env를 열어 내용을 읽는 것과 같은 동작은 permissions로 막을 수 없습니다. 그 부분을 OS 레벨에서 차단하는 것이 sandbox의 역할입니다. permissions로 "Claude가 건드리지 못하게" 하고, sandbox로 "애초에 프로세스가 닿지 못하게" 합니다. 이 두 겹을 겹쳐서, 한쪽을 뚫고 나가더라도 다른 한쪽에서 막히는 구조를 만드는 것입니다.
가장 먼저 작성하는 것은 이것입니다. .env나 키(Key)를 읽지 못하게 하는 deny 설정입니다.
"deny": [
"Read(.env)",
"Read(.env.*)",
...
여기서 경로 작성 방식 때문에 애를 먹었습니다. Read와 Edit의 패턴은 gitignore의 표기법을 따릅니다. Read(.env)는 파일명만 지정한 것이므로, 현재 디렉토리(Current Directory) 이하의 임의의 깊이에 있는 .env와 매치됩니다. Read(**/.env)라고 써도 마찬가지입니다. 깊은 계층에 숨겨진 .env도 찾아내 주므로 이 점은 솔직히 도움이 되었습니다.
제가 헤맸던 부분은 맨 앞의 슬래시(/) 처리입니다. 직관과는 조금 다릅니다.
/src는 파일 시스템의 루트가 아니라, 프로젝트 루트로부터의 상대 경로입니다. 진짜 절대 경로는 슬래시 두 개를 사용하여//Users/alice/secrets/**와 같이 작성합니다. 홈 디렉토리로부터의 경로는~/Documents/*.pdf와 같이 틸드(~)를 사용하여 작성합니다.
저는 처음에 /etc/...를 루트 직하의 경로라고 생각하고 작성했다가, 전혀 다른 곳을 가리키게 되었습니다. 이 부분은 몇 번이나 잘못 작성한 끝에, /permissions에서 실제로 어디를 가리키고 있는지 확인한 후에야 수정할 수 있었습니다.
또 하나, 심볼릭 링크(Symbolic Link)를 다루는 방식이 은근히 영리합니다. Claude가 링크에 접근할 때, 링크 자체의 경로와 그 너머에 있는 파일 양쪽을 모두 확인합니다. deny는 둘 중 하나라도 일치하면 차단합니다. 따라서 ./project/key가 ~/.ssh/id_rsa로 연결된 링크라 하더라도, 링크 대상이 deny에 해당하면 제대로 차단됩니다. 반대로 allow는 양쪽 모두 일치해야 통과되므로, 허용된 디렉토리 내에서 외부를 가리키는 링크는 확인 절차를 거치게 됩니다. 안전한 쪽으로 기울어진 설계라 이 부분은 안심하고 맡길 수 있습니다.
처음에 언급했던 Bash(git *)로 돌아가겠습니다. 이번에 가장 강조하고 싶었던 부분이 바로 여기입니다.
Bash 규칙의 *는 공백을 포함한 임의의 문자열과 매치됩니다. 따라서 * 하나가 여러 개의 인자(Argument)를 가로질러 매치될 수 있습니다. Bash(git *)는 git log --oneline --all도, git push origin main도 매치됩니다. "git 명령어 중에서도 읽기 계열만 허용하고 싶다"는 의도로 작성하면, 쓰기 계열까지 함께 허용되어 버립니다. 이것이 제가 느꼈던 서늘함의 정체였습니다.
공백의 유무에 따라서도 의미가 달라집니다. 이를 모르면 흔히 실수하게 됩니다.
Bash(ls *)는 공백이 포함되어 있어 단어 경계(Word Boundary)가 작동합니다.ls -la에는 매치되지만lsof에는 매치되지 않습니다.Bash(ls*)는 공백이 없어 경계가 없습니다.ls -la와lsof모두 매치됩니다.
끝에 붙는 :*는 끝에 공백이 포함된 *와 같은 의미입니다. Bash(ls:*)는 Bash(ls *)와 동일합니다. 단, :*가 작동하는 것은 끝부분뿐입니다. 중간에 작성하면, 예를 들어 Bash(git:* push)의 콜론(:)은 단순한 문자로 취급되어 git 명령어와 매치되지 않게 됩니다. 허용 다이얼로그에서 "앞으로 확인하지 않음"을 선택했을 때 저장되는 형태는 공백으로 구분된 형태입니다. 저는 헷갈릴 것 같으면 공백 구분 방식으로 통일하여, 이상하게 저장되지 않도록 하고 있습니다.
복합 명령어를 다루는 방식은 오히려 안심할 수 있는 요소였습니다. Claude Code는 셸의 구분자(&&, ||, ;, 파이프 등)를 이해하고 있어서, Bash(safe-cmd *)라는 허용 설정이 있더라도 safe-cmd && other-cmd를 그대로 통과시키지 않습니다. 각각의 서브 명령어(Sub-command)가 개별적으로 규칙과 대조됩니다. 명령어를 이어서 쓰면 규칙을 빠져나갈 수 있는 식의 허술한 구멍은 없다는 뜻입니다.
다만 예외는 있습니다. timeout이나 nice와 같이 정해진 래퍼(Wrapper)는 벗겨내고 내부 명령어로 대조해 주지만, npx나 docker exec, devbox run과 같은 실행 러너(Runner)는 벗겨내지 않습니다. 그래서 Bash(devbox run *)를 허용하면, devbox run rm -rf .까지 통과되어 버립니다. 러너를 허용하고 싶다면, Bash(devbox run npm test)와 같이 내부 내용까지 포함한 구체적인 규칙을 하나씩 작성할 수밖에 없습니다. 번거롭지만, 이 부분을 소홀히 하면 설정의 의미가 없어집니다.
반대로, 처음부터 확인 없이 통과되는 읽기 전용 명령어 그룹도 있습니다. ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd, 그리고 읽기 계열인 git. 이것들은 어떤 모드에서도 묵묵히 실행됩니다. 이 목록은 변경할 수 없으므로, 여기서 확인 절차를 거치고 싶다면 ask나 deny를 명시적으로 추가해야 합니다. 저는 "Claude가 멋대로 cat을 하는 것"은 오히려 환영하기 때문에 이 부분은 그대로 두었습니다.
도메인을 제한하고 싶어서, 처음에는 Bash(curl http://github.com *)와 같은 규칙으로 curl의 목적지를 GitHub로 묶으려 했습니다. 하지만 이것은 효과가 없었습니다. 이유는 인자(Argument)로 명령어를 제한하는 규칙 자체가 본래 취약하기 때문입니다.
- URL 앞에 옵션이 오면 벗어남 (
curl -X GET http://github.com/...) - 프로토콜이 다르면 벗어남 (
https://...) - 리다이렉트(Redirect)로 다른 도메인으로 이동 가능 (
curl -L http://bit.ly/xyz) - 변수를 경유하면 보이지 않음 (
URL=http://github.com && curl $URL)
문서에도 인자로 curl을 제한하는 것은 무리라고 적혀 있습니다. 대신 curl이나 wget을 deny로 통째로 막고, 인터넷 접속 용도는 WebFetch 도구로 돌려 WebFetch(domain:github.com)로 허용 도메인을 관리합니다. 이 방식으로 바꾼 뒤로 도메인 제어가 상당히 수월해졌습니다. "명령어의 인자로 애쓰는 것"을 그만두고 "도구 자체를 전환하는 것"이 결국 가장 편했습니다.
저는 defaultMode를 acceptEdits로 설정해 두었습니다.
"defaultMode": "acceptEdits"
이 모드는 파일 편집과 작업 디렉토리 내의 mkdir / touch / mv / cp 정도를 자동으로 수락합니다. 매번 "편집해도 될까요?"라는 질문이 나오지 않아 편합니다. 대신, 편집 대상이 되는 디렉토리를 deny로 제대로 감싸두지 않으면 수락하고 싶지 않은 것까지 수락해 버립니다. 편리함의 대가로, deny를 성실하게 작성하는 것을 전제로 하는 모드라고 생각합니다. 저처럼 deny를 먼저 단단히 구축한 뒤 이 모드로 전환하면 딱 적당한 수준이 됩니다.
다른 모드도 살펴보자면, plan은 읽기와 조사만 수행하고 편집은 하지 않는 계획 모드이며, bypassPermissions는 모든 것을 스킵하는 강력한 모드입니다. bypassPermissions는 .git이나 .claude에 대한 쓰기까지 그대로 통과시킵니다 (루트나 홈 디렉토리의 rm -rf만은 최후의 보루로서 확인 절차가 나타납니다). 컨테이너나 VM처럼 망가져도 상관없는 격리 환경이 아니라면 사용하지 않기로 결정했습니다. 한 번 가벼운 마음으로 직접 써봤다가, 일상적으로 사용하는 리포지토리에서 돌릴 만한 것이 아니라는 것을 몸소 깨달았습니다.
permissions를 단단히 구축했다면, 그 아래에 sandbox를 배치합니다. 제 설정은 다음과 같습니다.
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false,
...
enabled: true로 설정하면 Bash의 격리 기능이 활성화됩니다. 이 상태에서는 autoAllowBashIfSandboxed가 기본값으로 true이기 때문에, sandbox 경계 내에서 실행되는 Bash 명령어는 별도의 확인 없이 작동합니다. 확인 절차가 줄어드는 대신, 동작할 수 있는 범위가 제한된 틀 안에 갇히게 됩니다. 저는 이 교환(trade-off)이 마음에 듭니다. deny 목록은 이런 상태에서도 살아있어서, 루트나 홈 디렉토리를 노리는 rm 명령어는 여전히 확인을 요청합니다.
allowUnsandboxedCommands: false는 sandbox 외부에서 실행되는 우회 경로(예: dangerouslyDisableSandbox)를 차단합니다. 기본값은 true여서 빠져나갈 수 있는 상태이므로, 이곳을 false로 설정해야 비로소 '외부로 나갈 수 없다'가 됩니다. 짧은 한 줄이지만, 제 설정 중에서는 이것이 가장 효과적이라고 느낍니다.
excludedCommands는 제가 가장 많이 실수했던 부분입니다. 여기에 넣은 명령어는 sandbox 외부에서 작동합니다. 즉, 평소와 같은 권한으로 실행됩니다. 그리고 값은 `
이 파일을 설정한 후 며칠간 사용해 보니, 확인 다이얼로그(confirmation dialog)가 눈에 띄게 줄었습니다. 아찔했던 Bash(git *)도, 작성 습관을 파악한 뒤로는 git diff *와 git log *로 나누어 용도에 따라 구분하게 되었습니다. git push는 ask에 두어, 명령을 내리는 순간에만 수동으로 확인합니다. 이 정도의 거리감이 현재로서는 가장 마음이 편합니다.
sandbox 부분은 아직 완벽히 다듬지 못했습니다. excludedCommands에 무엇을 넣을지, allowedDomains를 어디까지 확장할지는 프로젝트마다 추가하거나 빼면서 수정하는 중입니다. OS 레벨의 격리(isolation)이기 때문에 환경에 따라 동작이 다른 부분도 있어, 결국 자신의 머신에서 직접 확인하며 진행하는 것이 가장 빠르다는 것이 현재의 체감입니다. 처음부터 깔끔한 정답을 쓰려고 고민하며 멈춰 있기보다는, 일단 실행하고 수정하는 방식이 저에게 더 맞았습니다.
다음에 새로운 리포지토리(repository)에서 Claude Code를 사용할 때는, 우선 이 사용자 설정을 베이스로 삼고 그 프로젝트에서만 사용하는 도메인과 허용 사항만 프로젝트 측에 추가할 것입니다. 실행한 뒤에 허용 권한과 씨름하는 것보다, 그 순서로 진행하는 것이 더 차분하게 시작할 수 있습니다. 다음의 나에게 전달하는 메모로서 여기에 남겨둡니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기