
확인 프롬프트를 줄였더니 더 안전해졌다: Claude Code의 auto 모드와 샌드박스(Sandbox) 1개월 운용 설계
요약
Claude Code의 확인 피로를 줄이기 위해 'auto' 모드를 활용하면서도, 샌드박스, 권한 설정, 훅(Hook)의 3단계 구조를 통해 보안을 강화한 운영 설계 사례를 소개합니다.
핵심 포인트
- 확인 프롬프트 과다로 인한 '확인 피로'가 오히려 보안 취약점을 유발함
- 샌드박스, permissions, 훅(Hook)의 3층 방어 체계 구축
- autoAllowBashIfSandboxed와 failIfUnavailable 설정을 통한 안전한 자동화
- 네트워크 화이트리스트를 통한 의존성 취득처 고정 및 보안 강화
Claude Code를 사용하기 시작했을 무렵에는 확인 프롬프트가 나올 때마다 읽었습니다.
그러다 어느덧 읽지 않게 되었습니다. 수십 번이나 비슷한 확인이 반복되면 당연히 그렇게 됩니다. 내용을 보지 않고 통과시키기. 그런 상태로 한동안 사용하다가 어느 순간 깨달았습니다. 확인이 많을수록 안전하다고 생각했는데, 실제로는 확인이 너무 많아서 읽지 않게 되었던 것이다.
지금 저는 defaultMode를 auto로 설정하여 일상적인 확인을 거의 하지 않습니다. 대신, 위험한 일을 구조적으로 할 수 없도록 만들었습니다. 약 1개월 정도 운용해 보니 확인 작업은 격감했고, 확인 피로(Confirmation fatigue)로 인한 실수도 줄었습니다. 그 설계에 대해 쓰겠습니다.
저는 WordPress 플러그인을 제작하여 WordPress.org에 배포하고 있는 한 명의 개발자입니다. 스테이징(Staging) 환경은 없습니다. 이 전제가 설정의 몇 가지 판단에 영향을 미쳤습니다.
전제: 한 번 폴더를 잃어버린 적이 있다
설계 이야기를 하기 전에 동기를 적어둡니다.
2026년 7월 19일에 플러그인 개발 폴더를 잃어버렸습니다. git 관리하에 있던 것들은 복구할 수 있었지만, 의도적으로 git 관리 외에 두었던 작업 메모나 문서는 복구되지 않았습니다. 혼자 작업하고 있고 스테이징도 없기에, 사라진 것은 사라진 채로 남았습니다.
이때 확인 프롬프트는 나오고 있었습니다. 나오고 있었고, 저는 통과시키고 있었습니다. 확인 기능이 작동하지 않았던 것입니다.
그래서 방침을 바꿨습니다. 확인에 의존하는 것을 그만두고, 할 수 없게 만든다.
3개 층으로 생각하기
현재 설정은 세 가지 층으로 나뉘어 있습니다.
**샌드박스(Sandbox)**가 가장 바깥쪽입니다. 파일 쓰기 대상과 네트워크 접속 대상을 OS 레벨에서 제한합니다. 여기서 막힌 것은 애초에 명령으로서 성립하지 않습니다.
permissions가 그 안쪽입니다. 무엇을 허용하고, 무엇을 확인하며, 무엇을 거부할 것인가. 명령 단위로 작성합니다.
**훅(Hook)**이 가장 안쪽입니다. permissions의 입도(Granularity)로는 표현할 수 없는 것을 셸 스크립트(Shell script)로 판정합니다.
이 세 가지가 있기 때문에 defaultMode: auto를 사용할 수 있습니다. 확인을 줄일 수 있는 이유는 확인 이외의 방법으로 막는 메커니즘이 있기 때문입니다. 순서대로 적겠습니다.
층 1: 샌드박스(Sandbox)
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
...
여기서 중요한 것이 두 가지 있습니다.
autoAllowBashIfSandboxed: true가 확인 감소의 핵심입니다. 샌드박스 안에서 실행된다면 Bash 확인을 띄우지 않습니다. 이것이 있기에 일상적인 작업이 끊기지 않습니다.
failIfUnavailable: true가 그 안전밸브입니다. 어떤 이유로든 샌드박스를 사용할 수 없을 때,
샌드박스 없이 실행하는 것이 아니라, 실패하게 만듭니다. 이 부분을 false로 설정하면, 샌드박스가 몰래 해제된 상태로 자동 실행이 계속됩니다. 가장 위험한 형태입니다. 이 두 가지는 세트입니다. 하나만 넣어서는 의미가 없습니다.
네트워크를 화이트리스트로 만든 이유
strictAllowlist: true를 통해 작성된 도메인 이외에는 나갈 수 없습니다. 나열된 것은 npm, Composer, Node, WordPress.org, GitHub뿐입니다.
이는 '수상한 사이트에 연결되지 않게 하기 위함'이라기보다, 의존성(Dependency) 취득처를 고정하기 위함입니다. 설정의 다른 곳에도 적어두었지만, 저에게는 프라이빗 레지스트리(Private registry)가 없습니다. 따라서 공개 레지스트리 이외를 가리키는 설치는 그 자체로 이상 징후가 됩니다.
git과 gh를 샌드박스에서 제외한 이유
"excludedCommands": ["docker", "git", "gh"]
솔직히 말하면 이것은 타협입니다. git과 gh는 인증 정보나 외부 통신을 사용하기 때문에 샌드박스 내부에서는 제대로 작동하지 않습니다. 작동하지 않는 것을 억지로 넣으면 결국 샌드박스 자체를 끄게 됩니다.
그래서 이 세 가지는 밖으로 빼고, 대신 permissions와 후술할 훅으로 묶는 형식을 취했습니다. 모든 것을 하나의 층으로 지키려 하지 않는다는 것이 3개 층으로 나눈 이유이기도 합니다.
인증 정보를 명시적으로 떨어뜨리기
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
...
~/.subversion/auth
를 포함한 이유는 WordPress.org의 배포가 SVN이기 때문입니다. 이곳의 인증 정보(credentials)를 읽을 수 있다면 릴리스(release)를 할 수 있습니다. 릴리스는 모든 사용자에게 배포되며, 취소할 수 없습니다. 저에게 있어 가장 되돌릴 수 없는 작업입니다.
계층 2: permissions (권한)
allow는 「읽기·변환」까지
allow에 넣은 것은 다음과 같은 것들입니다.
cat
grep
find
sed
awk
jq
git status
git diff
npm test
phpunit
phpcs
읽기, 찾기, 변환하기, 테스트하기. 이 단계는 확인(confirmation)을 요청할 가치가 없습니다.
그와 동시에, 외부로 내보내는 경로는 막아두었습니다.
"deny": [
"Bash(curl:*)",
"Bash(wget:*)",
...
sed나 awk로 파일을 읽고 가공하는 것은 허용하되, curl로 전송하는 것은 허용하지 않습니다. 읽기 권한과 반출 경로를 분리해서 생각하고 있습니다.
ask는 4개뿐
"ask": [
"Bash(svn commit:*)",
"Bash(svn ci:*)",
...
확인을 요청하는 것은 이 4가지뿐입니다. 전부 외부에 공개되며, 취소할 수 없는 작업입니다.
확인을 4개로 압축한 것이 이 설정에서 가장 효과적이었습니다. 수가 적기 때문에, 확인이 뜨면 읽게 됩니다. 「확인이 떴다 = 정말 위험하다」라는 신호로 기능하게 되었습니다. 이전에는 확인이 신호가 아니라 노이즈(noise)였습니다.
deny로 우회로를 차단
deny 리스트에서 제가 가장 중요하다고 생각하는 부분은 여기입니다.
"Bash(wp eval:*)",
"Bash(wp eval-file:*)",
"Bash(wp shell:*)"
wp eval은 임의의 PHP를 실행할 수 있습니다. 즉, 다른 wp 명령어를 아무리 세밀하게 제한해도 wp eval이 통과되면 전부 우회할 수 있습니다. wp db drop을 deny에 넣어도, wp eval을 통해 똑같은 일을 할 수 있습니다.
제한을 설계할 때는, 개별적인 위험한 작업보다 먼저, 그 제한을 무효화할 수 있는 범용적인 우회로를 찾는 것이 더 효과적입니다. wp eval, wp shell, eval-file. 이곳을 막아야 비로소 다른 deny가 의미를 갖습니다.
같은 생각으로 이것도 넣어두었습니다.
"Edit(~/.claude/settings.json)",
"Edit(~/.claude/hooks/**)",
"Edit(**/.git/hooks/**)"
설정 자체를 다시 쓸 수 있다면 설정의 의미가 없습니다. 가드레일을 지키는 가드레일입니다.
계층 3: 후크 (hooks)
permissions로는 표현할 수 없는 판정을 셸 스크립트(shell script)로 수행하고 있습니다. 7월 19일 사고 이후에 작성한 것입니다.
#!/usr/bin/env bash
# PreToolUse(Bash) guard
input=$(cat)
...
고민했던 지점: fail-open으로 설정
이 스크립트는 판정에 실패하면 허용합니다 (fail-open). jq가 없더라도, JSON이 깨져 있더라도, exit 0으로 통과시킵니다.
저는 평소 보안 구현 시 반대로 합니다. 판정할 수 없다면 거부합니다 (fail-close). 캐시 플러그인에서도 판정에 실패하면 캐시하지 않는 쪽을 택합니다.
이 부분만 반대로 한 이유는, 이 스크립트의 버그로 인해 세션 전체가 멈추는 것을 피하고 싶었기 때문입니다. 가드의 버그로 인해 모든 명령어가 통하지 않게 되면, 아마 저는 가드 자체를 제거할 것입니다. 제거되고 나면 지킬 것이 아무것도 남지 않게 됩니다.
fail-open을 선택할 수 있는 이유는, 이 계층이 최후의 보루가 아니기 때문이기도 합니다. 이곳이 그냥 통과되더라도 외곽에 샌드박스(sandbox)와 permissions가 있습니다. 다층 구조로 만들어 두면, 내부 계층에서 너무 안전한 쪽으로만 치우치지 않는 선택을 할 수 있습니다. 이 부분은 설계하면서 납득이 간 지점이었습니다.
무엇을 차단하고 있는가
1) 파멸적인 삭제. /, ~, $HOME, 현재 디렉터리, 생(naked) 글로브(glob)
if printf '%s' "$low" | grep -Eq 'rm[[:space:]]+(-[a-z][rf][a-z][[:space:]]+)+(/|~|$home|.|*)([[:space:]]|/|$)'; then
deny "..."
...
`.`
(현재 디렉터리)를 대상으로 포함시킨 것이 핵심입니다. 작업 중인 디렉터리 자체가 플러그인 폴더이므로, `rm -rf .`은 "해당 폴더를 삭제함"을 의미합니다. 7월 19일에 발생한 일은 이와 유사한 형태였습니다.
2) 플러그인 개발 폴더를 지목하는 파괴적 조작
target='(^|[/[:space:]"'"'"])(prime-cache|prime-cache-pro)/?([[:space:]"'"'"]|$)'
destructive='rm[[:space:]]+-[a-z]r|rmdir[[:space:]]|mv[[:space:]]|-delete|-exec[[:space:]]+rm|rsync[[:space:]].--delete|git[[:space:]]+clean'
삭제뿐만 아니라 `mv`도 포함했습니다. 이동 또한 해당 위치에서는 사라지기 때문입니다. `rsync --delete`와 `find -delete` 역시 잊기 쉬운 경로입니다.
그리고, **릴리스 빌드(release build)를 실수로 차단하지 않도록** 하는 장치를 하나 넣었습니다. 패턴의 끝부분을 "슬래시(/) 또는 공백 또는 행 끝"으로 설정했습니다. 따라서 `prime-cache/`는 매칭되지만, `prime-cache.zip`은 매칭되지 않습니다. 빌드 시의 `rm -f prime-cache.zip`은 허용됩니다.
이 부분을 대충 부분 일치(partial match)로 처리하면 일상적인 작업이 중단됩니다. **너무 과하게 막는 가드는, 결국 해제되는 가드입니다.**
##
`$defaults`의 함정
마지막으로, 가장 공유하고 싶은 부분입니다. auto 모드의 규칙을 직접 작성할 때의 이야기입니다.
```json
"autoMode": {
"environment": ["$defaults", "..."],
"soft_deny": ["$defaults", "..."],
...
$defaults는 값이 아니라, 내장 규칙(built-in rules)을 해당 위치에 전개(expand)하는 마커입니다.
여기에 함정이 있습니다. "배열을 쓰지 않는 것"과 "배열을 썼지만 $defaults가 없는 것"은 완전히 다른 동작을 합니다.
4개의 섹션은 독립적으로 평가되므로, environment만 작성하면 나머지는 내장된 상태 그대로 유지됩니다. 하지만 배열을 명시적으로 작성하면서 $defaults를 넣는 것을 잊으면, 해당 섹션의 내장 규칙은 전부 버려집니다.
이것이 무서운 이유는 soft_deny와 hard_deny 때문입니다. $defaults가 없는 soft_deny는 git push --force나 curl | bash에 대한 내장 차단 규칙을 전부 삭제합니다. hard_deny라면 내장된 데이터 유출 방지 및 auto 모드 우회 방지 규칙이 사라집니다.
직접 3줄의 규칙을 추가했다고 생각했지만, 실제로는 **"그 3줄 이외에는 전부 통과"**되는 상태가 됩니다. 게다가 겉보기에는 눈치챌 수 없습니다. 설정이 늘어난 것처럼 보이기 때문입니다.
확인하는 명령어
저장했다면 반드시 이것으로 확인하십시오.
# $defaults가 전개된 후, 실제로 사용되는 규칙을 표시
claude auto-mode config
# 내장 규칙만 표시
...
claude auto-mode config를 한 번만 확인하면 내장 규칙이 사라졌는지 여부를 한눈에 알 수 있습니다. 설정을 작성했을 때가 아니라, 전개된 후를 확인하십시오. 이 과정을 거치지 않고 운용하면 조용히 무방비 상태가 됩니다.
내장 규칙을 추가하는 것이 아니라 **재작성(rewrite)**하고 싶다면, claude auto-mode defaults의 출력을 파일로 저장한 뒤, 그 리스트를 편집하여 다시 붙여넣는 것이 정석입니다.
1개월 운용 결과
확인 작업은 격감했습니다. 일상적인 작업에서 막히는 일이 없어졌기에 작업 템포가 바뀌었습니다.
그리고, 확인 피로(Confirmation fatigue)로 인한 실수가 줄었습니다. 이것이 핵심입니다. 확인 작업이 4개로 압축되어 있으니, 확인이 뜰 때 손이 멈춥니다. 이전에는 확인이 떠도 멈추지 않았습니다. 수가 너무 많아서 몸이 알아서 통과시키고 있었습니다.
즉, 안전성을 높인 것은 확인의 개수가 아니라, 확인의 희소성이었습니다.
다음의 나에게 남기는 메모
- 확인이 많을수록 안전한 것은 아니다. 많으면 읽지 않게 된다. 읽히지 않는 확인은 없는 확인과 같다.
defaultMode: auto로 설정할 수 있는 것은, 확인 이외의 요소로 멈추는 메커니즘이 있을 때뿐이다.autoAllowBashIfSandboxed와failIfUnavailable은 세트다. 후자가 없으면 샌드박스(Sandbox)가 조용히 해제된다.- 제한은 개별적인 위험 작업보다 먼저 "제한을 우회할 수 있는 범용적인 통로"(
wp eval,shell, 설정 파일 자체의 편집)를 막아야 한다. - 읽기 권한과 반출 경로(
curl/wget/nc)는 분리해서 생각한다. - ask(질문)는 압축한다. 취소할 수 없는 작업에만 한정하면, 확인이 하나의 신호(Signal)로서 기능한다.
- 다층(Multi-layered) 구조로 만들어 두면, 안쪽 계층을 fail-open으로 설정할 수 있다. 너무 자주 멈추는 가드는 제거된다.
$defaults를 쓰는 것을 잊은 섹션은 내장 설정이 전부 사라진다.claude auto-mode config로 전개된 후의 내용을 반드시 확인한다.
사고가 난 뒤에 만든 설정이라, 곳곳에 "그때 무슨 일이 일어났었나"가 새겨져 있습니다. rm -rf .의 .을 대상으로 포함했던 것도, prime-cache.zip을 잘못 건드리지 않도록 한 것도 전부 그렇습니다.
설정 파일은 아마도, 그 사람이 무엇을 잃었는지에 대한 기록이라고 생각합니다.
평소에는 raplsworks.com에서 WordPress 플러그인 개발이나 Claude Code 관련 글을 쓰고 있습니다.
Discussion

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