src에서 TODO 찾아줘
grep -rn TODO src --include=*.ts
src/app.ts:12: // TODO retry on timeout
SafeZsh for Claude
Claude Code Bash 도구에서 no matches found 같은 zsh 오류를 막습니다. Claude는 bash로 쓰고 Bash 도구는 zsh로 실행하는데, 이 플러그인은 zsh가 그 명령을 bash처럼 읽게 합니다.
이 플러그인이 없으면 위 명령은 no matches found: --include=*.ts로 멈추고 grep은 실행되지 않습니다.
설치
Claude Code 터미널 프롬프트에서:
/plugin install safe-zsh --marketplace M1nu0x0/claude-safe-zshAdd marketplace?에 y를 누르고 scope는 user를 고릅니다.
셸에서는:
claude plugin install safe-zsh --marketplace M1nu0x0/claude-safe-zshClaude Desktop에서는 프롬프트 옆 + → Plugins → Add marketplace에 M1nu0x0/claude-safe-zsh를 넣고 safe-zsh를 추가합니다. 다음 세션부터 적용됩니다.
전과 후
$ grep -rn TODO src --include=*.ts
(eval):1: no matches found: --include=*.ts
$ echo =====; ls
(eval):1: ==== not found
$ T="a.py b.py"; pytest $T
ERROR: file or directory not found: a.py b.py$ grep -rn TODO src --include=*.ts
src/app.ts:12: // TODO retry on timeout
$ echo =====; ls
=====
README.md src
$ T="a.py b.py"; pytest $T
2 passed만든 이유
macOS는 로그인 셸이 zsh라서 Bash 도구도 zsh로 명령을 실행하지만, Claude는 bash 문법으로 명령을 씁니다.
anthropics/claude-code#103027은 12주 동안 Bash 호출 61,040번 중 2,570번에서 zsh 오류를 셌습니다. 9월 하순부터는 1,000번당 51번이었고, 그중 76%는 명령이 0으로 끝나 오류가 출력 중간에 묻힌 채 Claude가 그대로 읽고 넘어갔습니다.
이 이슈가 Claude Code에서 고쳐지면 이 플러그인은 필요 없어집니다. CI가 매일 이슈 상태를 확인합니다.
Claude Code의 안전 검사에도 zsh 문제가 있습니다. ${(flags)...}를 파싱하지 못해 그런 명령을 rm 위험으로 거부합니다(#104925). 이것이 고쳐지면 PreToolUse 훅과 그 안내도 뺍니다. CI가 매일 이 이슈의 상태도 확인합니다.
동작
세션이 시작될 때(startup, resume, clear, compact) SessionStart 훅이 CLAUDE_ENV_FILE에 한 줄을 덧붙입니다. Claude Code는 이 파일을 Bash 도구 명령마다 앞에 붙이므로, 그 줄이 같은 셸에서 실행됩니다.
if [ -n "${ZSH_VERSION:-}" ]; then setopt no_nomatch sh_word_split no_equals; fibash에서는 조건이 거짓이라 0으로 끝나므로, 뒤에 이어진 명령도 그대로 실행됩니다.
셸이 zsh이면 훅이 모델에게 두 줄을 더 알려 줍니다. 명령 안에서 직접 띄운 zsh -c는 이 옵션을 물려받지 않으니 스크립트 맨 앞에 setopt no_nomatch sh_word_split no_equals;를 붙이거나 bash -c를 쓰라는 안내, 그리고 zsh ${(flags)...} 확장은 안전 검사가 실행 전에 거부하니 printf '%s\n' "$PATH" | tr ':' '\n'처럼 POSIX sh로 쓰라는 안내입니다. 모델이 따르지 않을 수 있습니다.
PreToolUse 훅은 Claude Code의 안전 검사보다 먼저 Bash 도구 명령을 읽습니다. 작은따옴표 밖에 ${(s/:/)PATH} 같은 zsh ${(flags)...} 확장이 있으면 거부하고 모델에게 이유를 알립니다. 안전 검사가 실행 전에 rm 위험으로 거부할 것이니 printf '%s\n' "$PATH" | tr ':' '\n'처럼 POSIX sh로 쓰라는 내용입니다. 훅은 아무것도 승인하지 않고, 검사를 피하려고 명령을 zsh -c로 감싸지도 않습니다.
막는 것
no_nomatch--include=*.ts나ls libs/*/README.md처럼 매치 없는 glob이 명령을 멈추지 않고 그대로 넘어갑니다.sh_word_split- 따옴표 없는
$var가 단어로 나뉘어,for f in $list가 항목마다 한 번씩 돕니다. no_equals=로 시작하는 단어를 그대로 둡니다.echo =====가 출력되고,=ls가/bin/ls로 바뀌지 않습니다.${(flags)...}print -l ${(s/:/)PATH}같은 zsh${(flags)...}확장이 엉뚱한dangerous rm operations메시지로 거부되지 않습니다. 훅이 진짜 이유와 함께 거부하고, 모델이 POSIX sh로 고쳐 씁니다.
콜론 modifier($B:path), 첨자로 읽히는 $pat[...], 특수 변수 path와 status, ${!v}나 compgen 같은 bash 전용 문법은 막지 못합니다. 이런 경우는 CLAUDE_CODE_SHELL=/bin/bash로 Bash 도구를 아예 bash로 실행할 수 있습니다. $${처럼 안전 검사가 파싱하지 못하는 다른 구문은 여전히 rm 메시지로 거부됩니다.
관련 이슈
배지는 각 이슈의 현재 상태입니다.
- #103027
Claude가 zsh에서 bash 문법을 계속 씀. 이 플러그인을 만든 이유.
- #67146
같은 문제의 앞선 보고. 활동이 없어 닫힘.
- #103576
단어 분리가 안 돼 pytest가 테스트를 하나도 돌리지 않았고, 빈 결과가 그대로 보고됨.
- #74888
Monitor 도구 예시가 bash 단어 분리를 가정함.
- #101554
zsh에서 bash 내장 명령을 실행함. 이 플러그인으로는 막을 수 없음.
- #74746
Bash 도구를 bash로 실행하게 해 달라는 요청.
CLAUDE_CODE_SHELL로 닫힘.- #7490
Bash 도구가 쓸 셸을 고르거나, Claude Code를 실행한 셸을 물려받게 해 달라는 요청.
- #91498
Bash 도구가 로그인 셸을 실행하니 이름을 바꾸거나 셸 오류를 알려 달라는 제안.
- #52521
Bash도구 이름 변경 제안.- #2444
Bash 도구가 bash가 아니라 기본 셸에서 실행된다는 보고.
- #24573
bash 사용자인데도 Bash 도구가 zsh를 고름.
- #104925
rm이 없는 명령도 zsh${(flags)...}처럼 파싱에 실패하면rm위험으로 거부됨. PreToolUse 훅을 만든 이유.- #99249
zsh -c스크립트도 bash 문법으로 판단되는 것으로 보임.- #100419
PermissionRequest 훅 입력에 거부 사유가 없어 원인을 구분할 수 없음.
- #99899
서브에이전트에서는 이 승인 창에 자동 거부 시간이 없어 무기한 멈춤.
자주 묻는 질문
내 터미널이나 ~/.zshrc가 바뀌나요?
아니요. 옵션은 Bash 도구의 셸에만 적용됩니다. 터미널, dotfile, 명령 안에서 다시 띄운 zsh -c에는 적용되지 않습니다. 마지막 경우에는 훅이 모델에게 띄우는 방법만 알려 줍니다.
bash를 쓰는데 뭔가 하나요?
거의 없습니다. bash에서는 setopt 줄의 조건이 거짓이라 0으로 끝납니다. PreToolUse 훅은 zsh ${(flags)...} 확장이 있는 명령을 여전히 거부하는데, 이런 명령은 bash에서도 실행되지 않습니다.
그냥 CLAUDE_CODE_SHELL=/bin/bash를 쓰면 되지 않나요?
그것도 되고 더 많이 해결되지만, Bash 도구에서 내 zsh 함수와 설정을 쓸 수 없게 됩니다. 이 플러그인은 zsh를 그대로 두고 가장 자주 깨지는 세 가지만 끕니다.
어디로 데이터를 보내나요?
보내지 않습니다. SessionStart 훅은 Claude Code가 준 파일에 한 줄을 덧붙이고 모델에게 고정된 두 줄을 출력합니다. PreToolUse 훅은 입력으로 받은 Bash 명령을 읽고, zsh ${(flags)...} 확장이 있으면 고정된 거부 메시지를 출력합니다. 둘 다 네트워크 요청은 하지 않습니다.
어떤 OS에서 확인했나요?
Ubuntu와 macOS에서 CI가 돌고, 매일 최신 Claude Code로도 확인합니다.
제거하려면 어떻게 하나요?
셸에서 claude plugin uninstall safe-zsh를 실행합니다.
(eval):1: no matches found는 왜 나오나요?
(eval)은 Bash 도구의 zsh가 명령을 eval로 실행하다 난 오류라는 뜻입니다. zsh에서는 맞는 파일이 없는 glob이 오류라서 명령 전체가 거기서 멈추고, bash는 그 단어를 그대로 넘깁니다. 이 플러그인은 zsh도 bash처럼 그대로 넘기게 합니다(no_nomatch). 터미널에서는 같은 오류가 zsh: no matches found로 보입니다.
==== not found는 뭔가요?
zsh는 =로 시작하는 단어를 그 이름의 명령 경로로 바꿉니다(=ls는 /bin/ls). 그래서 echo =====는 ====라는 명령을 찾다가 실패합니다. 구분선으로 =를 늘어놓아 출력하는 명령이 여기에 걸립니다. 이 플러그인은 이 동작을 끕니다(no_equals).
rm이 없는데 왜 could not be checked for dangerous rm operations로 거부되나요?
Claude Code의 안전 검사는 셸이 실행하기 전에 명령을 파싱하는데, 그 bash 문법에는 ${(s/:/)PATH} 같은 zsh ${(flags)...} 확장이 없습니다. 파싱하지 못한 명령은 rm이 없어도 rm 위험으로 거부됩니다(#104925). zsh가 시작되지도 않으니 setopt로는 막을 수 없습니다. 이 플러그인의 PreToolUse 훅이 먼저 거부하고, 모델에게 printf '%s\n' "$PATH" | tr ':' '\n'처럼 POSIX sh로 쓰라고 알립니다.