When Hook Scripts Die Silently — Three macOS Traps A developer documented three silent failure traps in Claude Code hook scripts on macOS, including zsh's read-only 'status' variable, missing PATH when launched from the GUI, and the absence of the GNU 'timeout' command. The post recommends using alternative variable names, setting env.PATH in settings.json, and avoiding timeout in hooks. This is part of the "Claude Code environment" series, following on from Auto-syncing frontmatter and the index for Claude Code agents https://zenn.dev/bokuwalily/articles/agents-frontmatter-index . You wrote a hook script, but no notification ever arrives. No error. Nothing in the logs either. When you have an LLM write a Claude Code hook as a zsh script on macOS, this is what happens. The cause is one of three traps, different every time, and what they all share is that they fail silently . On 2026-07-18, failure notifications stopped arriving from the Stop hook in the note-autolike project. The script exits with exit 0. Claude Code isn't emitting any errors. Tracing with bash -x shows that the branches never satisfy their conditions at all. Digging into the cause turned up three separate traps, all hit at once. status is a read-only reserved variable ❌ zsh では status は read-only 予約変数 status=$? if $status -ne 0 ; then notify failure ← 永遠に呼ばれない fi status=$? is silently ignored, with no error . In zsh, status is reserved as an alias for the exit code of the last command, and cannot be assigned to. Since it works fine in bash, you won't notice when you port a script written for bash over to .zsh . echo ${ t status} → "integer-readonly-special" ← special を含めば代入禁止 echo ${ t rc} → "" ← 未定義 = 安全に使える zsh の read-only 変数一覧 typeset -r | grep '=' ✅ rc / exit code / rc など予約されていない名前を使う rc=$? if $rc -ne 0 ; then notify failure fi 関数化するならこの形が安全 run and check { "$@" local rc=$? $rc -ne 0 && echo "ERROR: $ returned $rc" return $rc } Note The main non-assignable variables in zsh: status / signals / commands / options / aliases / functions / modules / history . When porting a script from bash, scan it for these first. grep -n '\bstatus=' your script.zsh grep -n '\bsignals=& 124;\bcommands=& 124;\boptions=& 124;\baliases=' your script.zsh When Claude Code .app is launched from the GUI and a hook runs, the process does not read ~/.zshrc . The PATH handed down to child processes is just the bare minimum. /usr/bin:/bin:/usr/sbin:/sbin Neither node under nvm nor any of the Homebrew tools are on this minimal PATH. A hook that calls node "...mjs" terminates silently with node: command not found every time. Reproduce how things look to a child process launched from the GUI: env -i HOME="$HOME" /bin/sh -c 'PATH="/usr/bin:/bin"; node --version' → sh: node: command not found Add env.PATH at the top level of ~/.claude/settings.json so it's inherited by hooks: { "env": { "PATH": "~/.nvm/versions/node/