올가을, Claude Code의 Issue와 Reddit에 에이전트가 스스로 조합한 삭제 명령어로 데이터를 지워버렸다는 보고가 연이어 올라왔습니다. 이들 모두 모델이 특별히 이상한 행동을 한 것이 아니라, 셸(shell)에서 흔히 발생하는 함정을 그대로 밟은 경우입니다. 원인별로 5건의 사례를 정리하고, 사람이 작성하는 명령어에도 바로 적용할 수 있는 예방법을 모았습니다.
#109735 (Windows 및 WSL)에서는 서브 에이전트가 다음 명령어를 실행했습니다.
wsl -d Ubuntu-24.04 -- bash -lc 'set -u; R=~/some-dir; mkdir -p $R; rsync -a --delete /mnt/d/<project>/ $R/'
변수 할당은 싱글 쿼트 안에 있으며, set -u도 붙어 있습니다. 그럼에도 불구하고 명령어 라인은 wsl.exe --를 거치는 과정에서 한 번 셸에 해석되었고, 이때의 R은 미정의 상태였습니다. $R/는 /가 되었고, 실제로 실행된 것은 rsync -a --delete /mnt/d/<project>/ /였습니다. WSL을 통해 지워진 파일은 휴지통으로 가지 않습니다.
rm -rf "$D"/ 역시 쿼트(quote) 처리했더라도 동일하며, $D가 비어 있다면 rm -rf /가 됩니다. --preserve-root는 / 그 자체만 보호하므로, /*나 /mnt/d/는 통과합니다.
효과적인 방법은 빈 상태일 때 셸을 멈추게 하는 구문입니다.
rm -rf "${D:?}/"
rsync -a --delete src/ "${R:?}/"
${D:?}는 비어 있거나 미정의일 때 대체하지 않고 오류로 멈추며, 몇 단계를 거치는 셸을 통하더라도 그 자리에서 효과를 발휘합니다.
#95426 (Windows 및 Git Bash)에서는 테스트 후 정리 작업으로 다음 명령어가 실행되었습니다.
rm -rf "$(cygpath -u 'C:꿺me')" 2>/dev/null
cygpath -u 'C:꿺me'는 /c/ 즉 드라이브 전체를 반환합니다. 에러 출력을 버렸기 때문에 화면에는 아무것도 표시되지 않았고, 약 35분 만에 600GB가 삭제되었습니다. 이 경우에는 값이 비어있지 않으므로 ${D:?}로도 멈추지 않습니다. 삭제하기 전에 변수에 한 번 넣어 echo를 실행하여 프로젝트 내부를 가리키는지 확인해야 합니다. 끝에 고정된 서브 폴더를 붙인 "${D:?}/sub"라면, 루트가 반환되더라도 지워지는 것은 그 서브 폴더뿐입니다.
r/ClaudeCode에서 4,000점을 넘긴 게시물에서는 macOS의 ~/Library 전체가 사라졌습니다. 에이전트는 ~/Library/Application Support/ 아래에 자신이 만든 테스트용 폴더를 지우려 했고, xargs -n1 dirname으로 부모 폴더를 찾았습니다. Application Support의 공백 때문에 xargs가 경로를 두 개로 나누었고, dirname이 ~/Library를 반환하여 그것이 그대로 rm -rf에 전달되었습니다.
xargs는 기본적으로 공백과 줄바꿈으로 구분하며 쿼트도 해석합니다. 삭제에 사용할 때는 find ... -print0 | xargs -0 rm -rf -- 또는 find ... -delete를 사용해야 합니다.
#107274 (macOS)에서는 rm -rf가 리포지토리의 .git을 지워, 어디에도 push하지 않았던 12개월 분량의 이력이 손실되었습니다. 체크아웃(checkout)을 정리하려면 git worktree remove <path>를 사용하고, 다시 만들려면 mv .git .git.bak로 옆에 두는 것이 안전합니다.
r/ClaudeAI의 11월 10일 게시물에서는 Windows에서 오래된 git worktree 두 개를 정리하는 과정에서, 워크트리 안에 있던 디렉토리 션킹(junction)의 목적지, 즉 실제 models 폴더와 .venv까지 지워져 약 230GB가 손실되었습니다. 션킹이나 심볼릭 링크를 포함하는 폴더는 링크 자체를 먼저 제거한 후에 삭제해야 합니다 (Windows의 경우 cmd /c rmdir <링크>).
프롬프트나 CLAUDE.md에 "변수를 사용해 rm -rf를 하지 마세요"라고 적더라도, 그것은 요청일 뿐 검사는 아닙니다. 그래서 실행 전에 명령어 문자열을 읽고 멈추게 하는 PreToolUse 후크 모음인 claude-bulkhead를 만들었습니다 (MIT, 의존성 없음. 이 가드는 Codex, Gemini CLI, Copilot CLI, Cursor, opencode, Pi, Qwen Code, Kimi Code 등에서도 작동합니다). 0.4.0과 0.5.0 버전에서는 위 5가지 패턴을 막는 규칙을 추가했습니다. xargs의 경우, -0이 없는 xargs가 rm -r를 실행하는 파이프라인을, 심볼릭 링크(junction)의 경우 폴더 외부를 가리키는 링크를 포함하는 워크트리(worktree)에 대한 git worktree remove --force와 PowerShell의 Remove-Item -Recurse를 막습니다. 막았을 때는 이유와 안전한 작성 방법(대부분 "${VAR:?}")을 에이전트에게 반환하므로, 에이전트는 그대로 수정하여 작업을 계속합니다.
설치하지 않고도 특정 명령어가 실행 없이 막히는지 확인할 수 있습니다.
npx -y claude-bulkhead explain -- 'rsync -a --delete src/ $R/'
지난 30일간의 Claude Code 기록에서 막혔어야 할 명령어를 셀 수도 있습니다. ~/.claude/projects를 읽기만 하면 외부로 아무것도 전송하지 않습니다.
npx -y claude-bulkhead scan --since 30d --share
Claude Code에서는 /plugin marketplace add dooly6276/claude-bulkhead와 /plugin install bulkhead@bulkhead로 설치할 수 있습니다. 명령어 문자열을 읽는 방식이므로, 에이전트가 작성한 스크립트 파일 내부의 삭제까지는 볼 수 없습니다. 스크립트 내에서도 "${D:?}"를 사용하는 것이 가장 확실합니다.