From 6bae2ed08644e82edc8eb51a57b86591d50a3c9e Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Wed, 23 Sep 2026 11:16:57 +0200 Subject: [PATCH] fix(scripts): say "not found on PATH", not "not installed" A shell's "command not found" means the name did not resolve on PATH; the tool may still be installed elsewhere or hidden by a stale hash. The hook's advice already tells the model to check `type -P -a` and `hash -r` first, but opened by asserting the opposite. A live run with the installed plugin flagged exactly that: the model noted the claim was not supported, since PATH had been emptied on purpose. The advice now states what the hook knows. The new test fails against the previous wording. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_01NxSeVq1hDnGBCqKLGcjQ6m Agent-Host: 32116e Signed-off-by: Sebastian Mendel --- scripts/detect_missing_tool.py | 10 +++++----- tests/test_detect_missing_tool.py | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/detect_missing_tool.py b/scripts/detect_missing_tool.py index f734e83..dcdf60e 100755 --- a/scripts/detect_missing_tool.py +++ b/scripts/detect_missing_tool.py @@ -2,7 +2,7 @@ """Claude Code hook: point a failed command at the cli-tools catalog. Registered in hooks/hooks.json for PostToolUseFailure and PostToolUse on Bash. -A command that is not installed makes the shell print a "command not found" +A command the shell cannot find on PATH makes it print a "command not found" line; this hook finds it, looks the binary up in catalog/*.json and adds the install command to Claude's context. @@ -63,15 +63,15 @@ def advice(binary: str, root: Path) -> str: entry = catalog_entry(binary, root / "catalog") if entry is None: return ( - f"`{binary}` is not installed and has no entry in the cli-tools catalog. " - "Check `type -P -a` and `hash -r` first; the cli-tools skill lists alternatives " + f"`{binary}` was not found on PATH and has no entry in the cli-tools catalog. " + f"Check `type -P -a {binary}` and `hash -r` first; the cli-tools skill lists alternatives " "and troubleshooting." ) install = root / "scripts" / "install_tool.sh" via = "" if entry == binary else f" (provided by catalog entry `{entry}`)" return ( - f"`{binary}` is not installed{via}. Check `type -P -a {binary}` and `hash -r` first " - f"in case it is only off PATH; otherwise install it with `{install} {entry} install`. " + f"`{binary}` was not found on PATH{via}. Check `type -P -a {binary}` and `hash -r` first " + f"in case it is installed elsewhere or the shell cached a stale path; otherwise install it with `{install} {entry} install`. " "The cli-tools skill covers the rest of the workflow." ) diff --git a/tests/test_detect_missing_tool.py b/tests/test_detect_missing_tool.py index ccec4ca..ca4544a 100644 --- a/tests/test_detect_missing_tool.py +++ b/tests/test_detect_missing_tool.py @@ -75,6 +75,13 @@ def test_failure_names_the_catalog_entry_and_the_installer(self): assert "catalog entry `ripgrep`" in context assert f"{PROJECT_ROOT / 'scripts' / 'install_tool.sh'} ripgrep install" in context + def test_says_what_it_knows_not_what_it_infers(self): + # A shell reports "not found on PATH"; the tool may still be installed + # somewhere else, which is why the advice checks `type -P -a` first. + context = hook.context_for(_failure("bash: rg: command not found")) + assert "not found on PATH" in context + assert "not installed" not in context + def test_uncataloged_command_says_so(self): context = hook.context_for(_failure("bash: zzq: command not found")) assert "no entry in the cli-tools catalog" in context