From 560c29ade19be74968d8fec26d469913d383f2e8 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 17:39:33 -0500 Subject: feat(hooks+skills): destructive-bash confirm + architecture suite + problem-solving routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three improvements bundled together: 1. New hook — `destructive-bash-confirm.py` (PreToolUse/Bash): Gates `git push --force`, `git reset --hard`, `git clean -f`, `git branch -D`, and `rm -rf` behind a confirmation modal with the command, local context (branch, uncommitted counts, targeted paths), and a severity banner. Elevates severity when force-pushing protected branches (main/master/develop/release/prod) or when rm -rf targets root, home, or wildcard paths. Reuses _common.py. 2. Architecture suite rename — the "Part of the arch-* suite" footer in arch-design, arch-decide, arch-document, arch-evaluate descriptions now reads "Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming)." Matching footers added to c4-analyze and c4-diagram. c4-* keep their framework-specific prefix (C4 is a notation, arch-* is framework-agnostic workflow) but are now discoverable as suite members. 3. Problem-solving cluster routing — added YAML frontmatter with descriptions (including "Do NOT use for X (use Y)" clauses) to debug/SKILL.md and fix-issue/SKILL.md. Previously both had no frontmatter at all, which broke skill-router discovery. The four cluster members (debug, fix-issue, root-cause-trace, five-whys) now route unambiguously by description alone. --- arch-decide/SKILL.md | 2 +- arch-design/SKILL.md | 2 +- arch-document/SKILL.md | 2 +- arch-evaluate/SKILL.md | 2 +- c4-analyze/SKILL.md | 2 +- c4-diagram/SKILL.md | 2 +- debug/SKILL.md | 5 ++ fix-issue/SKILL.md | 5 ++ hooks/README.md | 1 + hooks/destructive-bash-confirm.py | 174 ++++++++++++++++++++++++++++++++++++++ hooks/settings-snippet.json | 3 +- 11 files changed, 193 insertions(+), 7 deletions(-) create mode 100755 hooks/destructive-bash-confirm.py diff --git a/arch-decide/SKILL.md b/arch-decide/SKILL.md index e0fd05f..8446ca3 100644 --- a/arch-decide/SKILL.md +++ b/arch-decide/SKILL.md @@ -1,6 +1,6 @@ --- name: arch-decide -description: Create, update, and manage Architecture Decision Records (ADRs) that capture significant technical decisions — context, options, chosen approach, consequences. Five template variants (MADR, Nygard, Y-statement, lightweight, RFC). Covers ADR lifecycle (proposed → accepted → deprecated / superseded), review process, and adr-tools automation. Use when documenting an architectural choice, reviewing past decisions, or establishing a decision process. Part of the arch-* suite (arch-design / arch-decide / arch-document / arch-evaluate). +description: Create, update, and manage Architecture Decision Records (ADRs) that capture significant technical decisions — context, options, chosen approach, consequences. Five template variants (MADR, Nygard, Y-statement, lightweight, RFC). Covers ADR lifecycle (proposed → accepted → deprecated / superseded), review process, and adr-tools automation. Use when documenting an architectural choice, reviewing past decisions, or establishing a decision process. Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). --- # Architecture Decision Records diff --git a/arch-design/SKILL.md b/arch-design/SKILL.md index d1381e3..90b58cf 100644 --- a/arch-design/SKILL.md +++ b/arch-design/SKILL.md @@ -1,6 +1,6 @@ --- name: arch-design -description: Shape the architecture of a new or restructured software project through structured intake (quality attributes, stakeholders, constraints, scale, change drivers), then propose candidate architectural paradigms with honest trade-off analysis and a recommended direction. Paradigm-agnostic — evaluates options across layered, hexagonal, microservices, event-driven, CQRS, modular-monolith, serverless, pipe-and-filter, DDD, and others. Outputs a brief at `.architecture/brief.md` that downstream skills (arch-decide, arch-document, arch-evaluate) read. Use when starting a new project or service, restructuring an existing one, choosing a tech stack, or formalizing architecture before implementation. Do NOT use for bug fixing, code review, small feature additions, documenting an existing architecture (use arch-document), evaluating an existing architecture against a brief (use arch-evaluate), or recording specific individual decisions (use arch-decide). Part of the arch-* suite (arch-design / arch-decide / arch-document / arch-evaluate). +description: Shape the architecture of a new or restructured software project through structured intake (quality attributes, stakeholders, constraints, scale, change drivers), then propose candidate architectural paradigms with honest trade-off analysis and a recommended direction. Paradigm-agnostic — evaluates options across layered, hexagonal, microservices, event-driven, CQRS, modular-monolith, serverless, pipe-and-filter, DDD, and others. Outputs a brief at `.architecture/brief.md` that downstream skills (arch-decide, arch-document, arch-evaluate) read. Use when starting a new project or service, restructuring an existing one, choosing a tech stack, or formalizing architecture before implementation. Do NOT use for bug fixing, code review, small feature additions, documenting an existing architecture (use arch-document), evaluating an existing architecture against a brief (use arch-evaluate), or recording specific individual decisions (use arch-decide). Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). --- # Architecture Design diff --git a/arch-document/SKILL.md b/arch-document/SKILL.md index fa3e798..0690038 100644 --- a/arch-document/SKILL.md +++ b/arch-document/SKILL.md @@ -1,6 +1,6 @@ --- name: arch-document -description: Produce a complete arc42-structured architecture document from a project's architecture brief and ADRs. Generates all twelve arc42 sections (Introduction & Goals, Constraints, Context & Scope, Solution Strategy, Building Block View, Runtime View, Deployment View, Crosscutting Concepts, Architecture Decisions, Quality Requirements, Risks & Technical Debt, Glossary). Dispatches to the c4-analyze and c4-diagram skills for building-block, container, and context diagrams. Outputs one file per section under `docs/architecture/`. Use when formalizing an architecture that already has a brief + ADRs, preparing documentation for a review, onboarding new engineers, or satisfying a compliance requirement. Do NOT use for shaping a new architecture (use arch-design), recording individual decisions (use arch-decide), auditing code against an architecture (use arch-evaluate), or for simple systems where a brief alone suffices. Part of the arch-* suite (arch-design / arch-decide / arch-document / arch-evaluate). +description: Produce a complete arc42-structured architecture document from a project's architecture brief and ADRs. Generates all twelve arc42 sections (Introduction & Goals, Constraints, Context & Scope, Solution Strategy, Building Block View, Runtime View, Deployment View, Crosscutting Concepts, Architecture Decisions, Quality Requirements, Risks & Technical Debt, Glossary). Dispatches to the c4-analyze and c4-diagram skills for building-block, container, and context diagrams. Outputs one file per section under `docs/architecture/`. Use when formalizing an architecture that already has a brief + ADRs, preparing documentation for a review, onboarding new engineers, or satisfying a compliance requirement. Do NOT use for shaping a new architecture (use arch-design), recording individual decisions (use arch-decide), auditing code against an architecture (use arch-evaluate), or for simple systems where a brief alone suffices. Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). --- # Architecture Documentation (arc42) diff --git a/arch-evaluate/SKILL.md b/arch-evaluate/SKILL.md index cc43779..f1c3391 100644 --- a/arch-evaluate/SKILL.md +++ b/arch-evaluate/SKILL.md @@ -1,6 +1,6 @@ --- name: arch-evaluate -description: Audit an existing codebase against its stated architecture brief and ADRs. Runs framework-agnostic checks (cyclic dependencies, stated-layer violations, public API drift) that work on any language without setup, and opportunistically invokes language-specific linters (dependency-cruiser for TypeScript, import-linter for Python, go vet + depguard for Go) when they're already configured in the repo — augmenting findings, never replacing. Produces a report with severity levels (error / warning / info) and pointers to the relevant brief section or ADR for each violation. Use when auditing conformance before a release, during code review, when an architecture is suspected to have drifted, or as a pre-merge CI gate. Do NOT use for designing an architecture (use arch-design), recording decisions (use arch-decide), or producing documentation (use arch-document). Part of the arch-* suite (arch-design / arch-decide / arch-document / arch-evaluate). +description: Audit an existing codebase against its stated architecture brief and ADRs. Runs framework-agnostic checks (cyclic dependencies, stated-layer violations, public API drift) that work on any language without setup, and opportunistically invokes language-specific linters (dependency-cruiser for TypeScript, import-linter for Python, go vet + depguard for Go) when they're already configured in the repo — augmenting findings, never replacing. Produces a report with severity levels (error / warning / info) and pointers to the relevant brief section or ADR for each violation. Use when auditing conformance before a release, during code review, when an architecture is suspected to have drifted, or as a pre-merge CI gate. Do NOT use for designing an architecture (use arch-design), recording decisions (use arch-decide), or producing documentation (use arch-document). Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). --- # Architecture Evaluation diff --git a/c4-analyze/SKILL.md b/c4-analyze/SKILL.md index 0817ca7..ab8986b 100644 --- a/c4-analyze/SKILL.md +++ b/c4-analyze/SKILL.md @@ -1,6 +1,6 @@ --- name: c4-analyze -description: Analyze a codebase or git repo and generate C4 architecture diagrams (System Context, Container, Component). Use when the user wants to visualize or document the architecture of an existing project. +description: Analyze a codebase or git repo and generate C4 architecture diagrams (System Context, Container, Component). Use when the user wants to visualize or document the architecture of an existing project. Dispatched by arch-document for building-block and container views; usable standalone for quick architecture visualization. Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). argument-hint: "[path-or-container-name]" --- diff --git a/c4-diagram/SKILL.md b/c4-diagram/SKILL.md index 57b056f..15948e4 100644 --- a/c4-diagram/SKILL.md +++ b/c4-diagram/SKILL.md @@ -1,6 +1,6 @@ --- name: c4-diagram -description: Generate C4 architecture diagrams from a textual description of a software system. Use when the user describes a system they want to diagram, or wants to create architecture diagrams for a planned/proposed system. +description: Generate C4 architecture diagrams from a textual description of a software system. Use when the user describes a system they want to diagram, or wants to create architecture diagrams for a planned/proposed system. Dispatched by arch-document for context and container views; usable standalone when the system is described in prose rather than existing code. Part of the architecture suite (arch-design / arch-decide / arch-document / arch-evaluate + c4-analyze / c4-diagram for notation-specific diagramming). argument-hint: "[description or diagram level]" --- diff --git a/debug/SKILL.md b/debug/SKILL.md index 7f3771a..7b207aa 100644 --- a/debug/SKILL.md +++ b/debug/SKILL.md @@ -1,3 +1,8 @@ +--- +name: debug +description: Investigate a bug or test failure methodically through four phases — understand the symptom (reproduce, read logs, locate failure point, trace data flow), isolate variables (minimal repro, bisect), form and test hypotheses, then fix at the root. Captures evidence before proposing fixes; rejects shotgun debugging; escalates to architectural investigation after three failed fix attempts. Use when the failure mode is unclear, the failure reproduces inconsistently, or you're about to start guessing. Do NOT use for clear local bugs where the fix site is obvious (just fix it), for ticket-driven implementation work with a known fix (use fix-issue), for backward-walking a specific error up the call stack (use root-cause-trace), or for process/organizational root-cause analysis of recurring incidents (use five-whys). Companion to fix-issue / root-cause-trace / five-whys — debug is the broad investigative workflow; the others specialize. +--- + # /debug — Systematic Debugging Investigate a bug or test failure methodically. No guessing, no shotgun fixes. diff --git a/fix-issue/SKILL.md b/fix-issue/SKILL.md index adfed14..0b6e208 100644 --- a/fix-issue/SKILL.md +++ b/fix-issue/SKILL.md @@ -1,3 +1,8 @@ +--- +name: fix-issue +description: Ticket-driven implementation workflow — fetch issue details from the tracker (Linear / GitHub Issues / Jira), create a branch, implement against acceptance criteria with tests, and commit/push. Reads the issue, verifies the delivery matches intent, handles the full branch lifecycle. Use when you have an issue ID or a well-scoped task ready to implement. Do NOT use for open-ended bug investigation without a clear fix path (use debug first), for tracing an error through a long call stack to its origin (use root-cause-trace), for small unticketed edits (just do them), or when requirements aren't yet clear (use brainstorm or arch-design). Companion to debug — fix-issue is the workflow scaffold around implementing a known fix; debug is the upstream investigative phase. +--- + # /fix-issue — Pick Up and Implement an Issue Create a branch, implement the fix, test, and commit. diff --git a/hooks/README.md b/hooks/README.md index 97dd881..5555514 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -9,6 +9,7 @@ Machine-wide Claude Code hooks that install into `~/.claude/hooks/` and apply to | `precompact-priorities.sh` | `PreCompact` | Injects a priority-preservation block into Claude's compaction prompt so the generated summary retains information most expensive to reconstruct (unanswered questions, root causes with `file:line`, subagent findings, 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 the parsed message, staged files, diff stats, and git author. Parses both HEREDOC and `-m`/`--message` forms. | | `gh-pr-create-confirm.py` | `PreToolUse(Bash)` | Gates `gh pr create` behind a confirmation modal showing title, base←head, reviewers, labels, assignees, milestone, draft flag, and body (HEREDOC or quoted). | +| `destructive-bash-confirm.py` | `PreToolUse(Bash)` | Gates destructive commands (`git push --force`, `git reset --hard`, `git clean -f`, `git branch -D`, `rm -rf`) with a modal showing the command, local context (branch, uncommitted file counts, targeted paths), and a warning banner. Elevates severity when force-pushing protected branches or targeting root/home/wildcard paths. | Shared library (not a hook): `_common.py` — `read_payload()`, `respond_ask()`, `scan_attribution()`. Installed as a sibling symlink so the two Python hooks can `from _common import …` at runtime. diff --git a/hooks/destructive-bash-confirm.py b/hooks/destructive-bash-confirm.py new file mode 100755 index 0000000..c1cf5f9 --- /dev/null +++ b/hooks/destructive-bash-confirm.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""PreToolUse hook for Bash: gate destructive commands behind a modal. + +Detects and asks for confirmation before: + - git push --force / -f / --force-with-lease (overwrites remote history) + - git reset --hard (discards working-tree) + - git clean -f (deletes untracked files) + - git branch -D (force-deletes branches) + - rm -rf (any flag combo containing both -r/-R and -f) + +Each pattern emits a modal with the command, local context (current +branch, uncommitted line count, targeted paths, etc.), and a warning +banner via systemMessage. First match wins — a command with multiple +destructive patterns fires on the first detected. + +Non-destructive Bash calls exit 0 silent. +""" + +import re +import subprocess +import sys +from typing import Optional + +from _common import read_payload, respond_ask + + +PROTECTED_BRANCHES = {"main", "master", "develop", "release", "prod", "production"} + + +def main() -> int: + payload = read_payload() + if payload.get("tool_name") != "Bash": + return 0 + + cmd = payload.get("tool_input", {}).get("command", "") + detection = detect_destructive(cmd) + if not detection: + return 0 + + kind, context = detection + reason = format_confirmation(kind, cmd, context) + banner = context.pop("_banner", f"DESTRUCTIVE: {kind}") + + respond_ask(reason, system_message=banner) + return 0 + + +def detect_destructive(cmd: str) -> Optional[tuple[str, dict]]: + """Return (kind, context) for the first destructive pattern matched.""" + + if is_force_push(cmd): + branch = run_git(["rev-parse", "--abbrev-ref", "HEAD"]).strip() + ctx: dict = {"branch": branch or "(detached)"} + if branch in PROTECTED_BRANCHES: + ctx["_banner"] = ( + f"DESTRUCTIVE: force-push to PROTECTED branch '{branch}' — " + f"rewrites shared history." + ) + return "git push --force", ctx + + if re.search(r"(?:^|[\s;&|()])git\s+reset\s+(?:\S+\s+)*--hard\b", cmd): + staged = count_lines(run_git(["diff", "--cached", "--stat"])) + unstaged = count_lines(run_git(["diff", "--stat"])) + return "git reset --hard", { + "staged_files": max(staged - 1, 0), + "unstaged_files": max(unstaged - 1, 0), + } + + if re.search(r"(?:^|[\s;&|()])git\s+clean\s+(?:\S+\s+)*-[a-zA-Z]*f", cmd): + untracked = run_git(["ls-files", "--others", "--exclude-standard"]) + return "git clean -f", { + "untracked_files": len(untracked.splitlines()), + } + + if m := re.search(r"(?:^|[\s;&|()])git\s+branch\s+(?:\S+\s+)*-D\s+(\S+)", cmd): + target = m.group(1) + unmerged = run_git( + ["log", f"main..{target}", "--oneline"] + ).strip() if target else "" + ctx = {"branch_to_delete": target} + if unmerged: + ctx["unmerged_commits"] = len(unmerged.splitlines()) + return "git branch -D", ctx + + rm_targets = detect_rm_rf(cmd) + if rm_targets is not None: + ctx = {"targets": rm_targets or ["(none parsed)"]} + dangerous = [ + t for t in rm_targets + if t in ("/", "~", "$HOME", ".", "..", "*") + or t.startswith("/") + or t.startswith("~") + ] + if dangerous: + ctx["_banner"] = ( + f"DESTRUCTIVE: rm -rf targeting root/home/wildcard paths: " + f"{', '.join(dangerous)}" + ) + return "rm -rf", ctx + + return None + + +def is_force_push(cmd: str) -> bool: + """Match `git push` with any force variant.""" + if not re.search(r"(?:^|[\s;&|()])git\s+(?:\S+\s+)*push\b", cmd): + return False + # Look for --force / --force-with-lease / -f as a standalone flag + # (avoid matching -f inside a longer token that isn't a flag chain) + return bool( + re.search(r"(?:\s|^)--force(?:-with-lease)?\b", cmd) + or re.search(r"(?:\s|^)-[a-zA-Z]*f[a-zA-Z]*\b", cmd[cmd.find("push"):]) + ) + + +def detect_rm_rf(cmd: str) -> Optional[list[str]]: + """If cmd invokes `rm` with both -r/-R and -f flags, return its targets.""" + m = re.search(r"(?:^|[\s;&|()])rm\s+(.+)$", cmd) + if not m: + return None + + rest = m.group(1).split() + flag_chars = "" + i = 0 + while i < len(rest) and rest[i].startswith("-") and rest[i] != "--": + flag_chars += rest[i][1:] + i += 1 + if rest[i:i+1] == ["--"]: + i += 1 + + has_r = bool(re.search(r"[rR]", flag_chars)) + has_f = "f" in flag_chars + if not (has_r and has_f): + return None + + return rest[i:] + + +def run_git(args: list) -> str: + try: + out = subprocess.run( + ["git"] + args, + capture_output=True, + text=True, + timeout=3, + ) + return out.stdout + except (subprocess.SubprocessError, OSError, FileNotFoundError): + return "" + + +def count_lines(text: str) -> int: + return len([ln for ln in text.splitlines() if ln.strip()]) + + +def format_confirmation(kind: str, cmd: str, context: dict) -> str: + lines = [f"Run destructive command — {kind}?", ""] + lines.append("Command:") + lines.append(f" {cmd}") + lines.append("") + + if context: + lines.append("Context:") + for key, val in context.items(): + lines.append(f" {key}: {val}") + lines.append("") + + lines.append("This operation is destructive and typically irreversible.") + lines.append("Confirm before proceeding.") + return "\n".join(lines) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hooks/settings-snippet.json b/hooks/settings-snippet.json index 2a8ac54..11459f2 100644 --- a/hooks/settings-snippet.json +++ b/hooks/settings-snippet.json @@ -12,7 +12,8 @@ "matcher": "Bash", "hooks": [ { "type": "command", "command": "~/.claude/hooks/git-commit-confirm.py" }, - { "type": "command", "command": "~/.claude/hooks/gh-pr-create-confirm.py" } + { "type": "command", "command": "~/.claude/hooks/gh-pr-create-confirm.py" }, + { "type": "command", "command": "~/.claude/hooks/destructive-bash-confirm.py" } ] } ] -- cgit v1.2.3