Skip to content

/freeze enforcement chain has three independent install/runtime defects #1459

@jerrysam13

Description

@jerrysam13

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions