#!/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