garrytan / garrytan/gstack

/freeze enforcement chain has three independent install/runtime defects

Open
#1,459 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.