You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#1368 correctly diagnosed that hooks run in a stripped shell and that settings.jsonenv.PATH is required. It was closed 2026-06-21, but neither half of its proposed fix landed. On current main (v7.1.1), every .ts hook still fails on any install where bun is not in the default non-interactive PATH — which is the normal case for the official ~/.bun/bin install.
The failure is completely silent in the UI. It only appears if you go read the session transcripts.
Evidence the fix never landed
On current main:
HookSystem.md has no stripped-environment section.grep -ciE 'stripped environment|Hook Execution Environment|env\.PATH' over the file returns 0.
The docs contradict themselves on the workaround.HookSystem.md line 39 states hooks are referenced "directly (e.g., $HOME/.claude/hooks/Safety.hook.ts) without a bun prefix", while the troubleshooting section at line ~953 says "Is path correct in settings.json? Use bun $HOME/.claude/hooks/..." and claims "all LifeOS hooks use bun prefix". install/hooks/hooks.json uses the bare form for everything except HookHealer.
Reproduction
echo'{"session_id":"probe","transcript_path":"/tmp/probe.jsonl","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo hi"}}'> /tmp/probe.json
# A) the shebang form used by hooks.json for nearly every hook
env -i HOME="$HOME" /bin/sh -c "$HOME/.claude/hooks/PreToolGuard.hook.ts < /tmp/probe.json"# -> env: bun: No such file or directory# B) the `bun` prefix the troubleshooting section recommends
env -i HOME="$HOME" /bin/sh -c "bun $HOME/.claude/hooks/PreToolGuard.hook.ts < /tmp/probe.json"# -> /bin/sh: bun: command not found
Both strings appear verbatim in real transcripts as attachment.stderr on hook_non_blocking_error entries.
The bun prefix is not the fix
HookHealer is deliberately registered as bun $HOME/.claude/hooks/HookHealer.hook.ts — per HookSystem.md line 74, "so it is immune to losing its own exec bit." It fails too, with form (B) above. The prefix protects against a missing exec bit; it does nothing about a missing PATH, because bun itself is what can't be resolved. Only env.PATH fixes both forms.
Why it stays invisible
Hooks that exit 0 with empty output are not persisted to transcripts, so there is no "before" baseline to compare against — a hook that has never once succeeded looks the same as a quiet, healthy one. Guarded shell hooks make it worse: ContextReduction.hook.sh does command -v rtk || exit 0, so it exits clean and logs nothing while doing nothing, which is exactly how #1368 was originally found.
This is not only cosmetic. PreToolGuard.hook.ts is the consolidated Bash|Write|Edit|MultiEdit blocking guard — it dispatches SystemFileGuard, CommunicationSkillGuard, and EgressClassGuard. On an affected install it never executes, so all three security checks are silently absent while settings.json still shows them wired. On the install where I found this, it had 63 recorded failures and zero recorded successes.
Same story for EventLogger, MemoryTurnStart, LoadContext, ISASync, CheckpointPerISC, StopGates, VoiceCompletion, and the rest of the .ts hook layer.
Proposed fix
Write the HookSystem.md section docs: hooks run in stripped environment — settings.json env.PATH is a required but undocumented field #1368 asked for — hooks run in a stripped shell, env.PATH is required, and the value must be a literal absolute string ($HOME / ${HOME} do not expand there). Worth stating explicitly that env.PATH also becomes the PATH for the session's Bash tool calls, so it should be a superset of the user's interactive PATH rather than a minimal list.
Have the installer write env.PATH into settings.json, resolved at install time from the user's login shell (zsh -lc 'echo $PATH' or equivalent) rather than hardcoded, filtering volatile per-session temp directories. A generic default is not enough here — bun lives in ~/.bun/bin for the official installer but in Homebrew or a version manager elsewhere.
Reconcile lines 39 and ~953 so the bun-prefix guidance says one thing, and add a note that the prefix does not substitute for env.PATH.
Optional but high-value: have Doctor check that every command hook's interpreter resolves under the configured env.PATH, and surface hook_non_blocking_error counts from recent transcripts. That turns a silent failure into a reported one.
Summary
#1368 correctly diagnosed that hooks run in a stripped shell and that
settings.jsonenv.PATHis required. It was closed 2026-06-21, but neither half of its proposed fix landed. On currentmain(v7.1.1), every.tshook still fails on any install wherebunis not in the default non-interactive PATH — which is the normal case for the official~/.bun/bininstall.The failure is completely silent in the UI. It only appears if you go read the session transcripts.
Evidence the fix never landed
On current
main:HookSystem.mdhas no stripped-environment section.grep -ciE 'stripped environment|Hook Execution Environment|env\.PATH'over the file returns 0.install/settings.system.jsonships noPATHkey in itsenvblock — so a fresh install has exactly the broken configuration docs: hooks run in stripped environment — settings.json env.PATH is a required but undocumented field #1368 describes.The docs contradict themselves on the workaround.
HookSystem.mdline 39 states hooks are referenced "directly (e.g.,$HOME/.claude/hooks/Safety.hook.ts) without abunprefix", while the troubleshooting section at line ~953 says "Is path correct in settings.json? Usebun $HOME/.claude/hooks/..." and claims "all LifeOS hooks usebunprefix".install/hooks/hooks.jsonuses the bare form for everything exceptHookHealer.Reproduction
Both strings appear verbatim in real transcripts as
attachment.stderronhook_non_blocking_errorentries.The
bunprefix is not the fixHookHealeris deliberately registered asbun $HOME/.claude/hooks/HookHealer.hook.ts— perHookSystem.mdline 74, "so it is immune to losing its own exec bit." It fails too, with form (B) above. The prefix protects against a missing exec bit; it does nothing about a missing PATH, becausebunitself is what can't be resolved. Onlyenv.PATHfixes both forms.Why it stays invisible
Hooks that exit 0 with empty output are not persisted to transcripts, so there is no "before" baseline to compare against — a hook that has never once succeeded looks the same as a quiet, healthy one. Guarded shell hooks make it worse:
ContextReduction.hook.shdoescommand -v rtk || exit 0, so it exits clean and logs nothing while doing nothing, which is exactly how #1368 was originally found.To audit an existing install:
Impact
This is not only cosmetic.
PreToolGuard.hook.tsis the consolidatedBash|Write|Edit|MultiEditblocking guard — it dispatchesSystemFileGuard,CommunicationSkillGuard, andEgressClassGuard. On an affected install it never executes, so all three security checks are silently absent whilesettings.jsonstill shows them wired. On the install where I found this, it had 63 recorded failures and zero recorded successes.Same story for
EventLogger,MemoryTurnStart,LoadContext,ISASync,CheckpointPerISC,StopGates,VoiceCompletion, and the rest of the.tshook layer.Proposed fix
Write the
HookSystem.mdsection docs: hooks run in stripped environment — settings.json env.PATH is a required but undocumented field #1368 asked for — hooks run in a stripped shell,env.PATHis required, and the value must be a literal absolute string ($HOME/${HOME}do not expand there). Worth stating explicitly thatenv.PATHalso becomes the PATH for the session's Bash tool calls, so it should be a superset of the user's interactive PATH rather than a minimal list.Have the installer write
env.PATHintosettings.json, resolved at install time from the user's login shell (zsh -lc 'echo $PATH'or equivalent) rather than hardcoded, filtering volatile per-session temp directories. A generic default is not enough here —bunlives in~/.bun/binfor the official installer but in Homebrew or a version manager elsewhere.Reconcile lines 39 and ~953 so the
bun-prefix guidance says one thing, and add a note that the prefix does not substitute forenv.PATH.Optional but high-value: have
Doctorcheck that everycommandhook's interpreter resolves under the configuredenv.PATH, and surfacehook_non_blocking_errorcounts from recent transcripts. That turns a silent failure into a reported one.Happy to open a PR for items 1 and 3 if useful.