From 4957c60c9ee985628ad59344e593d20a18ca8fdb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 17:06:10 -0500 Subject: feat(hooks): add global hooks — PreCompact priorities + git/gh confirm modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new machine-wide hooks installed via `make install-hooks`: - `precompact-priorities.sh` (PreCompact) — injects a priority block into the compaction prompt so the generated summary retains information most expensive to reconstruct: unanswered questions, root causes with file:line, subagent findings as primary evidence, exact numbers/IDs, A-vs-B decisions, open TODOs, classified-data handling. - `git-commit-confirm.py` (PreToolUse/Bash) — gates `git commit` behind a confirmation modal showing parsed message, staged files, diff stats, author. Parses both HEREDOC and `-m`/`--message` forms. - `gh-pr-create-confirm.py` (PreToolUse/Bash) — gates `gh pr create` behind a modal showing title, base ← head, reviewers, labels, assignees, milestone, draft flag, body (HEREDOC or quoted). Makefile: adds `install-hooks` / `uninstall-hooks` targets and extends `list` with a Hooks section. Install prints the settings.json snippet (in `hooks/settings-snippet.json`) to merge into `~/.claude/settings.json`. Also: `languages/elisp/claude/hooks/validate-el.sh` now emits JSON with `hookSpecificOutput.additionalContext` on failure (via new `fail_json()` helper) so Claude sees a structured error in context, in addition to the existing stderr output and exit 2. Patterns synthesized clean-room from fcakyon/claude-codex-settings (Apache-2.0). Each hook is original content. --- Makefile | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) (limited to 'Makefile') diff --git a/Makefile b/Makefile index d37e955..1e9a1e8 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,14 @@ SHELL := /bin/bash SKILLS_DIR := $(HOME)/.claude/skills RULES_DIR := $(HOME)/.claude/rules +HOOKS_DIR := $(HOME)/.claude/hooks SKILLS := c4-analyze c4-diagram debug add-tests respond-to-review review-code fix-issue security-check \ arch-design arch-decide arch-document arch-evaluate \ brainstorm codify root-cause-trace five-whys prompt-engineering \ playwright-js playwright-py frontend-design pairwise-tests \ finish-branch RULES := $(wildcard claude-rules/*.md) +HOOKS := $(wildcard hooks/*.sh hooks/*.py) LANGUAGES := $(notdir $(wildcard languages/*)) # Pick target project — use PROJECT= or interactive fzf over local .git dirs. @@ -35,7 +37,7 @@ $(if $(shell command -v pacman 2>/dev/null),sudo pacman -S --noconfirm $(1),\ $(error No supported package manager found (brew/apt-get/pacman))))) endef -.PHONY: help install uninstall list \ +.PHONY: help install uninstall list install-hooks uninstall-hooks \ install-lang install-elisp install-python list-languages \ diff lint deps @@ -168,6 +170,50 @@ list: ## Show global install status echo " - $$name"; \ fi \ done + @echo "" + @echo "Hooks:" + @for hook in $(HOOKS); do \ + name=$$(basename $$hook); \ + if [ -L "$(HOOKS_DIR)/$$name" ]; then \ + echo " ✓ $$name (installed)"; \ + else \ + echo " - $$name"; \ + fi \ + done + +install-hooks: ## Symlink global hooks into ~/.claude/hooks/ + print settings.json snippet + @mkdir -p $(HOOKS_DIR) + @echo "Hooks:" + @for hook in $(HOOKS); do \ + name=$$(basename $$hook); \ + if [ -L "$(HOOKS_DIR)/$$name" ]; then \ + echo " skip $$name (already linked)"; \ + elif [ -e "$(HOOKS_DIR)/$$name" ]; then \ + echo " WARN $$name exists and is not a symlink — skipping"; \ + else \ + ln -s "$(CURDIR)/$$hook" "$(HOOKS_DIR)/$$name"; \ + echo " link $$name → $(HOOKS_DIR)/$$name"; \ + fi \ + done + @echo "" + @echo "Merge this into ~/.claude/settings.json (preserve any existing hooks arrays):" + @echo "" + @cat hooks/settings-snippet.json + @echo "" + @echo "After merging, reload Claude Code (open /hooks menu once, or restart the session)." + +uninstall-hooks: ## Remove global hook symlinks from ~/.claude/hooks/ + @for hook in $(HOOKS); do \ + name=$$(basename $$hook); \ + if [ -L "$(HOOKS_DIR)/$$name" ]; then \ + rm "$(HOOKS_DIR)/$$name"; \ + echo " rm $$name"; \ + else \ + echo " skip $$name (not a symlink)"; \ + fi \ + done + @echo "" + @echo "Note: this does NOT edit ~/.claude/settings.json — remove the hook entries manually." ##@ Per-project language bundles -- cgit v1.2.3