mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
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>
This commit is contained in:
@@ -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..."
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
# /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.
|
||||
@@ -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.
|
||||
Executable
+112
@@ -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
|
||||
@@ -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..."
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
# /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 "<user-provided-path>" 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 `<path>/`. 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
|
||||
@@ -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 "<user-provided-path>" 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 `<path>/`. 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
|
||||
Executable
+68
@@ -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
|
||||
@@ -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..."
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
# /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 "<user-provided-path>" 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 `<path>/`. 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.
|
||||
@@ -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 "<user-provided-path>" 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 `<path>/`. 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.
|
||||
@@ -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
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
# /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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user