From b0191a7cbed7bba48f8109854b8a778cdbf1ab26 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 14 May 2026 05:34:29 +0000 Subject: [PATCH] feat(scripts): add library-policy-nudge hook + smoke tests Registers .claude/hooks/library-policy-nudge.sh under PreToolUse/Bash and PostToolUse/Edit|Write|MultiEdit. The hook emits a non-blocking system-reminder pointing at /evaluate-library before runtime deps are added via pnpm add or via direct package.json edits, so policy evaluation happens before the pre-commit gate fires. Co-Authored-By: Claude Sonnet 4.6 --- .claude/hooks/library-policy-nudge.sh | 47 ++++++++++++++ .claude/hooks/library-policy-nudge.test.sh | 73 ++++++++++++++++++++++ .claude/settings.json | 8 +++ 3 files changed, 128 insertions(+) create mode 100755 .claude/hooks/library-policy-nudge.sh create mode 100755 .claude/hooks/library-policy-nudge.test.sh diff --git a/.claude/hooks/library-policy-nudge.sh b/.claude/hooks/library-policy-nudge.sh new file mode 100755 index 0000000..0b817b9 --- /dev/null +++ b/.claude/hooks/library-policy-nudge.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Advisory — nudges the agent to run /evaluate-library before adding runtime +# dependencies. Non-blocking (exit 0). Stdout is injected as system-reminder +# context by the harness. +# +# Dispatches on payload shape: +# .tool_input.command → PreToolUse / Bash (pnpm add / pnpm i ) +# .tool_input.file_path → PostToolUse / Edit|Write (**/package.json edits) + +set -euo pipefail + +input=$(cat) + +# --- PreToolUse / Bash path --- +cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""') +if [[ -n "$cmd" ]]; then + # Match: pnpm add <...> or pnpm i — must have a space after keyword + if [[ "$cmd" =~ (^|[[:space:]])pnpm[[:space:]]+(add[[:space:]]|i[[:space:]]) ]]; then + # Skip dev-dependency installs — no policy evaluation needed for devDeps + if [[ ! "$cmd" =~ (^|[[:space:]])(-D|--save-dev)([[:space:]]|$) ]]; then + cat <<'EOF' +[library-policy-nudge] Runtime dependency detected — evaluate before adding. + +Run the evaluate-library skill first: + /evaluate-library --tier --target + +This ensures the dependency is logged in docs/decisions/ before the pre-commit gate fires. +EOF + fi + fi + exit 0 +fi + +# --- PostToolUse / Edit|Write path --- +file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""') +if [[ "$file_path" == */package.json ]]; then + cat <<'EOF' +[library-policy-nudge] package.json edited — verify any new runtime dependencies are evaluated. + +If you added a runtime dependency, run the evaluate-library skill: + /evaluate-library --tier --target + +This ensures the dependency is logged in docs/decisions/ before the pre-commit gate fires. +EOF +fi + +exit 0 diff --git a/.claude/hooks/library-policy-nudge.test.sh b/.claude/hooks/library-policy-nudge.test.sh new file mode 100755 index 0000000..e861e40 --- /dev/null +++ b/.claude/hooks/library-policy-nudge.test.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Smoke tests for library-policy-nudge.sh +# Usage: bash .claude/hooks/library-policy-nudge.test.sh + +set -euo pipefail + +SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/library-policy-nudge.sh" +MARKER="/evaluate-library" +PASS=0 +FAIL=0 + +assert_contains() { + local name="$1" + local input="$2" + local output + output=$(printf '%s' "$input" | bash "$SCRIPT" 2>/dev/null) + if echo "$output" | grep -qF "$MARKER"; then + echo " PASS: $name" + PASS=$((PASS + 1)) + else + echo " FAIL: $name" + echo " Expected stdout to contain: $MARKER" + echo " Got: ${output:-}" + FAIL=$((FAIL + 1)) + fi +} + +assert_no_output() { + local name="$1" + local input="$2" + local output + output=$(printf '%s' "$input" | bash "$SCRIPT" 2>/dev/null) + if ! echo "$output" | grep -qF "$MARKER"; then + echo " PASS: $name" + PASS=$((PASS + 1)) + else + echo " FAIL: $name" + echo " Expected no $MARKER in stdout, got: $output" + FAIL=$((FAIL + 1)) + fi +} + +echo "library-policy-nudge.sh smoke tests" +echo "------------------------------------" + +# pnpm add → reminder (runtime dep) +assert_contains \ + "pnpm add foo triggers reminder" \ + '{"tool_input":{"command":"pnpm add foo"}}' + +# pnpm add -D → no reminder (dev dep) +assert_no_output \ + "pnpm add -D foo produces no reminder" \ + '{"tool_input":{"command":"pnpm add -D foo"}}' + +# pnpm add --save-dev → no reminder (dev dep, long flag) +assert_no_output \ + "pnpm add --save-dev foo produces no reminder" \ + '{"tool_input":{"command":"pnpm add --save-dev foo"}}' + +# Edit on non-package.json → no reminder +assert_no_output \ + "Edit on feature.manifest.ts produces no reminder" \ + '{"tool_input":{"file_path":"/workspace/packages/auth/src/feature.manifest.ts"}}' + +# Edit on package.json → reminder +assert_contains \ + "Edit on package.json triggers reminder" \ + '{"tool_input":{"file_path":"/workspace/packages/auth/package.json"}}' + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]] diff --git a/.claude/settings.json b/.claude/settings.json index b93dec9..13efb5b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -12,6 +12,10 @@ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/generator-first-nudge.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/library-policy-nudge.sh" } ] } @@ -23,6 +27,10 @@ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-manifest-edit.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/library-policy-nudge.sh" } ] }