From 533fdca1f2451104406f5c281f01b70021d3ac1b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 18 Mar 2026 11:05:18 -0700 Subject: [PATCH] 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) --- careful/SKILL.md | 59 ++++++++++++++++++ careful/SKILL.md.tmpl | 57 ++++++++++++++++++ careful/bin/check-careful.sh | 112 +++++++++++++++++++++++++++++++++++ freeze/SKILL.md | 82 +++++++++++++++++++++++++ freeze/SKILL.md.tmpl | 80 +++++++++++++++++++++++++ freeze/bin/check-freeze.sh | 68 +++++++++++++++++++++ guard/SKILL.md | 82 +++++++++++++++++++++++++ guard/SKILL.md.tmpl | 80 +++++++++++++++++++++++++ unfreeze/SKILL.md | 40 +++++++++++++ unfreeze/SKILL.md.tmpl | 38 ++++++++++++ 10 files changed, 698 insertions(+) create mode 100644 careful/SKILL.md create mode 100644 careful/SKILL.md.tmpl create mode 100755 careful/bin/check-careful.sh create mode 100644 freeze/SKILL.md create mode 100644 freeze/SKILL.md.tmpl create mode 100755 freeze/bin/check-freeze.sh create mode 100644 guard/SKILL.md create mode 100644 guard/SKILL.md.tmpl create mode 100644 unfreeze/SKILL.md create mode 100644 unfreeze/SKILL.md.tmpl diff --git a/careful/SKILL.md b/careful/SKILL.md new file mode 100644 index 00000000..7513b293 --- /dev/null +++ b/careful/SKILL.md @@ -0,0 +1,59 @@ +--- +name: careful +version: 0.1.0 +description: | + Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE, + force-push, git reset --hard, kubectl delete, and similar destructive operations. + User can override each warning. Use when touching prod, debugging live systems, + or working in a shared environment. Use when asked to "be careful", "safety mode", + "prod mode", or "careful mode". +allowed-tools: + - Bash + - Read +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh" + statusMessage: "Checking for destructive commands..." +--- + + + +# /careful — Destructive Command Guardrails + +Safety mode is now **active**. Every bash command will be checked for destructive +patterns before running. If a destructive command is detected, you'll be warned +and can choose to proceed or cancel. + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"careful","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 +``` + +## What's protected + +| Pattern | Example | Risk | +|---------|---------|------| +| `rm -rf` / `rm -r` / `rm --recursive` | `rm -rf /var/data` | Recursive delete | +| `DROP TABLE` / `DROP DATABASE` | `DROP TABLE users;` | Data loss | +| `TRUNCATE` | `TRUNCATE orders;` | Data loss | +| `git push --force` / `-f` | `git push -f origin main` | History rewrite | +| `git reset --hard` | `git reset --hard HEAD~3` | Uncommitted work loss | +| `git checkout .` / `git restore .` | `git checkout .` | Uncommitted work loss | +| `kubectl delete` | `kubectl delete pod` | Production impact | +| `docker rm -f` / `docker system prune` | `docker system prune -a` | Container/image loss | + +## Safe exceptions + +These patterns are allowed without warning: +- `rm -rf node_modules` / `.next` / `dist` / `__pycache__` / `.cache` / `build` / `.turbo` / `coverage` + +## How it works + +The hook reads the command from the tool input JSON, checks it against the +patterns above, and returns `permissionDecision: "ask"` with a warning message +if a match is found. You can always override the warning and proceed. + +To deactivate, end the conversation or start a new one. Hooks are session-scoped. diff --git a/careful/SKILL.md.tmpl b/careful/SKILL.md.tmpl new file mode 100644 index 00000000..d8bd4662 --- /dev/null +++ b/careful/SKILL.md.tmpl @@ -0,0 +1,57 @@ +--- +name: careful +version: 0.1.0 +description: | + Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE, + force-push, git reset --hard, kubectl delete, and similar destructive operations. + User can override each warning. Use when touching prod, debugging live systems, + or working in a shared environment. Use when asked to "be careful", "safety mode", + "prod mode", or "careful mode". +allowed-tools: + - Bash + - Read +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh" + statusMessage: "Checking for destructive commands..." +--- + +# /careful — Destructive Command Guardrails + +Safety mode is now **active**. Every bash command will be checked for destructive +patterns before running. If a destructive command is detected, you'll be warned +and can choose to proceed or cancel. + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"careful","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 +``` + +## What's protected + +| Pattern | Example | Risk | +|---------|---------|------| +| `rm -rf` / `rm -r` / `rm --recursive` | `rm -rf /var/data` | Recursive delete | +| `DROP TABLE` / `DROP DATABASE` | `DROP TABLE users;` | Data loss | +| `TRUNCATE` | `TRUNCATE orders;` | Data loss | +| `git push --force` / `-f` | `git push -f origin main` | History rewrite | +| `git reset --hard` | `git reset --hard HEAD~3` | Uncommitted work loss | +| `git checkout .` / `git restore .` | `git checkout .` | Uncommitted work loss | +| `kubectl delete` | `kubectl delete pod` | Production impact | +| `docker rm -f` / `docker system prune` | `docker system prune -a` | Container/image loss | + +## Safe exceptions + +These patterns are allowed without warning: +- `rm -rf node_modules` / `.next` / `dist` / `__pycache__` / `.cache` / `build` / `.turbo` / `coverage` + +## How it works + +The hook reads the command from the tool input JSON, checks it against the +patterns above, and returns `permissionDecision: "ask"` with a warning message +if a match is found. You can always override the warning and proceed. + +To deactivate, end the conversation or start a new one. Hooks are session-scoped. diff --git a/careful/bin/check-careful.sh b/careful/bin/check-careful.sh new file mode 100755 index 00000000..c8bc2c7a --- /dev/null +++ b/careful/bin/check-careful.sh @@ -0,0 +1,112 @@ +#!/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 diff --git a/freeze/SKILL.md b/freeze/SKILL.md new file mode 100644 index 00000000..00aaef61 --- /dev/null +++ b/freeze/SKILL.md @@ -0,0 +1,82 @@ +--- +name: freeze +version: 0.1.0 +description: | + Restrict file edits to a specific directory for the session. Blocks Edit and + Write outside the allowed path. Use when debugging to prevent accidentally + "fixing" unrelated code, or when you want to scope changes to one module. + Use when asked to "freeze", "restrict edits", "only edit this folder", + or "lock down edits". +allowed-tools: + - Bash + - Read + - AskUserQuestion +hooks: + PreToolUse: + - matcher: "Edit" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." + - matcher: "Write" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." +--- + + + +# /freeze — Restrict Edits to a Directory + +Lock file edits to a specific directory. Any Edit or Write operation targeting +a file outside the allowed path will be **blocked** (not just warned). + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"freeze","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 +``` + +## Setup + +Ask the user which directory to restrict edits to. Use AskUserQuestion: + +- Question: "Which directory should I restrict edits to? Files outside this path will be blocked from editing." +- Text input (not multiple choice) — the user types a path. + +Once the user provides a directory path: + +1. Resolve it to an absolute path: +```bash +FREEZE_DIR=$(cd "" 2>/dev/null && pwd) +echo "$FREEZE_DIR" +``` + +2. Ensure trailing slash and save to the freeze state file: +```bash +FREEZE_DIR="${FREEZE_DIR%/}/" +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +mkdir -p "$STATE_DIR" +echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt" +echo "Freeze boundary set: $FREEZE_DIR" +``` + +Tell the user: "Edits are now restricted to `/`. Any Edit or Write +outside this directory will be blocked. To change the boundary, run `/freeze` +again. To remove it, run `/unfreeze` or end the session." + +## How it works + +The hook reads `file_path` from the Edit/Write tool input JSON, then checks +whether the path starts with the freeze directory. If not, it returns +`permissionDecision: "deny"` to block the operation. + +The freeze boundary persists for the session via the state file. The hook +script reads it on every Edit/Write invocation. + +## Notes + +- The trailing `/` on the freeze directory prevents `/src` from matching `/src-old` +- Freeze applies to Edit and Write tools only — Read, Bash, Glob, Grep are unaffected +- This prevents accidental edits, not a security boundary — Bash commands like `sed` can still modify files outside the boundary +- To deactivate, run `/unfreeze` or end the conversation diff --git a/freeze/SKILL.md.tmpl b/freeze/SKILL.md.tmpl new file mode 100644 index 00000000..8765cc1f --- /dev/null +++ b/freeze/SKILL.md.tmpl @@ -0,0 +1,80 @@ +--- +name: freeze +version: 0.1.0 +description: | + Restrict file edits to a specific directory for the session. Blocks Edit and + Write outside the allowed path. Use when debugging to prevent accidentally + "fixing" unrelated code, or when you want to scope changes to one module. + Use when asked to "freeze", "restrict edits", "only edit this folder", + or "lock down edits". +allowed-tools: + - Bash + - Read + - AskUserQuestion +hooks: + PreToolUse: + - matcher: "Edit" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." + - matcher: "Write" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." +--- + +# /freeze — Restrict Edits to a Directory + +Lock file edits to a specific directory. Any Edit or Write operation targeting +a file outside the allowed path will be **blocked** (not just warned). + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"freeze","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 +``` + +## Setup + +Ask the user which directory to restrict edits to. Use AskUserQuestion: + +- Question: "Which directory should I restrict edits to? Files outside this path will be blocked from editing." +- Text input (not multiple choice) — the user types a path. + +Once the user provides a directory path: + +1. Resolve it to an absolute path: +```bash +FREEZE_DIR=$(cd "" 2>/dev/null && pwd) +echo "$FREEZE_DIR" +``` + +2. Ensure trailing slash and save to the freeze state file: +```bash +FREEZE_DIR="${FREEZE_DIR%/}/" +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +mkdir -p "$STATE_DIR" +echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt" +echo "Freeze boundary set: $FREEZE_DIR" +``` + +Tell the user: "Edits are now restricted to `/`. Any Edit or Write +outside this directory will be blocked. To change the boundary, run `/freeze` +again. To remove it, run `/unfreeze` or end the session." + +## How it works + +The hook reads `file_path` from the Edit/Write tool input JSON, then checks +whether the path starts with the freeze directory. If not, it returns +`permissionDecision: "deny"` to block the operation. + +The freeze boundary persists for the session via the state file. The hook +script reads it on every Edit/Write invocation. + +## Notes + +- The trailing `/` on the freeze directory prevents `/src` from matching `/src-old` +- Freeze applies to Edit and Write tools only — Read, Bash, Glob, Grep are unaffected +- This prevents accidental edits, not a security boundary — Bash commands like `sed` can still modify files outside the boundary +- To deactivate, run `/unfreeze` or end the conversation diff --git a/freeze/bin/check-freeze.sh b/freeze/bin/check-freeze.sh new file mode 100755 index 00000000..ed748e93 --- /dev/null +++ b/freeze/bin/check-freeze.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# check-freeze.sh — PreToolUse hook for /freeze skill +# Reads JSON from stdin, checks if file_path is within the freeze boundary. +# Returns {"permissionDecision":"deny","message":"..."} to block, or {} to allow. +set -euo pipefail + +# Read stdin +INPUT=$(cat) + +# Locate the freeze directory state file +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +FREEZE_FILE="$STATE_DIR/freeze-dir.txt" + +# If no freeze file exists, allow everything (not yet configured) +if [ ! -f "$FREEZE_FILE" ]; then + echo '{}' + exit 0 +fi + +FREEZE_DIR=$(tr -d '[:space:]' < "$FREEZE_FILE") + +# If freeze dir is empty, allow +if [ -z "$FREEZE_DIR" ]; then + echo '{}' + exit 0 +fi + +# Extract file_path from tool_input JSON +# Try grep/sed first, fall back to Python for escaped quotes +FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true) + +# Python fallback if grep returned empty +if [ -z "$FILE_PATH" ]; then + FILE_PATH=$(printf '%s' "$INPUT" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read()).get("tool_input",{}).get("file_path",""))' 2>/dev/null || true) +fi + +# If we couldn't extract a file path, allow (don't block on parse failure) +if [ -z "$FILE_PATH" ]; then + echo '{}' + exit 0 +fi + +# Resolve file_path to absolute if it isn't already +case "$FILE_PATH" in + /*) ;; # already absolute + *) + FILE_PATH="$(pwd)/$FILE_PATH" + ;; +esac + +# Normalize: remove double slashes and trailing slash +FILE_PATH=$(printf '%s' "$FILE_PATH" | sed 's|/\+|/|g;s|/$||') + +# Check: does the file path start with the freeze directory? +case "$FILE_PATH" in + "${FREEZE_DIR}"*) + # Inside freeze boundary — allow + echo '{}' + ;; + *) + # Outside freeze boundary — deny + # Log hook fire event + mkdir -p ~/.gstack/analytics 2>/dev/null || true + echo '{"event":"hook_fire","skill":"freeze","pattern":"boundary_deny","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 + + printf '{"permissionDecision":"deny","message":"[freeze] Blocked: %s is outside the freeze boundary (%s). Only edits within the frozen directory are allowed."}\n' "$FILE_PATH" "$FREEZE_DIR" + ;; +esac diff --git a/guard/SKILL.md b/guard/SKILL.md new file mode 100644 index 00000000..f846d38a --- /dev/null +++ b/guard/SKILL.md @@ -0,0 +1,82 @@ +--- +name: guard +version: 0.1.0 +description: | + Full safety mode: destructive command warnings + directory-scoped edits. + Combines /careful (warns before rm -rf, DROP TABLE, force-push, etc.) with + /freeze (blocks edits outside a specified directory). Use for maximum safety + when touching prod or debugging live systems. Use when asked to "guard mode", + "full safety", "lock it down", or "maximum safety". +allowed-tools: + - Bash + - Read + - AskUserQuestion +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh" + statusMessage: "Checking for destructive commands..." + - matcher: "Edit" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." + - matcher: "Write" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." +--- + + + +# /guard — Full Safety Mode + +Activates both destructive command warnings and directory-scoped edit restrictions. +This is the combination of `/careful` + `/freeze` in a single command. + +**Dependency note:** This skill references hook scripts from the sibling `/careful` +and `/freeze` skill directories. Both must be installed (they are installed together +by the gstack setup script). + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"guard","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 +``` + +## Setup + +Ask the user which directory to restrict edits to. Use AskUserQuestion: + +- Question: "Guard mode: which directory should edits be restricted to? Destructive command warnings are always on. Files outside the chosen path will be blocked from editing." +- Text input (not multiple choice) — the user types a path. + +Once the user provides a directory path: + +1. Resolve it to an absolute path: +```bash +FREEZE_DIR=$(cd "" 2>/dev/null && pwd) +echo "$FREEZE_DIR" +``` + +2. Ensure trailing slash and save to the freeze state file: +```bash +FREEZE_DIR="${FREEZE_DIR%/}/" +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +mkdir -p "$STATE_DIR" +echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt" +echo "Freeze boundary set: $FREEZE_DIR" +``` + +Tell the user: +- "**Guard mode active.** Two protections are now running:" +- "1. **Destructive command warnings** — rm -rf, DROP TABLE, force-push, etc. will warn before executing (you can override)" +- "2. **Edit boundary** — file edits restricted to `/`. Edits outside this directory are blocked." +- "To remove the edit boundary, run `/unfreeze`. To deactivate everything, end the session." + +## What's protected + +See `/careful` for the full list of destructive command patterns and safe exceptions. +See `/freeze` for how edit boundary enforcement works. diff --git a/guard/SKILL.md.tmpl b/guard/SKILL.md.tmpl new file mode 100644 index 00000000..4dc35244 --- /dev/null +++ b/guard/SKILL.md.tmpl @@ -0,0 +1,80 @@ +--- +name: guard +version: 0.1.0 +description: | + Full safety mode: destructive command warnings + directory-scoped edits. + Combines /careful (warns before rm -rf, DROP TABLE, force-push, etc.) with + /freeze (blocks edits outside a specified directory). Use for maximum safety + when touching prod or debugging live systems. Use when asked to "guard mode", + "full safety", "lock it down", or "maximum safety". +allowed-tools: + - Bash + - Read + - AskUserQuestion +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../careful/bin/check-careful.sh" + statusMessage: "Checking for destructive commands..." + - matcher: "Edit" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." + - matcher: "Write" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh" + statusMessage: "Checking freeze boundary..." +--- + +# /guard — Full Safety Mode + +Activates both destructive command warnings and directory-scoped edit restrictions. +This is the combination of `/careful` + `/freeze` in a single command. + +**Dependency note:** This skill references hook scripts from the sibling `/careful` +and `/freeze` skill directories. Both must be installed (they are installed together +by the gstack setup script). + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"guard","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 +``` + +## Setup + +Ask the user which directory to restrict edits to. Use AskUserQuestion: + +- Question: "Guard mode: which directory should edits be restricted to? Destructive command warnings are always on. Files outside the chosen path will be blocked from editing." +- Text input (not multiple choice) — the user types a path. + +Once the user provides a directory path: + +1. Resolve it to an absolute path: +```bash +FREEZE_DIR=$(cd "" 2>/dev/null && pwd) +echo "$FREEZE_DIR" +``` + +2. Ensure trailing slash and save to the freeze state file: +```bash +FREEZE_DIR="${FREEZE_DIR%/}/" +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +mkdir -p "$STATE_DIR" +echo "$FREEZE_DIR" > "$STATE_DIR/freeze-dir.txt" +echo "Freeze boundary set: $FREEZE_DIR" +``` + +Tell the user: +- "**Guard mode active.** Two protections are now running:" +- "1. **Destructive command warnings** — rm -rf, DROP TABLE, force-push, etc. will warn before executing (you can override)" +- "2. **Edit boundary** — file edits restricted to `/`. Edits outside this directory are blocked." +- "To remove the edit boundary, run `/unfreeze`. To deactivate everything, end the session." + +## What's protected + +See `/careful` for the full list of destructive command patterns and safe exceptions. +See `/freeze` for how edit boundary enforcement works. diff --git a/unfreeze/SKILL.md b/unfreeze/SKILL.md new file mode 100644 index 00000000..d4ad37e2 --- /dev/null +++ b/unfreeze/SKILL.md @@ -0,0 +1,40 @@ +--- +name: unfreeze +version: 0.1.0 +description: | + Clear the freeze boundary set by /freeze, allowing edits to all directories + again. Use when you want to widen edit scope without ending the session. + Use when asked to "unfreeze", "unlock edits", "remove freeze", or + "allow all edits". +allowed-tools: + - Bash + - Read +--- + + + +# /unfreeze — Clear Freeze Boundary + +Remove the edit restriction set by `/freeze`, allowing edits to all directories. + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"unfreeze","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 +``` + +## Clear the boundary + +```bash +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +if [ -f "$STATE_DIR/freeze-dir.txt" ]; then + PREV=$(cat "$STATE_DIR/freeze-dir.txt") + rm -f "$STATE_DIR/freeze-dir.txt" + echo "Freeze boundary cleared (was: $PREV). Edits are now allowed everywhere." +else + echo "No freeze boundary was set." +fi +``` + +Tell the user the result. Note that `/freeze` hooks are still registered for the +session — they will just allow everything since no state file exists. To re-freeze, +run `/freeze` again. diff --git a/unfreeze/SKILL.md.tmpl b/unfreeze/SKILL.md.tmpl new file mode 100644 index 00000000..12968579 --- /dev/null +++ b/unfreeze/SKILL.md.tmpl @@ -0,0 +1,38 @@ +--- +name: unfreeze +version: 0.1.0 +description: | + Clear the freeze boundary set by /freeze, allowing edits to all directories + again. Use when you want to widen edit scope without ending the session. + Use when asked to "unfreeze", "unlock edits", "remove freeze", or + "allow all edits". +allowed-tools: + - Bash + - Read +--- + +# /unfreeze — Clear Freeze Boundary + +Remove the edit restriction set by `/freeze`, allowing edits to all directories. + +```bash +mkdir -p ~/.gstack/analytics +echo '{"skill":"unfreeze","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 +``` + +## Clear the boundary + +```bash +STATE_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.gstack}" +if [ -f "$STATE_DIR/freeze-dir.txt" ]; then + PREV=$(cat "$STATE_DIR/freeze-dir.txt") + rm -f "$STATE_DIR/freeze-dir.txt" + echo "Freeze boundary cleared (was: $PREV). Edits are now allowed everywhere." +else + echo "No freeze boundary was set." +fi +``` + +Tell the user the result. Note that `/freeze` hooks are still registered for the +session — they will just allow everything since no state file exists. To re-freeze, +run `/freeze` again.