Skip to content

docs+install: #1368 env.PATH fix never landed — every .ts hook still fails with "env: bun: No such file or directory" #1600

Description

@cristbc

Summary

#1368 correctly diagnosed that hooks run in a stripped shell and that settings.json env.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:

  1. HookSystem.md has no stripped-environment section. grep -ciE 'stripped environment|Hook Execution Environment|env\.PATH' over the file returns 0.

  2. install/settings.system.json ships no PATH key in its env block — 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.

  3. 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.

To audit an existing install:

rg '"type":"hook_non_blocking_error"' ~/.claude/projects/**/*.jsonl \
  | jq -r '.attachment | "\(.command)\t\(.stderr)"' | sort | uniq -c | sort -rn

Impact

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Happy to open a PR for items 1 and 3 if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions