From 924dd3ee4d043a81a0a8adcf21f85ffe91268b12 Mon Sep 17 00:00:00 2001 From: Noah Horton Date: Thu, 16 Apr 2026 12:03:19 -0600 Subject: [PATCH] fix: plugin hook scripts must fall back to uvx when deepwork not on PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-user installs launch the MCP server via `uvx deepwork serve`, so the bare `deepwork` binary is not on PATH when hooks fire. Three plugin hooks (`post_commit_reminder.sh`, `deepschema_write.sh`, `post_compact.sh`) were calling `deepwork ...` directly and failing with exit 127 on every Bash tool use — Claude Code reports them as failed PostToolUse hooks. This regression was introduced in PR #361 when the bash-only commit reminder was rewritten to delegate to a Python hook via the deepwork CLI. The dev workflow masked the bug because direnv/Nix puts `.venv/bin` on PATH. Fix: each hook now probes `command -v deepwork` and falls back to `uvx deepwork ...` when missing. Adds a `.deepreview` rule (`claude_plugin_hook_deepwork_invocation` in plugins/claude/.deepreview) that flags any plugin hook script with an unguarded `deepwork` invocation, to prevent future regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 4 +++ plugins/claude/.deepreview | 35 ++++++++++++++++++++ plugins/claude/hooks/deepschema_write.sh | 8 ++++- plugins/claude/hooks/post_commit_reminder.sh | 8 ++++- plugins/claude/hooks/post_compact.sh | 17 +++++++--- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 646f9205..f0097c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- New `claude_plugin_hook_deepwork_invocation` review rule in `plugins/claude/.deepreview` that flags plugin hook scripts which call bare `deepwork` without a `uvx deepwork` fallback + ### Changed ### Fixed +- Plugin hook scripts (`post_commit_reminder.sh`, `deepschema_write.sh`, `post_compact.sh`) now fall back to `uvx deepwork` when the bare `deepwork` binary is not on PATH. End-user installs launch the MCP server via `uvx deepwork serve`, so `deepwork` is not available as a command — previously these hooks failed with exit 127 on every Bash tool use, and Claude Code reported them as failed PostToolUse hooks (regression introduced in PR #361) + ### Removed ## [0.13.8] - 2026-04-14 diff --git a/plugins/claude/.deepreview b/plugins/claude/.deepreview index 9519678a..8215f224 100644 --- a/plugins/claude/.deepreview +++ b/plugins/claude/.deepreview @@ -1,3 +1,38 @@ +claude_plugin_hook_deepwork_invocation: + description: "Plugin hook scripts must fall back to `uvx deepwork` when the bare binary is not on PATH." + match: + include: + - "hooks/*.sh" + review: + strategy: individual + instructions: | + Plugin hook scripts run in end-user installs where the MCP server is + launched via `uvx deepwork serve` (see plugins/claude/.mcp.json). In + that environment the bare `deepwork` binary is NOT on PATH. Any hook + that calls `deepwork ...` (or `uv run deepwork ...`) directly will + exit 127, and Claude Code reports it as a failed hook on every Bash + tool use. Regression history: PR #361 reintroduced this exact bug. + + For each `deepwork` (or `uv run deepwork`) invocation in this script, + verify it uses a fallback pattern equivalent to: + + ```bash + if command -v deepwork >/dev/null 2>&1; then + echo "${INPUT}" | deepwork hook some_hook + else + echo "${INPUT}" | uvx deepwork hook some_hook + fi + ``` + + A bare `deepwork ...` call without a `command -v deepwork` guard (or + equivalent fallback to `uvx deepwork ...`) is a FAIL. + + Output Format: + - PASS: Every `deepwork` invocation has a `uvx` fallback, or the + script makes no `deepwork` calls. + - FAIL: List each unguarded `deepwork` invocation with its line + number and the suggested fallback edit. + claude_plugin_skill_instructions: description: "PLUG-REQ-001 & REVIEW-REQ-007: Verify skill instructions adequately convey behavioral requirements." match: diff --git a/plugins/claude/hooks/deepschema_write.sh b/plugins/claude/hooks/deepschema_write.sh index 10007d0b..a8a05462 100755 --- a/plugins/claude/hooks/deepschema_write.sh +++ b/plugins/claude/hooks/deepschema_write.sh @@ -1,8 +1,14 @@ #!/usr/bin/env bash # DeepSchema write hook # PostToolUse hook for Write/Edit - validates files against applicable DeepSchemas +# Falls back to `uvx deepwork` so end-user installs (where the plugin's +# MCP server is launched via uvx and `deepwork` is not on PATH) still work. INPUT=$(cat) export DEEPWORK_HOOK_PLATFORM="claude" -echo "${INPUT}" | deepwork hook deepschema_write +if command -v deepwork >/dev/null 2>&1; then + echo "${INPUT}" | deepwork hook deepschema_write +else + echo "${INPUT}" | uvx deepwork hook deepschema_write +fi exit $? diff --git a/plugins/claude/hooks/post_commit_reminder.sh b/plugins/claude/hooks/post_commit_reminder.sh index f36b2eb1..ad85b6e2 100755 --- a/plugins/claude/hooks/post_commit_reminder.sh +++ b/plugins/claude/hooks/post_commit_reminder.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash # Post-commit reminder hook — delegates to deepwork Python hook. +# Falls back to `uvx deepwork` so end-user installs (where the plugin's +# MCP server is launched via uvx and `deepwork` is not on PATH) still work. INPUT=$(cat) export DEEPWORK_HOOK_PLATFORM="claude" -echo "${INPUT}" | deepwork hook post_commit_reminder +if command -v deepwork >/dev/null 2>&1; then + echo "${INPUT}" | deepwork hook post_commit_reminder +else + echo "${INPUT}" | uvx deepwork hook post_commit_reminder +fi exit $? diff --git a/plugins/claude/hooks/post_compact.sh b/plugins/claude/hooks/post_compact.sh index e71f33bf..fc235736 100755 --- a/plugins/claude/hooks/post_compact.sh +++ b/plugins/claude/hooks/post_compact.sh @@ -22,10 +22,19 @@ if [ -z "$CWD" ]; then fi # ==== Fetch active sessions ==== -STACK_JSON=$(deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { - echo '{}' - exit 0 -} +# Fall back to `uvx deepwork` so end-user installs (where the plugin's +# MCP server is launched via uvx and `deepwork` is not on PATH) still work. +if command -v deepwork >/dev/null 2>&1; then + STACK_JSON=$(deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { + echo '{}' + exit 0 + } +else + STACK_JSON=$(uvx deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { + echo '{}' + exit 0 + } +fi # ==== Check for active sessions ==== SESSION_COUNT=$(echo "$STACK_JSON" | jq '(.active_sessions // []) | length')