Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions scripts/detect_missing_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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."
)

Expand Down
7 changes: 7 additions & 0 deletions tests/test_detect_missing_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading