mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
c4f679d829
* feat: add /careful, /freeze, /guard, /unfreeze safety hook skills Four new on-demand skills using Claude Code's PreToolUse hooks: - /careful: warns before destructive commands (rm -rf, DROP TABLE, force-push, etc.) - /freeze: blocks file edits outside a specified directory - /guard: composes both into one command - /unfreeze: clears freeze boundary without ending session Pure bash hook scripts with Python fallback for JSON edge cases. Safe exceptions for build artifacts (node_modules, dist, .next, etc.). Hook fire telemetry logs pattern name only (never command content). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add skill usage telemetry to preamble TemplateContext system passes skill name through resolver pipeline so each generated SKILL.md gets its own name baked into the telemetry line. Appends to ~/.gstack/analytics/skill-usage.jsonl on every invocation. Covers 14 preamble-using skills + 4 hook skills (inline telemetry). JSONL format: {"skill":"ship","ts":"...","repo":"my-project"} Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add analytics CLI for skill usage stats bun run analytics reads ~/.gstack/analytics/skill-usage.jsonl and shows top skills, per-repo breakdown, hook fire stats, and daily timeline. Supports --period 7d/30d/all. Handles missing/empty/malformed data. 22 unit tests cover parsing, filtering, formatting, and edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add skills-used-this-week to /retro Retro Step 2 now reads skill-usage.jsonl and shows which gstack skills were used during the retro window. Follows the same pattern as the Greptile signal and Backlog Health metrics — read file, filter by date, aggregate, present. Skips silently if no analytics data exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add hook script and telemetry tests 32 unit tests for check-careful.sh covering all 8 destructive patterns, safe exceptions, Python fallback, and malformed input handling. 7 unit tests for check-freeze.sh covering boundary enforcement, trailing slash edge case, and missing state file. Telemetry tests verify per-skill name correctness in generated output. Adds careful/freeze/guard/unfreeze/document-release to ALL_SKILLS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version to 0.6.5 + changelog + mark TODOs shipped Safety hook skills and skill usage telemetry shipped. Analytics CLI and /retro integration included. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: /debug auto-freezes edits to the module being debugged Add PreToolUse hooks (Edit/Write) to debug/SKILL.md.tmpl that reference the existing freeze/bin/check-freeze.sh. After Phase 1 investigation, /debug locks edits to the narrowest affected directory. Graceful degradation: if freeze script is unavailable, scope lock is skipped. Users can run /unfreeze to remove the restriction. Deferred 6 enhancements to TODOS.md, gated on telemetry showing the freeze hook actually fires in real debugging sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
4.2 KiB
Bash
Executable File
113 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-careful.sh — PreToolUse hook for /careful skill
|
|
# Reads JSON from stdin, checks Bash command for destructive patterns.
|
|
# Returns {"permissionDecision":"ask","message":"..."} to warn, or {} to allow.
|
|
set -euo pipefail
|
|
|
|
# Read stdin (JSON with tool_input)
|
|
INPUT=$(cat)
|
|
|
|
# Extract the "command" field value from tool_input
|
|
# Try grep/sed first (handles 99% of cases), fall back to Python for escaped quotes
|
|
CMD=$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true)
|
|
|
|
# Python fallback if grep returned empty (e.g., escaped quotes in command)
|
|
if [ -z "$CMD" ]; then
|
|
CMD=$(printf '%s' "$INPUT" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read()).get("tool_input",{}).get("command",""))' 2>/dev/null || true)
|
|
fi
|
|
|
|
# If we still couldn't extract a command, allow
|
|
if [ -z "$CMD" ]; then
|
|
echo '{}'
|
|
exit 0
|
|
fi
|
|
|
|
# Normalize: lowercase for case-insensitive SQL matching
|
|
CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# --- Check for safe exceptions (rm -rf of build artifacts) ---
|
|
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then
|
|
SAFE_ONLY=true
|
|
RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm\s+(-[a-zA-Z]+\s+)*//;s/--recursive\s*//')
|
|
for target in $RM_ARGS; do
|
|
case "$target" in
|
|
*/node_modules|node_modules|*/\.next|\.next|*/dist|dist|*/__pycache__|__pycache__|*/\.cache|\.cache|*/build|build|*/\.turbo|\.turbo|*/coverage|coverage)
|
|
;; # safe target
|
|
-*)
|
|
;; # flag, skip
|
|
*)
|
|
SAFE_ONLY=false
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
if [ "$SAFE_ONLY" = true ]; then
|
|
echo '{}'
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# --- Destructive pattern checks ---
|
|
WARN=""
|
|
PATTERN=""
|
|
|
|
# rm -rf / rm -r / rm --recursive
|
|
if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r|--recursive)' 2>/dev/null; then
|
|
WARN="Destructive: recursive delete (rm -r). This permanently removes files."
|
|
PATTERN="rm_recursive"
|
|
fi
|
|
|
|
# DROP TABLE / DROP DATABASE
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE 'drop\s+(table|database)' 2>/dev/null; then
|
|
WARN="Destructive: SQL DROP detected. This permanently deletes database objects."
|
|
PATTERN="drop_table"
|
|
fi
|
|
|
|
# TRUNCATE
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD_LOWER" | grep -qE '\btruncate\b' 2>/dev/null; then
|
|
WARN="Destructive: SQL TRUNCATE detected. This deletes all rows from a table."
|
|
PATTERN="truncate"
|
|
fi
|
|
|
|
# git push --force / git push -f
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+push\s+.*(-f\b|--force)' 2>/dev/null; then
|
|
WARN="Destructive: git force-push rewrites remote history. Other contributors may lose work."
|
|
PATTERN="git_force_push"
|
|
fi
|
|
|
|
# git reset --hard
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+reset\s+--hard' 2>/dev/null; then
|
|
WARN="Destructive: git reset --hard discards all uncommitted changes."
|
|
PATTERN="git_reset_hard"
|
|
fi
|
|
|
|
# git checkout . / git restore .
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'git\s+(checkout|restore)\s+\.' 2>/dev/null; then
|
|
WARN="Destructive: discards all uncommitted changes in the working tree."
|
|
PATTERN="git_discard"
|
|
fi
|
|
|
|
# kubectl delete
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'kubectl\s+delete' 2>/dev/null; then
|
|
WARN="Destructive: kubectl delete removes Kubernetes resources. May impact production."
|
|
PATTERN="kubectl_delete"
|
|
fi
|
|
|
|
# docker rm -f / docker system prune
|
|
if [ -z "$WARN" ] && printf '%s' "$CMD" | grep -qE 'docker\s+(rm\s+-f|system\s+prune)' 2>/dev/null; then
|
|
WARN="Destructive: Docker force-remove or prune. May delete running containers or cached images."
|
|
PATTERN="docker_destructive"
|
|
fi
|
|
|
|
# --- Output ---
|
|
if [ -n "$WARN" ]; then
|
|
# Log hook fire event (pattern name only, never command content)
|
|
mkdir -p ~/.gstack/analytics 2>/dev/null || true
|
|
echo '{"event":"hook_fire","skill":"careful","pattern":"'"$PATTERN"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
|
|
|
|
WARN_ESCAPED=$(printf '%s' "$WARN" | sed 's/"/\\"/g')
|
|
printf '{"permissionDecision":"ask","message":"[careful] %s"}\n' "$WARN_ESCAPED"
|
|
else
|
|
echo '{}'
|
|
fi
|