{"slug": "anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks", "title": "Anti-deferral hook.  IMPORTANT: The .sh file in expects to live in (project dir)/.claude/hooks/ and expects the .txt files to live in the lib subdir at (project dir)/.claude/hooks/lib/.", "summary": "A developer has created an anti-deferral hook script that scans the last assistant message in a transcript for banned deferral phrases and blocks responses containing them. The script, designed to live in a project's `.claude/hooks/` directory with supporting `.txt` files in a `lib` subfolder, catches chat-response failure modes where an assistant types phrases like \"we can revisit this\" without filing an operational change. The hook skips lines inside fenced code blocks and honors a dismiss sentinel for meta-discussion of the rule itself.", "body_md": "|\n#!/bin/bash |\n|\n# |\n|\n# IMPORTANT: |\n|\n# * This must be copied to (project dir)/.claude/hooks/ |\n|\n# * The txt files must be copied to the lib subfolder: (project dir)/.claude/hooks/lib/ |\n|\n# * Customize the feedback claude gets if deferral phrases are detected, by editing the |\n|\n# echo section at the bottom. |\n|\n# |\n|\n# response-deferral-phrase-gate.sh |\n|\n# |\n|\n# Stop hook -- scans the last assistant message in the transcript for banned |\n|\n# deferral phrases and exits 2 (blocking) if any are found. |\n|\n# |\n|\n# Catches the chat-response failure mode: typing \"we can revisit this\" or |\n|\n# \"or accept the slight gap\" in a reply without filing a real opsx change. |\n|\n# |\n|\n# Scope: |\n|\n# - Scans only the LAST assistant message |\n|\n# - Skips lines inside fenced code blocks (so quoting the rule is fine) |\n|\n# - Honors a dismiss sentinel: `<!-- deferral-meta -->` anywhere in the |\n|\n# response, for meta-discussion of the rule itself |\n|\n|\n|\nset -uo pipefail |\n|\n|\n|\nINPUT=$(cat) |\n|\nTRANSCRIPT=$(echo \"$INPUT\" | jq -r '.transcript_path // empty' 2>/dev/null || echo \"\") |\n|\n|\n|\nif [[ -z \"$TRANSCRIPT\" ]] || [[ ! -f \"$TRANSCRIPT\" ]]; then exit 0; fi |\n|\n|\n|\nREPO_ROOT=\"${CLAUDE_PROJECT_DIR:-$(pwd)}\" |\n|\n|\n|\n# Load banned phrases + the \"pre-existing\" dismissal regex from data files |\n|\n# shared with the archive-gate and write-gate hooks. |\n|\ndeclare -a BANNED_PHRASES=() |\n|\nwhile IFS= read -r LINE; do |\n|\nLINE=\"${LINE%%#*}\" |\n|\nLINE=\"${LINE#\"${LINE%%[![:space:]]*}\"}\" |\n|\nLINE=\"${LINE%\"${LINE##*[![:space:]]}\"}\" |\n|\n[[ -z \"$LINE\" ]] && continue |\n|\nBANNED_PHRASES+=(\"$LINE\") |\n|\ndone < \"$REPO_ROOT/.claude/hooks/lib/deferral-banned-phrases.txt\" |\n|\nPRE_EXISTING_DISMISSAL_REGEX=\"\" |\n|\nwhile IFS= read -r LINE; do |\n|\nLINE=\"${LINE%%#*}\" |\n|\nLINE=\"${LINE#\"${LINE%%[![:space:]]*}\"}\" |\n|\nLINE=\"${LINE%\"${LINE##*[![:space:]]}\"}\" |\n|\n[[ -z \"$LINE\" ]] && continue |\n|\nPRE_EXISTING_DISMISSAL_REGEX=\"$LINE\" |\n|\nbreak |\n|\ndone < \"$REPO_ROOT/.claude/hooks/lib/deferral-pre-existing-regex.txt\" |\n|\n|\n|\n# Find the last assistant message JSONL line in the transcript. |\n|\nLAST_ASSISTANT_LINE=$(tac \"$TRANSCRIPT\" 2>/dev/null | while IFS= read -r LINE; do |\n|\nROLE=$(echo \"$LINE\" | jq -r '(.message.role // .role) // empty' 2>/dev/null || echo \"\") |\n|\nif [[ \"$ROLE\" == \"assistant\" ]]; then |\n|\necho \"$LINE\" |\n|\nbreak |\n|\nfi |\n|\ndone) |\n|\n|\n|\nif [[ -z \"$LAST_ASSISTANT_LINE\" ]]; then exit 0; fi |\n|\n|\n|\n# Extract concatenated text content from the message (skip tool_use, thinking, etc). |\n|\nTEXT=$(echo \"$LAST_ASSISTANT_LINE\" | jq -r ' |\n|\n(.message.content // .content // []) as $c |\n|\n| if ($c | type) == \"array\" then |\n|\n$c | map(select(type == \"object\" and .type == \"text\") | .text) | join(\"\\n\") |\n|\nelif ($c | type) == \"string\" then |\n|\n$c |\n|\nelse |\n|\n\"\" |\n|\nend |\n|\n' 2>/dev/null || echo \"\") |\n|\n|\n|\nif [[ -z \"$TEXT\" ]]; then exit 0; fi |\n|\n|\n|\n# Dismiss sentinel for meta-discussion of the rule itself. |\n|\nif echo \"$TEXT\" | grep -qF '<!-- deferral-meta -->'; then exit 0; fi |\n|\n|\n|\n# Strip fenced code blocks so quoting the rule in ``` ... ``` doesn't trip. |\n|\nTEXT_NO_FENCES=$(echo \"$TEXT\" | awk ' |\n|\n/^[[:space:]]*```/ { in_block = !in_block; next } |\n|\n!in_block { print } |\n|\n') |\n|\n|\n|\n# Collect hits as: line:phrase:text |\n|\ndeclare -a HITS=() |\n|\n|\n|\nfor PHRASE in \"${BANNED_PHRASES[@]}\"; do |\n|\nwhile IFS= read -r MATCH; do |\n|\n[[ -z \"$MATCH\" ]] && continue |\n|\nHITS+=(\"L$MATCH :: phrase=\\\"$PHRASE\\\"\") |\n|\ndone < <(echo \"$TEXT_NO_FENCES\" | grep -inF \"$PHRASE\" 2>/dev/null) |\n|\ndone |\n|\n|\n|\n# \"pre-existing\" dismissal pattern |\n|\nwhile IFS= read -r MATCH; do |\n|\n[[ -z \"$MATCH\" ]] && continue |\n|\nif echo \"$MATCH\" | grep -iqE \"$PRE_EXISTING_DISMISSAL_REGEX\"; then |\n|\nHITS+=(\"L$MATCH :: phrase=\\\"pre-existing (dismissal)\\\"\") |\n|\nfi |\n|\ndone < <(echo \"$TEXT_NO_FENCES\" | grep -inF \"pre-existing\" 2>/dev/null) |\n|\n|\n|\nif [[ ${#HITS[@]} -eq 0 ]]; then exit 0; fi |\n|\n|\n|\n{ |\n|\necho \"[response-deferral-phrase-gate] BLOCK: banned deferral phrase(s) in your last response.\" |\n|\necho \"\" |\n|\necho \"Hits (line numbers relative to your last message, code blocks excluded):\" |\n|\nfor HIT in \"${HITS[@]}\"; do |\n|\necho \" $HIT\" |\n|\ndone |\n|\necho \"\" |\n|\necho \"Per .claude/rules/no-ephemeral-deferrals.md, every deferral-shaped\" |\n|\necho \"statement must resolve via case 1, case 2, or case 3:\" |\n|\necho \"\" |\n|\necho \" case 1: revise an existing pending change + /opsx:apply this turn\" |\n|\necho \" case 2: /opsx:propose <name> -> apply -> sync -> archive, this turn\" |\n|\necho \" case 3: /opsx:new <name> proposal-only + loud ## DEFERRED: callout\" |\n|\necho \"\" |\n|\necho \"If you genuinely need to discuss the rule itself, include the sentinel\" |\n|\necho \"<!-- deferral-meta --> in your response. The bar for using the sentinel\" |\n|\necho \"is HIGH -- it dismisses the gate entirely for that response.\" |\n|\necho \"\" |\n|\necho \"Otherwise: rewrite the deferral sentence to close the loop, or delete\" |\n|\necho \"it (the concern was not real), and respond again.\" |\n|\n} >&2 |\n|\n|\n|\nexit 2 |", "url": "https://wpnews.pro/news/anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks", "canonical_source": "https://gist.github.com/michael-jennings/7d31353941cc90a2e7d7cb251a8afb0e", "published_at": "2026-05-16 05:53:31+00:00", "updated_at": "2026-05-25 22:34:39.597430+00:00", "lang": "en", "topics": ["ai-safety", "ai-policy", "ai-ethics", "mlops", "ai-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks", "markdown": "https://wpnews.pro/news/anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks.md", "text": "https://wpnews.pro/news/anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks.txt", "jsonld": "https://wpnews.pro/news/anti-deferral-hook-important-the-sh-file-in-expects-to-live-in-project-dir-hooks.jsonld"}}