claude -p
를 무인으로 실행하거나 (혹은 평소 대화에서도), .env 파일을 AI가 읽는 것을 원치 않아 settings.json의 deny에 Read(./.env)를 넣었거나, 앞으로 넣으려고 하는 분들에게 이 글을 드립니다. 본문에서는 해당 규칙이 '어떤 경로를 막고, 어떤 것을 허용하는지'를 공식 문서를 통해 확인하고, 남아있는 구멍을 sandbox로 어떻게 막을 수 있는지까지 정리합니다.
결론부터 말씀드리자면, deny는 '이 툴 호출을 거부한다'는 규칙이지, '이 파일을 누구도 읽지 못하게 한다'는 규칙이 아닙니다. Read(./.env)가 차단하는 것은 Read 툴과 파일명을 명시하여 읽는 셸 명령어(cat/head/tail/sed/tee)입니다. .env.local 같은 별칭 파일, 이름 없이 읽는 명령어인 grep -r, 그리고 python/node가 스스로 여는 파일은 막지 못합니다. 후자까지 차단하려면 OS 레벨에서 작동하는 sandbox의 filesystem.denyRead가 필요하며, 심지어 이 기능조차 Read 툴을 막지는 못합니다. 따라서 두 가지를 함께 사용해야 합니다.
deny는 '파일 보호'가 아니라 '툴 호출 제한'입니다
Claude Code의 허용 규칙은 allow / ask / deny 세 종류로 나뉘며, 공식 문서에는 다음과 같이 명시되어 있습니다.
Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn't change the order.
(규칙은 deny → ask → allow 순서로 평가됩니다. 그 순서대로 가장 먼저 일치하는 것이 결과를 결정하며, 규칙의 구체성은 순서를 바꾸지 않습니다.)
Read(./.env)를 deny에 작성했을 때 이것이 무엇을 의미하는지는 같은 문서의 표에도 나와 있습니다.
| Read(./.env) | 현재 디렉터리에서 .env 파일을 읽는 것과 일치합니다 |
즉, '현재 디렉터리의 .env를 읽는 Read 툴 호출에 일치한다'는 의미입니다. 여기서 중요한 것은 이것이 파일 자체가 아니라 툴 호출에 대한 규칙이라는 점입니다. 따라서 '.env를 읽는다'는 목적에 다른 경로가 있다면, 규칙을 하나 더 추가하는 것만으로는 우회할 수 있습니다. 경로마다 막히는 경우와 안 되는 경우가 다릅니다. 앞으로 이 내용을 설명하겠습니다.
우선 제 환경부터: 지금은 막고 있지 않습니다
남들에게 설득당하기 전에 제 설정을 확인해 보겠습니다. 이 Zenn 아티클 리포지토리는 매일 오전 10:30에 launchd를 통해 claude -p를 무인으로 실행하며, 그날의 제안 중 하나를 골라 아티클을 작성하고 commit → push까지 자동으로 처리합니다. 해당 작업 권한은 scripts/daily-article-write.sh의 --allowedTools와 --permission-mode default만으로 결정됩니다.
claude -p "/write-article ..." \
--permission-mode default \
--allowedTools "Read Glob Grep WebSearch WebFetch Edit(articles/**) Edit(proposals/**) Bash(git log:*) ... Bash(go test:*) Bash(go vet:*) Bash(go build:*)"
...
.claude/와 scripts/, 그리고 사용자 설정(~/.claude/settings.json)을 보면, deny 규칙은 하나도 없고, sandbox 설정도 없으며, Read는 무제한으로 허용되어 있습니다.
--permission-mode default는 허용 규칙이 적용되는 모드이지만, 여기에 .env를 막는 규칙이 없습니다. 이 리포지토리에는 .env 파일을 두고 있지 않기 때문에 실제 피해는 없지만, 문제는 '비밀이 없어서 안전한 것'이 아니라 '비밀을 두면 지나가면서 읽힐 수 있는 상태로 되어 있다'는 것입니다. 같은 claude -p
.env 파일의 경우, 해당 리포지토리에 접근하는 순간 구멍이 생깁니다. 따라서 먼저 Read(./.env)를 deny에 추가합니다. 여기부터가 본론입니다.
Read(./.env)가 차단하는 경로와 우회하는 경로
Read 및 Edit의 deny 규칙이 '어떤 파일 접근에 적용되는지'는 문서의 Warning 섹션에 정확하게 나와 있습니다.
Read and Edit deny rules apply to Claude's built-in file tools, to file commands Claude Code recognizes in Bash, such as
cat,
head,
tail,
sed,
및 tee,
그리고 > file나 < file 같은 Bash 리디렉션의 대상에 적용됩니다. 하지만 파일 이름을 명시하지 않고 파일을 읽는 명령어, 예를 들어 해당 파일이 있는 디렉토리에서 실행하는 grep -r pattern .이나, 파일을 직접 여는 Python 또는 Node 스크립트와 같은 임의의 서브프로세스에는 적용되지 않습니다. 모든 프로세스로부터 경로에 대한 접근을 차단하려면 sandbox를 활성화해야 합니다.
이것을 .env 파일에 적용하면, Read(./.env) 하나가 막는 경로는 다음과 같습니다.
.env를 읽는 경로 | Read(./.env) deny | |
---|---|
| Read 도구 (Grep・Glob도 베스트 에포트로 대상) | ✅ 차단됨 |
cat .env / head / tail / sed / tee ,< .env 리디렉션 | ✅ 차단됨 |
grep -r KEY . (파일 이름을 명시하지 않고 읽기) | ❌ 적용 안 됨 |
python / node가 open('.env') | ❌ 적용 안 됨 |
아래 두 줄이 '규칙을 하나만 추가해서 우회할 수 있는' 경로입니다. 순서대로 이름을 붙였습니다.
남아있는 구멍을 이름으로 언급하기
구멍 1: 별칭 파일은 별 규칙
Read(./.env)는 .env라는 이름의 파일에만 일치합니다. .env.local이나 .env.production은 다른 파일명이므로 이 규칙으로는 막을 수 없습니다. 허용 규칙의 경로는 전방 일치가 아니라 gitignore의 글로브 구문이므로, 별칭까지 차단하려면 별도의 패턴이 필요합니다. 공식의 '비밀 파일 제외' 설정 예시도 .env와 .env.*를 모두 언급하고 있습니다.
{
"permissions": {
"deny": [
...
이러한 동작, 즉
기준이 어긋나면 벗어납니다. 이는 별도 글의 '상대 경로 허용 규칙은 쉘의 cwd 기준'과 같은 함정입니다.
함정 2: 이름 없이 읽는 명령어
Warning에서 지적한 grep -r pattern .
이 .env에도 적용됩니다. grep은 파일명을 인수로 받지 않고 디렉토리를 탐색하기 때문에, Read / Edit의 deny 대상이 아닙니다. 게다가 grep은 규칙 없이도 통과하는 '내장 읽기 전용 명령어 집합'에 포함되어 있습니다.
The set includes
ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd, 그리고 git의 읽기 전용 형태들입니다. 이 집합은 설정 변경이 불가능합니다. 만약 이 명령어 중 하나에 대해 프롬프트를 요구하려면, 해당 명령어에 ask 또는 deny 규칙을 추가해야 합니다.
여기에 cat과 grep 사이에 비대칭성이 있습니다. cat .env는 Read(./.env) deny로 막히지만, 같은 읽기 전용 집합에 속한 grep -r KEY .은 막히지 않습니다. 그 이유는 cat이 '파일을 명시하여 읽는 명령어'로서 deny 대상이지만, grep -r은 '이름을 쓰지 않고 읽는 명령어'라서 대상이 아니기 때문입니다. 물론 Bash(grep -r *)를 deny에 추가해서 막을 수도 있지만, find ... -exec cat이나 인터프리터와 같이 형태가 다른 방법들이 너무 많아서 추격전이 됩니다. 문서 자체도 Bash 규칙에 대해 '평소 Claude가 작성하는 호출을 커버하는 것이지, 프로그램을 둘러싼 보안 경계는 아니다'라고 명시하고 있습니다.
함정 3: 스스로 파일을 여는 인터프리터
Bash(python:*)나 Bash(node:*)를 허용하면, open('.env')로 내용을 읽을 수 있습니다. 이 역시 Warning에서 지적했듯이 deny 대상이 아닙니다. 게다가 'verify/'에만 쓸 수 있는 node'와 같은 부분적인 허가는 만들 수 없습니다—Read / Edit의 경로 규칙은 인터프리터가 스스로 여는 파일에는 적용되지 않기 때문에, node를 허용한다면 node가 접근할 수 있는 범위 전체를 허용하는 것이 됩니다. 이 내용은 무인 실행 검증 시에 별도 글에서 다루었습니다.
filesystem.denyRead
또 한 번: sandbox의 함정 2, 3에 공통되는 것은 '쉘 명령어와 그 서브 프로세스가 도구 계층의 규칙을 빠져나와 파일을 여는 것'입니다. 이것을 막는 것이 sandbox입니다. sandbox 문서 초반부에 다음과 같이 나와 있습니다.
The Bash sandbox is a boundary that the operating system enforces around the shell commands Claude runs on your machine.
(Bash sandbox는 Claude가 머신 위에서 실행하는 쉘 명령어를 OS가 강제하는 경계)
sandbox는 기본적으로 비활성화되어 있으며, sandbox.enabled를 true로 설정하거나 세션에서 /sandbox를 실행하면 활성화됩니다. macOS, Linux, WSL2에서 작동합니다 (네이티브 Windows는 샌드박스 없이 작동하므로, WSL2 안에서 실행합니다). 읽기를 막는 핵심은 sandbox.filesystem.denyRead입니다. 여기서 '쉘 명령어와 그 서브 프로세스'를 OS 레벨에서 막기 때문에, grep -r로든 python의 open()으로든, 명령어 작성 방식에 관계없이 효과가 있습니다.
하지만, sandbox도 만능은 아닙니다. 같은 문서에 이렇게 나와 있습니다.
The sandbox는 셸 명령어만 커버합니다. Claude의 파일 도구(file tools), MCP 서버, 그리고 hook은 그 외부에서 실행됩니다.
A
denyRead
엔트리는 Read 도구를 막지 못하며, allowedDomains는 WebFetch를 제한하지 못합니다.
즉, deny와 sandbox는 커버하는 층이 명확하게 어긋나 있습니다. 그래서 둘 다 필요합니다.
.env를 읽는 경로 |
① Read(./.env) deny (도구 계층) | ② sandbox filesystem.denyRead (OS 계층・셸만) |
|---|---|---|
| Read 도구 (Grep・Glob은 베스트 에포트) | ✅ 막힘 | ❌ 효과 없음 (도구는 sandbox 외부에 있음) |
cat / head / tail / sed / tee ・리다이렉트 | ✅ 막힘 | ✅ 막힘 |
grep -r KEY . (이름을 쓰지 않고 읽기) | ❌ 효과 없음 | ✅ 막힘 |
python / node가 open('.env') |
| ❌ 효과 없음 | ✅ 막힘 |
둘 중 하나만으로는 반드시 한쪽의 구멍이 남습니다. Read(./.env)만으로는 grep -r과 인터프리터가 통과하고, sandbox만으로는 Read 도구가 통과합니다.
결국 어떻게 조합할 것인가
사용하지 않는 claude -p를 .env 파일이 있는 리포지토리로 향하게 하려면, 시작 시 두 가지를 모두 명시해야 합니다. 도구 계층은 위의 '비밀 파일 제외' deny를 그대로 사용하고, OS 계층은 sandbox를 활성화하여 .env를 denyRead에 포함합니다. sandbox의 경로 표기법은 Read / Edit 규칙과 달리, /tmp/build가 절대 경로, ~/.kube가 홈 상대 경로, 나머지는 상대 경로라는 표준적인 방식이므로 섞지 않도록 주의해야 합니다.
claude -p "..." \
--permission-mode default \
--allowedTools "Read Glob Grep WebSearch WebFetch Edit(articles/**) ..." \
...
deny에 Bash(curl *)를 넣는 것은 '읽을 수는 있어도 외부로 내보내지 못하게' 하기 위한 띠+멜빵 같은 것이지만, 이것 역시 작성 방식을 바꾸면 우회할 수 있습니다 (sh -c 'curl ...' 등). 따라서 유출까지 진심으로 막으려면 sandbox의 네트워크 허용 목록과 병행해야 합니다. 요컨대, 도구 계층의 deny는 'AI가 의도한 길에 태우는' 도구이고, sandbox는 'OS에 강제하는 경계'라서 역할이 다릅니다.
저의 업무는 지금 이 둘을 모두 넣고 있지는 않습니다. 다루는 것이 비밀이 없는 공개 리포지토리이기 때문이지만, .env 파일이 있는 리포지토리에 같은 유형을 향하게 하기 전에, deny와 sandbox를 모두 넣은 후에 하는 것이 제가 설정을 재검토하여 내린 결론입니다.
요약
Read(./.env)의 deny는 '파일 보호'가 아니라 '도구 호출 제한'. 막는 것은 Read 도구와, 명시적으로 읽는 cat / head / tail / sed / tee와 리다이렉트 -
.env만으로는 .env.local 같은 별명 파일은 누출될 수 있다. 공식의 제외 예도 Read(./.env)와 Read(./.env.*)를 모두 언급하고 있다 -
grep -r은 이름을 쓰지 않고 읽기 때문에 deny 대상이 아니다. 게다가 grep은 읽기 전용 집합에 있고 규칙 없이 작동한다. cat는 막히는데 grep -r은 안 막히는 비대칭성이 있다 -
python / node가 스스로 여는 파일도 deny 대상이 아니다 - 이것들을 OS 레벨에서 막는 것이 sandbox의 filesystem.denyRead
다만, sandbox는 셸 명령어만을 커버하며, Read 도구, MCP(Multi-Context Prompt), hook에는 적용되지 않습니다. deny와 sandbox가 커버하는 계층이 다르기 때문에, 어느 한쪽만으로는 반드시 반대편에 구멍이 남게 됩니다. 무인 작업은 두 가지를 조합하여 사용해야 합니다.
자신의 settings.json,
.env
계열의 deny는 어떤 패턴까지 작성하셨나요? deny만으로 또는 sandbox만으로 막을 수 있다고 생각했던 경로가 있다면, 댓글로 알려주시면 감사하겠습니다.
참고
- Configure permissions — Claude Code Docs (규칙 우선순위 / Read 및 Edit / 읽기 전용 명령어 / Bash 규칙이 일치하지 않는 경우)
- Configure the sandboxed Bash tool — Claude Code Docs (sandbox가 제한하는 것 /
filesystem.denyRead / sandbox 외부에서 실행되는 것) - Settings reference — Claude Code Docs (민감한 파일 제외 / sandbox.enabled / sandbox.filesystem.*)
토론