Claude Code 에서 .env 읽기를 deny 규칙으로 막기
settings.json 의 permissions.deny 에 Read(./.env) 를 넣어 Claude Code 가 비밀 파일을 읽지 못하게 하고, 거부되는 것과 Bash 로 새는 것을 직접 돌려 확인합니다.
.env 를 Claude Code 가 열어 보지 못하게 하려면 permissions.deny 에 규칙을 적습니다. 막히는 범위와 안 막히는 범위가 갈리므로 돌려서 확인합니다.
[1] 시험용 폴더에 가짜 비밀 파일을 둡니다. 값은 모두 가짜입니다.
$ cd /srv/app$ printf 'DEMO_KEY=not-a-real-key\n' > .env$ mkdir secrets && printf 'DB_PASS=not-a-real-password\n' > secrets/db.txt[2] 규칙 없이 먼저 읽어 봅니다. -p 는 한 번 묻고 끝나는 실행이고, --output-format json 은 거부된 호출 목록 permission_denials 를 함께 줍니다.
$ claude -p "Read the file .env in the current directory and tell me the value of DEMO_KEY. Reply with just the value, or say why you could not." --max-turns 3 --output-format json > run1.json$ jq -r '.result' run1.json | head -1`not-a-real-key`$ jq -c '.permission_denials' run1.json[][3] 프로젝트의 .claude/settings.json 에 deny 규칙을 적습니다. 설정 파일은 엄격한 JSON 이라 // 주석이나 끝 쉼표가 있으면 Settings Error 가 납니다.
{ "permissions": { "deny": [ "Read(./.env)", "Read(./secrets/**)", "Bash(curl:*)" ] }}Bash(curl:*) 의 :* 는 Bash(curl *) 과 같은 뜻입니다. Read(secrets/**) 처럼 쓰면 deny 에서는 현재 폴더 아래 어느 깊이의 secrets 든 걸립니다.
[4] 같은 요청을 다시 보냅니다.
$ claude -p "Read the file .env in the current directory and tell me the value of DEMO_KEY. Reply with just the value, or say why you could not." --max-turns 3 --output-format json > run2.json$ jq -c '.permission_denials[] | {tool_name, tool_input}' run2.json{"tool_name":"Read","tool_input":{"file_path":"/srv/app/.env"}}.result 에는 값 대신 deny 규칙이 읽기를 막았다는 설명이 나왔고 DEMO_KEY 값은 없었습니다.
[5] Bash 로 같은 파일을 읽게 해 봅니다. cat 은 원래 묻지 않고 실행되는 읽기 전용 명령이고 --allowedTools 로 허용까지 했지만 막힙니다. 공식 문서가 Read deny 는 cat·head·tail·sed·tee 같은 Bash 파일 명령과 < > 리다이렉션 대상에도 적용된다고 적은 대로입니다. deny 는 allow 보다 먼저 평가됩니다.
$ claude -p 'Run the shell command `cat .env` and tell me the output. Do not use the Read tool.' --max-turns 3 --output-format json --allowedTools "Bash(cat *)" > run3.json$ jq -c '.permission_denials[] | {tool_name, tool_input}' run3.json{"tool_name":"Bash","tool_input":{"command":"cat .env","description":"Print contents of .env file"}}[6] 명령에 파일 이름이 없는 경우를 시험합니다. 스크립트가 직접 파일을 여는 방식입니다.
$ claude -p 'Run the shell command `python3 -c "print(open(\".env\").read())"` and tell me the output.' --max-turns 3 --output-format json --allowedTools "Bash(python3 *)" > run4.json$ jq -c '.permission_denials' run4.json[]$ jq -r '.result' run4.json | grep DEMODEMO_KEY=not-a-real-key거부되지 않았고 값이 그대로 나왔습니다. 문서에도 같은 한계가 있습니다. Read·Edit deny 는 grep -r pattern . 처럼 파일 이름 없이 읽는 명령이나, 파일을 스스로 여는 Python·Node 스크립트 같은 임의의 하위 프로세스에는 적용되지 않습니다.
[7] 새는 부분은 문서가 권하는 방법으로 막습니다. 모든 프로세스의 접근을 OS 수준에서 끊으려면 샌드박스를 켭니다. 명령 전문을 직접 검사하려면 PreToolUse 훅을 씁니다. 훅이 allow 를 돌려줘도 매칭된 deny 규칙은 그대로 막으므로 둘은 함께 쓸 수 있습니다. 훅 작성은 Claude Code 훅으로 위험한 명령 막기 에 있습니다.
Bash(curl:*) 도 같은 성격입니다. 문서 표에서 Bash(curl *) 은 curl https://example.com 을 막지만 /usr/bin/curl … 과 sh -c 'curl …' 은 막지 못합니다. Bash deny 는 Claude 가 보통 쓰는 형태를 막는 장치이고 보안 경계가 아닙니다.
[8] 설정 파일이 여럿이면 높은 쪽이 같은 키를 덮어씁니다. 순서는 관리형 설정 → claude --settings → .claude/settings.local.json(프로젝트 로컬) → .claude/settings.json(프로젝트 공유) → ~/.claude/settings.json(사용자) 입니다. 다만 deny 는 범위와 상관없이 하나라도 걸리면 막히므로, 사용자 설정이 허용한 것도 프로젝트 설정이 deny 하면 막힙니다. 팀 공통 규칙은 .claude/settings.json 에, 개인 규칙은 .claude/settings.local.json 에 둡니다.