Tested on macOS, GStack installed under ~/.claude/skills/gstack/. /freeze does not actually block Edit/Write/MultiEdit tool calls outside its configured boundary. Investigation revealed three independent issues:
1. Install routine does not symlink skill bin/ directories
link_claude_skill_dirs() in ~/.claude/skills/gstack/setup (lines 373-411) symlinks SKILL.md but not bin/. Result: ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh referenced in the SKILL.md hook frontmatter resolves to a non-existent path. Same defect affects /careful and /browse.
Workaround applied locally:
ln -s ~/.claude/skills/gstack/freeze/bin ~/.claude/skills/freeze/bin
2. Install routine does not register PreToolUse hooks with Claude Code
Hook frontmatter in SKILL.md is decorative — Claude Code only fires hooks declared in ~/.claude/settings.json or in a plugin's hooks/hooks.json. GStack ships skills, not plugins, and the install routine doesn't write to settings.json. Result: even with the bin/ symlink in place, the hook never fires from a real Edit tool call.
Workaround applied locally (then reverted, see below):
Manually added to ~/.claude/settings.json:
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "bash ~/.claude/skills/freeze/bin/check-freeze.sh"
}]
}
3. With the hook manually registered, deny decision is not enforced
After symlinking bin/ and adding the PreToolUse entry to settings.json pointing at check-freeze.sh:
- The hook fires on Edit attempts outside the boundary ✓
- A
boundary_deny telemetry entry gets written to ~/.gstack/analytics/skill-usage.jsonl ✓
- BUT the Edit tool returns success and the file is modified ✗
Test conditions: /freeze invoked via the Skill tool with boundary /tmp/freeze-test-in/. Attempted Edit on /tmp/freeze-test-out/foo.txt. File content changed from outside original to outside modified. Telemetry confirmed hook fired and classified the operation correctly:
{"event":"hook_fire","skill":"freeze","pattern":"boundary_deny","ts":"2026-05-13T02:11:06Z","repo":"cafe-route-tool"}
Likely candidates (not investigated): wrong JSON shape, wrong exit code, or wrong output stream — but the script's direct stdin test produces what looks like the right deny JSON:
{"permissionDecision":"deny","message":"[freeze] Blocked: /private/tmp/freeze-test-out/foo.txt is outside the freeze boundary (/private/tmp/freeze-test-in). Only edits within the frozen directory are allowed."}
Net effect
/freeze appears to work (skill invocation succeeds, state file gets set) but provides no actual protection. Users get false confidence that edits outside the boundary will be blocked.
I've reverted the settings.json change locally — running it as a global PreToolUse without enforcement just adds telemetry noise to every Edit/Write in every session. The bin/ symlink remains in place since it doesn't hurt anything.
Happy to share full investigation transcripts if useful.
Environment: macOS, Claude Code (Sonnet/Opus), GStack installed via the standard setup script.
Tested on macOS, GStack installed under
~/.claude/skills/gstack/./freezedoes not actually block Edit/Write/MultiEdit tool calls outside its configured boundary. Investigation revealed three independent issues:1. Install routine does not symlink skill
bin/directorieslink_claude_skill_dirs()in~/.claude/skills/gstack/setup(lines 373-411) symlinksSKILL.mdbut notbin/. Result:${CLAUDE_SKILL_DIR}/bin/check-freeze.shreferenced in the SKILL.md hook frontmatter resolves to a non-existent path. Same defect affects/carefuland/browse.Workaround applied locally:
ln -s ~/.claude/skills/gstack/freeze/bin ~/.claude/skills/freeze/bin
2. Install routine does not register PreToolUse hooks with Claude Code
Hook frontmatter in SKILL.md is decorative — Claude Code only fires hooks declared in
~/.claude/settings.jsonor in a plugin'shooks/hooks.json. GStack ships skills, not plugins, and the install routine doesn't write tosettings.json. Result: even with the bin/ symlink in place, the hook never fires from a real Edit tool call.Workaround applied locally (then reverted, see below):
Manually added to
~/.claude/settings.json:{ "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "bash ~/.claude/skills/freeze/bin/check-freeze.sh" }] }3. With the hook manually registered, deny decision is not enforced
After symlinking
bin/and adding thePreToolUseentry tosettings.jsonpointing atcheck-freeze.sh:boundary_denytelemetry entry gets written to~/.gstack/analytics/skill-usage.jsonl✓Test conditions:
/freezeinvoked via the Skill tool with boundary/tmp/freeze-test-in/. Attempted Edit on/tmp/freeze-test-out/foo.txt. File content changed fromoutside originaltooutside modified. Telemetry confirmed hook fired and classified the operation correctly:{"event":"hook_fire","skill":"freeze","pattern":"boundary_deny","ts":"2026-05-13T02:11:06Z","repo":"cafe-route-tool"}
Likely candidates (not investigated): wrong JSON shape, wrong exit code, or wrong output stream — but the script's direct stdin test produces what looks like the right deny JSON:
{"permissionDecision":"deny","message":"[freeze] Blocked: /private/tmp/freeze-test-out/foo.txt is outside the freeze boundary (/private/tmp/freeze-test-in). Only edits within the frozen directory are allowed."}Net effect
/freezeappears to work (skill invocation succeeds, state file gets set) but provides no actual protection. Users get false confidence that edits outside the boundary will be blocked.I've reverted the
settings.jsonchange locally — running it as a global PreToolUse without enforcement just adds telemetry noise to every Edit/Write in every session. Thebin/symlink remains in place since it doesn't hurt anything.Happy to share full investigation transcripts if useful.
Environment: macOS, Claude Code (Sonnet/Opus), GStack installed via the standard setup script.