freeze · garrytan — Claude Code skill: what it does, how to install
The skill blocks Edit, Write and NotebookEdit outside one directory. The hook returns a deny, not a warning.
npx skills add garrytan/gstack --skill freeze -a claude-codeWhat freeze does
The skill asks which folder is the boundary, then stores that path through freeze-state.sh. From then on every edit call passes through check-freeze.sh, which pulls file_path from the tool input (or notebook_path for a Jupyter notebook) and compares it against the boundary. No match, no edit: the hook answers with permissionDecision: deny.
It fails closed. A payload the hook cannot parse is denied, since a boundary that lets odd input through is not a boundary. Symlinks resolve through their final component, so a link sitting inside the folder but pointing elsewhere gets checked against its target. A trailing slash keeps /src from matching /src-old, and paths with spaces work. State lives in a file and outlives the conversation — ending or killing the session does not clear it.
Only the file-editing tools are hooked. Read, Bash, Glob and Grep behave as before, and that cuts both ways: sed or Set-Content from Bash will happily write outside the boundary. Read this as protection against accidental edits rather than a security wall. Run /unfreeze to drop the limit, /freeze again to move it.
What it helps with
- keep an agent out of files beyond
src/ - hold an edit boundary across a long session
- catch a stray write into a neighbouring package
- verify that a symlink does not carry an edit out of the folder
- lift the limit once work in the folder is done
Example prompts for the agent
- `/freeze` and restrict edits to `packages/api/`
- Lock editing to `src/` until the session ends
- `/unfreeze` — we are done with the boundary
How to install freeze
1Skills CLI (Vercel)
npx skills add garrytan/gstack --skill freeze -a claude-code2Manually: put the files in place
~/.claude/skills/freeze/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/freeze/
1Skills CLI (Vercel)
npx skills add garrytan/gstack --skill freeze -a codex2Manually: put the files in place
.agents/skills/freeze/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add garrytan/gstack --skill freeze -a cursor2Manually: put the files in place
.cursor/skills/freeze/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install garrytan/gstack freeze2Manually: put the files in place
.github/skills/freeze/SKILL.md1Skills CLI (Vercel)
npx skills add garrytan/gstack --skill freeze -a gemini-cli2Platform CLI
gemini skills install https://github.com/garrytan/gstack --path freezeManually: put the files in place
.devin/skills/freeze/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add garrytan/gstack --skill freeze -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set12
The whole set →Other versions of freeze
Similar
Questions about freeze
How do I install freeze in Claude Code?
With one command: npx skills add garrytan/gstack --skill freeze -a claude-code. It leaves the skill at ~/.claude/skills/freeze/SKILL.md.
What does freeze do?
It locks edits to a single directory: Edit, Write and NotebookEdit outside that path get denied by the hook. The boundary stays until /unfreeze.
Does it block edits made through Bash?
No. The hook covers the file-editing tools only, so sed or Set-Content run through Bash still change files past the boundary. It guards against accidental edits, not against a determined agent.
Source text
SKILL.md · MIT
---
name: freeze
version: 0.1.0
description: Restrict file edits to a specific directory for the session. (gstack)
triggers:
- freeze edits to directory
- lock editing scope
- restrict file changes
allowed-tools:
- Bash
- Read
- AskUserQuestion
hooks:
PreToolUse:
- matcher: "Edit"
hooks:
- type: command
command: 'bash -c "exec bash \"$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh\""'
statusMessage: "Checking freeze boundary..."
- matcher: "Write"
hooks:
- type: command
command: 'bash -c "exec bash \"$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh\""'
statusMessage: "Checking freeze boundary..."
- matcher: "NotebookEdit"
hooks:
- type: command
command: 'bash -c "exec bash \"$HOME/.claude/skills/gstack/freeze/bin/check-freeze.sh\""'
statusMessage: "Checking freeze boundary..."
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
# /freeze — Restrict Edits to a Directory
Lock file edits to a specific directory. Any Edit, Write or NotebookEdit
operation targeting a file outside the allowed path will be **blocked** (not
just warned).
```bash
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
mkdir -p "$GSTACK_STATE_ROOT"/analytics
echo '{"skill":"freeze","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> "$GSTACK_STATE_ROOT"/analytics/skill-usage.jsonl 2>/dev/null || true
```
## Setup
Ask the user which directory to restrict edits to. Use AskUserQuestion:
- Question: "Which directory should I restrict edits to? Files outside this path will be blocked from editing."
- Text input (not multiple choice) — the user types a path.
Once the user provides a directory path:
Set the user-selected boundary with the shared state writer. It resolves the physical absolute path and serializes replacement with investigation cleanup:
```bash
bash "$HOME/.claude/skills/gstack/freeze/bin/freeze-state.sh" set "<user-provided-path>"
```
Only report success if the helper succeeds. On `FREEZE_BUSY` or unexpected state, preserve it and ask the user to inspect recovery after any active writer finishes; never write or delete the state file directly.
Tell the user: "Edits are now restricted to `<path>/`. Any Edit, Write or
NotebookEdit outside this directory will be blocked. To change the boundary, run `/freeze`
again. To remove it, run `/unfreeze`."
## How it works
The hook reads `file_path` from the Edit/Write tool input JSON, or
`notebook_path` from a NotebookEdit (Jupyter notebook) call (shared
real-JSON extractor with /careful — one copy, sourced by both hooks), then
checks whether the path starts with the freeze directory. If not, it returns a
`hookSpecificOutput` payload with `permissionDecision: "deny"` to block the
operation (nested under `hookSpecificOutput` — Claude Code ignores a top-level
`permissionDecision`).
Polarity is fail-closed: a tool payload the hook cannot parse is DENIED, not
allowed — a boundary that fails open is not a boundary. A payload that parses
but has neither path field (a non-file tool) is allowed. A deny names the
tool, the path field, the boundary and `/unfreeze`. Symlinks are resolved
through their FINAL component, so an in-boundary symlink pointing outside the
boundary is checked against its target.
The freeze boundary persists until explicitly removed via the state file. The hook
script reads it on every Edit/Write/NotebookEdit invocation. Boundaries containing spaces
are supported.
## Notes
- The trailing `/` on the freeze directory prevents `/src` from matching `/src-old`
- Freeze applies to Edit, Write and NotebookEdit only — Read, Bash, PowerShell, Glob, Grep are unaffected
- This prevents accidental edits, not a security boundary — Bash or PowerShell commands like `sed` or `Set-Content` can still modify files outside the boundary
- To deactivate, run `/unfreeze`; ending or killing a conversation does not remove persisted state
© garrytan · MIT · garrytan/gstack