/freeze enforcement chain has three independent install/runtime defects
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
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`:
```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:
```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.
Contributor guide
Assessment
This issue has not been assessed yet.