mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-19 03:12:21 +02:00
fix(hooks): nest freeze/careful permissionDecision under hookSpecificOutput
Claude Code ignores a top-level permissionDecision, so the /freeze deny and /careful ask guards silently allowed everything. Nest both under hookSpecificOutput with permissionDecisionReason, update the shape-blind tests to pin the nested form, and document the constraint in both skill templates (regen included). Closes half of #1459 (freeze enforcement chain). Contributed by @jawadakram20 (PR #2331; team-init hunk deferred to the dedicated team-init fix). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1d41ee3ab3
commit
2918542c78
+4
-2
@@ -77,8 +77,10 @@ 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.
|
||||
whether the path starts with the freeze directory. If not, it returns a
|
||||
`hookSpecificOutput` payload with `permissionDecision: "deny"` to block the
|
||||
operation (nested under `hookSpecificOutput` — Claude Code ignores a top-level
|
||||
`permissionDecision`).
|
||||
|
||||
The freeze boundary persists for the session via the state file. The hook
|
||||
script reads it on every Edit/Write invocation.
|
||||
|
||||
@@ -72,8 +72,10 @@ 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.
|
||||
whether the path starts with the freeze directory. If not, it returns a
|
||||
`hookSpecificOutput` payload with `permissionDecision: "deny"` to block the
|
||||
operation (nested under `hookSpecificOutput` — Claude Code ignores a top-level
|
||||
`permissionDecision`).
|
||||
|
||||
The freeze boundary persists for the session via the state file. The hook
|
||||
script reads it on every Edit/Write invocation.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/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.
|
||||
# Returns a PreToolUse hookSpecificOutput with permissionDecision "deny" to block,
|
||||
# or {} to allow. The decision MUST be nested under hookSpecificOutput — Claude
|
||||
# Code ignores a top-level permissionDecision, which silently no-ops the block.
|
||||
set -euo pipefail
|
||||
|
||||
# Read stdin
|
||||
@@ -74,6 +76,6 @@ case "$FILE_PATH" in
|
||||
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"
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"[freeze] Blocked: %s is outside the freeze boundary (%s). Only edits within the frozen directory are allowed."}}\n' "$FILE_PATH" "$FREEZE_DIR"
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user