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
29 changes: 29 additions & 0 deletions .claude/hooks/block-ai-tells.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Claude Code PreToolUse hook (matcher: Bash).
# Blocks git commit / gh issue|pr create|comment commands whose message, title,
# or body contains an AI tell or emoji. Advisory layer; the git hooks enforce.
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "$DIR/lib.sh"

PY="$(command -v python3 || echo /usr/bin/python3)"
input="$(cat)"
cmd="$(printf '%s' "$input" | "$PY" -c 'import json,sys
try: print(json.load(sys.stdin).get("tool_input",{}).get("command",""))
except Exception: print("")')"

case "$cmd" in
*"git commit"*|*"gh issue create"*|*"gh pr create"*|*"gh issue comment"*|*"gh pr comment"*) ;;
*"gh api"*comment*) ;;
*) exit 0 ;;
esac

tells="$(printf '%s' "$cmd" | gov_find_text_tells)"
emoji="$(printf '%s' "$cmd" | gov_find_emoji)"
if [ -n "$tells$emoji" ]; then
reason="Blocked: AI-tell or emoji in a commit/issue/PR command. Remove attribution trailers (Co-Authored-By, Generated with), emojis, and session/AI references, then retry. Matched: ${tells} ${emoji}"
"$PY" -c 'import json,sys
print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":sys.argv[1]}}))' "$reason"
fi
exit 0
29 changes: 29 additions & 0 deletions .claude/hooks/check-push-readiness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Claude Code PreToolUse hook (matcher: Bash).
# On `git push`, gives fast feedback: blocks if there are uncommitted changes or
# if a quick test pass fails for the detected ecosystem. The git pre-push hook is
# the authoritative gate; this just shortens the feedback loop.
set -euo pipefail
PY="$(command -v python3 || echo /usr/bin/python3)"
input="$(cat)"
cmd="$(printf '%s' "$input" | "$PY" -c 'import json,sys
try: print(json.load(sys.stdin).get("tool_input",{}).get("command",""))
except Exception: print("")')"

case "$cmd" in *"git push"*) ;; *) exit 0 ;; esac

deny() {
"$PY" -c 'import json,sys
print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":sys.argv[1]}}))' "$1"
exit 0
}

if ! git diff --quiet --exit-code 2>/dev/null; then
deny "Uncommitted changes present. Commit or stash before pushing."
fi
if [ -f go.mod ]; then
if ! go test -short ./... >/dev/null 2>&1; then
deny "Quick test pass failed (go test -short ./...). Fix before pushing."
fi
fi
exit 0
27 changes: 27 additions & 0 deletions .claude/hooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Git commit-msg hook (enforcement). Requires a Conventional Commit title and
# rejects AI-tells or emoji anywhere in the message.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
# shellcheck source=/dev/null
source "$ROOT/.claude/hooks/lib.sh"

msg_file="$1"
title="$(head -1 "$msg_file")"

if ! printf '%s' "$title" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?!?: .+'; then
echo "commit-msg: title must be a Conventional Commit: type(scope)?: description" >&2
echo " valid types: feat fix docs style refactor perf test build ci chore revert" >&2
echo " got: $title" >&2
exit 1
fi

tells="$(gov_find_text_tells < "$msg_file")"
emoji="$(gov_find_emoji < "$msg_file")"
if [ -n "$tells$emoji" ]; then
echo "commit-msg: AI-tell or emoji not allowed in commit message:" >&2
[ -n "$tells" ] && echo "$tells" >&2
[ -n "$emoji" ] && echo "$emoji" >&2
exit 1
fi
exit 0
116 changes: 116 additions & 0 deletions .claude/hooks/forbidden-text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# AI-tell text patterns blocked in commit messages, issue/PR titles and bodies,
# comments, and source files. Extended-regex (grep -E), matched case-insensitively.
# One pattern per line. Lines beginning with # and blank lines are ignored.
# Emoji are blocked separately by unicode range (see lib.sh), not listed here.
#
# Goal: HUMAN WRITE, not CLAUDE WRITE. Every human-facing artifact should read
# as if a person wrote it. The ACTIVE patterns below are reliable generated-text
# fingerprints: attribution trailers, assistant self-reference, working-session
# framing, and the multi-word filler/cliche phrasing that betrays an LLM. They
# are multi-word on purpose, to avoid false positives on real source and docs.
#
# Single common buzzwords (robust, comprehensive, leverage, ...) live in the
# commented OPT-IN block at the bottom: they read as generated but also appear
# in legitimate human writing, so enable them only if you want them enforced.

# --- Attribution trailers / tool fingerprints ---
Co-Authored-By
Co-authored-by
Generated with
Generated by (Claude|AI|GPT|Copilot|Codex|an AI|a language model)
written by (Claude|an AI|AI)
Claude Code
Claude Opus
Claude Sonnet
@anthropic-ai
anthropic\.com
openai\.com

# --- Assistant self-reference ---
as an AI
as an AI( language)? model
as a large language model
as an AI assistant
I am an AI
I'?m an AI
I am a language model
I do not have (personal|the ability)
I cannot (browse|access|provide real-time)
my (training data|knowledge cutoff|last update)
knowledge cutoff
as of my last (update|training)

# --- Working-session / process framing ---
in this( CI)? session
in (this|our) (conversation|chat|thread|session|exchange)
as (previously )?discussed
as (you )?requested
as (mentioned|noted|stated|described) (earlier|above|previously|below)
per (your|our) (request|discussion|conversation)
in the previous (turn|message|response|step)
based on (your|the) (request|previous)

# --- LLM hedging / filler cliches ---
It('?s| is) important to (note|remember|understand|keep in mind)
It('?s| is) worth (noting|mentioning|highlighting)
I hope this helps
Hope (this|that) helps
Let me know if (you|there|that|anything)
feel free to (reach out|ask|let me know|modify|adjust|customize)
If you have any (questions|concerns|feedback)
(do not|don'?t) hesitate to
without further ado
let'?s (dive|jump) (in|into|right)
(delve|delving) into
rest assured
that being said
needless to say
it goes without saying
when it comes to
at the end of the day
in today'?s (fast-paced|digital|modern|ever-)
in the (realm|world) of
a testament to
plays a (crucial|vital|key|pivotal|significant) role
it'?s not just about
the key (takeaway|thing to remember)

# --- AI structure / boilerplate openers ---
Here('?s| is) (a |an )?(breakdown|summary|quick overview|rundown|high-level overview)
In (summary|conclusion|essence),
To (summarize|conclude|sum up),
Certainly!
Absolutely!
Sure(,| thing)!
Great question
I'?d be happy to
Below is (a|an|the)
Let'?s get started
Let'?s break (it|this) down

# --- OPT-IN: single-word AI buzzwords (disabled by default) ---
# These read as generated but also appear in legitimate docs, so they would
# false-positive on real source/READMEs. Uncomment any you want enforced.
# seamless(ly)?
# robust
# comprehensive
# leverage
# utilize
# delve
# intricate
# pivotal
# realm
# tapestry
# multifaceted
# nuanced
# underscore(s|d)?
# showcase(s|d)?
# elevate
# unleash
# supercharge
# cutting-edge
# state-of-the-art
# paradigm shift
# holistic
# synergy
# game-?chang(er|ing)
28 changes: 28 additions & 0 deletions .claude/hooks/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Install the governance hooks into the current git repository.
# Copies the Claude Code hooks + shared lib into .claude/hooks/ and the git
# enforcement hooks into .git/hooks/. Run from anywhere inside the repo.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST="$ROOT/.claude/hooks"

mkdir -p "$DEST"
# Skip the copy when the source already IS the destination (e.g. a repo that
# vendored the hooks under .claude/hooks); copying onto itself errors.
if [ "$SRC" != "$DEST" ]; then
cp "$SRC/lib.sh" "$SRC/forbidden-text.txt" \
"$SRC/block-ai-tells.sh" "$SRC/validate-conventional-commit.sh" "$SRC/check-push-readiness.sh" \
"$DEST/"
fi
chmod +x "$DEST/"*.sh

for h in pre-commit commit-msg pre-push; do
cp "$SRC/$h" "$ROOT/.git/hooks/$h"
chmod +x "$ROOT/.git/hooks/$h"
done

echo "Installed:"
echo " Claude Code hooks -> $ROOT/.claude/hooks/ (wire them in .claude/settings.json)"
echo " git hooks -> $ROOT/.git/hooks/ (pre-commit, commit-msg, pre-push)"
echo "Add the PreToolUse entries from .claude/settings.json (see governance settings.template.json)."
25 changes: 25 additions & 0 deletions .claude/hooks/lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Shared helpers for the project governance hooks.
# Works with BSD (macOS) and GNU tooling. Emoji detection uses perl (BSD grep
# lacks -P); text-pattern detection uses grep -E.

GOV_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GOV_PATTERNS_FILE="${GOV_PATTERNS_FILE:-$GOV_HOOKS_DIR/forbidden-text.txt}"

# Build one extended-regex alternation from the patterns file.
gov_text_regex() {
grep -vE '^[[:space:]]*(#|$)' "$GOV_PATTERNS_FILE" 2>/dev/null | paste -sd '|' -
}

# Read stdin; print offending lines for forbidden AI-tell phrases (empty if none).
gov_find_text_tells() {
local re
re="$(gov_text_regex)"
[ -z "$re" ] && return 0
grep -inE "$re" || true
}

# Read stdin; print offending lines containing emoji/pictographs (empty if none).
gov_find_emoji() {
perl -CSD -ne 'print "$.: $_" if /[\x{1F000}-\x{1FAFF}\x{2600}-\x{27BF}\x{2B00}-\x{2BFF}\x{FE00}-\x{FE0F}\x{1F1E6}-\x{1F1FF}]/'
}
63 changes: 63 additions & 0 deletions .claude/hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Git pre-commit hook (enforcement).
# - Emoji are banned in every staged text file EXCEPT CI workflow files, where
# they are allowed in job/step names.
# - AI-tell phrases are banned too, EXCEPT in files whose purpose is to document
# the rule (CLAUDE.md, the governance hooks, the pattern list), which
# legitimately contain those phrases as examples.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
# shellcheck source=/dev/null
source "$ROOT/.claude/hooks/lib.sh"

is_binary() {
case "$1" in
*.png|*.jpg|*.jpeg|*.gif|*.pdf|*.ico|*.svg|*.woff|*.woff2|*.ttf|*.eot|*.zip|*.gz|*.jar|*.keystore) return 0 ;;
*) return 1 ;;
esac
}

# Paths exempt from the AI-tell TEXT scan (still subject to the emoji scan).
tells_exempt() {
case "$1" in
CLAUDE.md|*/CLAUDE.md) return 0 ;;
.claude/*|*/.claude/*) return 0 ;;
governance/*|*/governance/*) return 0 ;;
.github/workflows/*|*workflows/*.yml|*workflows/*.yaml) return 0 ;;
*forbidden-text.txt) return 0 ;;
*) return 1 ;;
esac
}

# Paths where emoji ARE allowed: Markdown docs (Bugs5382 standard), GitHub
# config (CI workflow job/step names and release-drafter category titles), and
# the hub's governance/github tree that holds that GitHub config before sync.
emoji_exempt() {
case "$1" in
.github/*|*/.github/*) return 0 ;;
*.md) return 0 ;;
NOTES.txt|*/NOTES.txt) return 0 ;;
governance/github/*) return 0 ;;
*) return 1 ;;
esac
}

fail=0
while IFS= read -r f; do
[ -f "$f" ] || continue
case "$f" in .git/*) continue ;; esac
is_binary "$f" && continue

if ! emoji_exempt "$f"; then
emoji="$(gov_find_emoji < "$f")"
if [ -n "$emoji" ]; then echo "pre-commit: emoji not allowed in $f:" >&2; echo "$emoji" >&2; fail=1; fi
fi

if ! tells_exempt "$f"; then
tells="$(gov_find_text_tells < "$f")"
if [ -n "$tells" ]; then echo "pre-commit: AI-tell not allowed in $f:" >&2; echo "$tells" >&2; fail=1; fi
fi
done < <(git diff --cached --name-only --diff-filter=ACM)

[ "$fail" -eq 0 ] || { echo "pre-commit: blocked. Remove the above before committing." >&2; exit 1; }
exit 0
45 changes: 45 additions & 0 deletions .claude/hooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Git pre-push hook (enforcement). Detects the ecosystem from manifest files and
# runs format/lint/test gates before anything is pushed. Verified and clean
# before GitHub. Runs only the tooling that is actually installed.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"

run() { echo "pre-push: $*"; "$@"; }

if [ -f go.mod ]; then
unformatted="$(gofmt -l . 2>/dev/null | grep -v '^vendor/' || true)"
if [ -n "$unformatted" ]; then
echo "pre-push: not gofmt-clean (run: gofmt -w .):" >&2; echo "$unformatted" >&2; exit 1
fi
run go vet ./... || { echo "pre-push: go vet failed" >&2; exit 1; }
if command -v golangci-lint >/dev/null 2>&1; then
run golangci-lint run ./... || { echo "pre-push: golangci-lint failed" >&2; exit 1; }
else
echo "pre-push: golangci-lint not installed; skipping (CI will run it)"
fi
run go test ./... || { echo "pre-push: tests failed" >&2; exit 1; }
fi

if [ -f package.json ]; then
if [ ! -d node_modules ]; then
echo "pre-push: node_modules not installed; skipping npm lint/test (CI runs them)"
else
has() { node -e "process.exit(require('./package.json').scripts?.['$1']?0:1)" 2>/dev/null; }
if has lint; then run npm run --silent lint || { echo "pre-push: npm lint failed" >&2; exit 1; }; fi
if has test; then run npm test --silent || { echo "pre-push: npm test failed" >&2; exit 1; }; fi
fi
fi

if [ -f pyproject.toml ]; then
if command -v ruff >/dev/null 2>&1; then
run ruff check . || { echo "pre-push: ruff failed" >&2; exit 1; }
fi
if command -v pytest >/dev/null 2>&1; then
run pytest -q || { echo "pre-push: pytest failed" >&2; exit 1; }
fi
fi

echo "pre-push: checks passed"
exit 0
Loading
Loading