feat(scripts): hook that points a missing command at the catalog - #159
Conversation
When a Bash call fails with "command not found", the plugin hook adds the catalog entry that provides the binary and the install command to Claude's context: `rg` -> catalog entry `ripgrep`, `install_tool.sh ripgrep install`. The cli-tools-skill fork had such a hook, and it could never have fired. Measured against the Claude Code hooks reference and by feeding it the documented payloads: - it was registered for PostToolUse only, which fires after a successful call; a missing command exits 127 and fires PostToolUseFailure; - it read `output`/`stdout`/`stderr` at the top level, where the input has `error` (PostToolUseFailure) or `tool_response.stderr` (PostToolUse); - it printed to stdout, which these events do not pass to the model; the text has to go into hookSpecificOutput.additionalContext. Given either documented payload it prints nothing. This hook listens on both events: PostToolUseFailure reads `error`, and PostToolUse reads `tool_response.stderr` for a missing command inside a pipeline or list that still succeeded as a whole. It matches only the lines shells print -- bash, zsh (including `(eval):1:`) and dash forms -- not the phrase anywhere in the output, looks the binary up by file name and `binary_name` in catalog/*.json, and fails open on any error. tests/test_detect_missing_tool.py covers each message form, the real stderr of bash, sh and (where installed) zsh, both payload shapes, the JSON output and the hooks.json registration. With the fork's script in place, the output test fails: it emits nothing. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_01NxSeVq1hDnGBCqKLGcjQ6m Agent-Host: 32116e Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
Warning Review limit reachedNext included review available in 45 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Review before mergeCodeRabbit refused this head as rate limited and Copilot is out of quota, so the review of Live, in a real session. With this branch rebased locally onto
So Code. Only lines shells print are matched, never the phrase anywhere in the output. A command name must start alphanumeric, so paths such as 21 checks pass, 0 fail; 0 unresolved threads; full suite 1019 passed / 2 skipped locally. Assisted by claude-code:claude-opus-5-5 — Session |
|
Self-review: bae273c The bot review this pull request demands is unsatisfiable (Copilot quota wall or repeated bot failures on this head). The diff on this head was reviewed by the PR author; this comment is the on-the-record attestation the merge gate reads back. It stops matching on the next push. |
Merging this documents two things the `cli-tools` move had to work out by hand. A new `references/plugin-hooks.md` covers how a plugin hook actually reaches the model. A **Relocation** section in `skill-retirement.md` covers moving a skill to another repository instead of retiring it. **Plugin hooks.** `cli-tools-skill`'s hook could never have fired, and no check noticed: - It was registered for `PostToolUse`, which a failed command does not trigger; `PostToolUseFailure` does. - It read keys the input does not carry. - It printed to stdout, which the model never sees. The page states which event to pick, where each event carries the text, and that only `hookSpecificOutput.additionalContext` reaches the model. It also gives a live check with `claude -p --plugin-dir` that unit tests cannot replace; that check is how the replacement hook ([coding_agent_cli_toolset#159](netresearch/coding_agent_cli_toolset#159)) was verified. **Relocation.** The section gives the order: port first, verify the new plugin live, then decide the version source. Without `version`, Claude Code versions the plugin by commit SHA, and such a repository does not belong in the release fleet (#346). After that come repointing, the moved notice and archiving. Existing installations switch over only after `claude plugin marketplace update` followed by `plugin update`; measured on `cli-tools`, 1.9.2 → commit SHA. `validate-skill.sh .` reports 0 errors. _Assisted by claude-code:claude-opus-5-5 — [Session](https://claude.ai/code/session_01NxSeVq1hDnGBCqKLGcjQ6m)_ https://claude.ai/code/session_01NxSeVq1hDnGBCqKLGcjQ6m



Merging this adds the
cli-toolsplugin's hook. When a Bash call fails withcommand not found, Claude gets the catalog entry that provides the binary and the exact install command in its context (rg→ catalog entryripgrep→…/scripts/install_tool.sh ripgrep install).Why a new hook rather than the fork's.
netresearch/cli-tools-skillshipped one, and against the hooks reference it could never have fired:PostToolUseonly, which fires after a successful call. A missing command exits 127 and firesPostToolUseFailure.output/stdout/stderrat the top level of the input. The failure text actually arrives inerror(PostToolUseFailure) or intool_response.stderr(PostToolUse).hookSpecificOutput.additionalContext.Given either documented payload, the fork's script prints nothing; that was measured, not inferred.
How this one works.
hooks/hooks.jsonregistersscripts/detect_missing_tool.pyfor both events onBash:PostToolUseFailurereadserror.PostToolUsereadstool_response.stderr. This covers a missing command inside a pipeline or list whose exit status is still 0.The script matches only the lines shells print: bash, zsh (including
(eval):1:) and dash forms, never the phrase anywhere in the output. It looks the binary up by file name and bybinary_nameincatalog/*.json. It uses only the standard library and fails open on any error, so it cannot break the call it observes.Tests.
tests/test_detect_missing_tool.pycovers:bash,shand (where installed)zsh;PostToolUsereads stderr only;hooks.jsonregistration.With the fork's script in place, the output test fails: it emits nothing. Full suite 1019 passed / 2 skipped.
Independent of #158, which adds the plugin manifest and skill; the hook is loaded once both are on
mainand the plugin is installed from this repository.Assisted by claude-code:claude-opus-5-5 — Session
https://claude.ai/code/session_01NxSeVq1hDnGBCqKLGcjQ6m