Skip to content

fix: /freeze and /careful enforcement chain — three independent bugs (#1459)#1509

Open
NikhileshNanduri wants to merge 3 commits into
garrytan:mainfrom
NikhileshNanduri:fix/1459-freeze-enforcement-chain
Open

fix: /freeze and /careful enforcement chain — three independent bugs (#1459)#1509
NikhileshNanduri wants to merge 3 commits into
garrytan:mainfrom
NikhileshNanduri:fix/1459-freeze-enforcement-chain

Conversation

@NikhileshNanduri
Copy link
Copy Markdown

Summary

Fixes issue #1459: /freeze and /careful provided zero actual enforcement. An Edit outside the freeze boundary would succeed silently despite telemetry logging a boundary_deny event. Three independent bugs in the install/runtime chain were all required:

Bug Root cause Symptom
Bug 1 link_claude_skill_dirs symlinked SKILL.md but not bin/ ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh resolved to a non-existent path — hook invocation always fails silently
Bug 2 SKILL.md frontmatter hooks: is decoration — Claude Code only reads ~/.claude/settings.json Hook never fired at all from a real Edit/Write call
Bug 3 Scripts returned {"permissionDecision":"deny","message":"..."} — flat object, unrecognised by Claude Code Hook fired, deny was silently ignored; tool executed anyway

Changes

Bug 1 — bin/ symlink (setup, bin/gstack-relink):

  • link_claude_skill_dirs() now symlinks each skill's bin/ alongside SKILL.md
  • gstack-relink has the same fix for the upgrade path

Bug 2 — hook registration (bin/gstack-settings-hook, setup):

  • gstack-settings-hook gains add-pretooluse <matcher> <command> and remove-pretooluse actions (idempotent, atomic write via .tmp + rename)
  • setup Step 11 calls these at install time to write freeze (Edit|Write|MultiEdit) and careful (Bash) entries into ~/.claude/settings.json

Bug 3 — hookSpecificOutput format (freeze/bin/check-freeze.sh, careful/bin/check-careful.sh):

  • Both scripts now output the correct envelope: {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}}
  • check-freeze.sh also resolves its state root via gstack-paths to honour GSTACK_HOME in addition to CLAUDE_PLUGIN_DATA
  • check-careful.sh is now a no-op unless ~/.gstack/careful-active.txt exists — safe to leave the hook registered globally without polluting sessions where /careful was never invoked. /careful SKILL.md now writes that file and documents how to clear it

Tests (test/hook-scripts.test.ts):

  • All careful tests use withCarefulActive() helper and pass CLAUDE_PLUGIN_DATA to the hook
  • Freeze tests assert the hookSpecificOutput envelope shape (2 new tests)
  • 35 tests passing, 0 failing

Test plan

  • bun test test/hook-scripts.test.ts — 35 pass, 0 fail
  • Pre-existing doc-inventory failures for document-generate confirmed pre-existing on main (stash test)
  • End-to-end: run ./setup, invoke /freeze src/, attempt Edit on a file outside src/ — should be blocked with [freeze] Blocked: ...
  • /careful + rm -rf /var — should see permission ask prompt

🤖 Generated with Claude Code

NikhileshNanduri and others added 3 commits May 15, 2026 05:01
…ime (garrytan#1459 Bugs 1+2)

Bug 1: link_claude_skill_dirs() in setup and gstack-relink now symlink each
skill's bin/ directory alongside SKILL.md. Previously only SKILL.md was
linked, so hook scripts referenced in SKILL.md frontmatter (e.g.
${CLAUDE_SKILL_DIR}/bin/check-freeze.sh) resolved to non-existent paths.

Bug 2: gstack-settings-hook gains add-pretooluse / remove-pretooluse
actions. setup's new Step 11 calls these during install to write freeze
and careful PreToolUse entries into ~/.claude/settings.json. Claude Code
only fires hooks declared in settings.json — the SKILL.md frontmatter
hooks section is documentation; registration must happen separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rrytan#1459 Bugs 2+3)

Bug 3: check-freeze.sh and check-careful.sh were returning a flat
{"permissionDecision":"deny","message":"..."} object. Claude Code requires
the hookSpecificOutput envelope:
  {"hookSpecificOutput":{"hookEventName":"PreToolUse",
    "permissionDecision":"deny","permissionDecisionReason":"..."}}
Without the wrapper the decision is silently ignored and the tool executes.

Bug 2 (careful side): check-careful.sh now checks for
~/.gstack/careful-active.txt before inspecting any command; it is a no-op
when /careful has not been invoked. careful/SKILL.md now writes that file
on activation and explains how to clear it. This makes the globally-
registered hook safe to leave in settings.json permanently.

check-freeze.sh also resolves its state-root via gstack-paths so it
honours GSTACK_HOME in addition to CLAUDE_PLUGIN_DATA / $HOME/.gstack.

Tests: all careful tests now use withCarefulActive() and pass
CLAUDE_PLUGIN_DATA to the hook. freeze and careful tests assert the new
hookSpecificOutput shape. Two new freeze tests verify the envelope format
and confirm allow responses remain plain {}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three independent bugs all required for hooks to fire and enforce:
Bug 1 — bin/ not symlinked (hook script unreachable)
Bug 2 — hooks not registered in settings.json (never fires)
Bug 3 — wrong hookSpecificOutput format (fires, deny ignored)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant