ABOUTME: /freeze sets, shows and lifts the repo-local edit boundary read by hooks/freeze-guard.sh
ABOUTME: Focus aid for long debugging sessions, deliberately not a security boundary
/freeze
Keep Edit, Write and NotebookEdit inside one directory of the current repo. The boundary is a
single file, .freeze-boundary, at the git root, holding one absolute physical directory path.
hooks/freeze-guard.sh reads it on every mutating tool call and denies paths outside it.
What this is not
- Not a security boundary.
Bash is not gated: sed -i, cat >, git checkout and any
other shell write go straight through. gstack, where this idea comes from, says the same thing
about its own version. Treat it as a focus aid that makes an out-of-scope edit loud, not as
containment.
- Not per-agent. The boundary is session-wide and repo-local. Parallel subagents share the
process and therefore the boundary; it does not give each of them its own scope.
- Not persistent policy. Nothing re-applies it. A new session starts unfrozen unless the
file survived on disk, which it will:
/freeze off is a deliberate step, not an automatic one.
Commands
/freeze <dir>
Resolve the repo root and the target, both physical (pwd -P, so a symlinked checkout and the
hook agree on one spelling):
root=$(git rev-parse --show-toplevel) && root=$(cd "$root" && pwd -P)
dir=$(cd "<dir>" && pwd -P)
If either cd fails, stop and say why: an unresolvable path would write a boundary that denies
everything.
Refuse a target outside the repo. A boundary the repo cannot contain freezes the whole repo
while enforcing nothing elsewhere (the guard is repo-local by design):
case "$dir/" in "$root"/*) ;; *) echo "refusing: $dir is outside $root"; exit 1 ;; esac
Write the boundary, with the trailing slash the guard's prefix match expects:
printf '%s/\n' "${dir%/}" > "$root/.freeze-boundary"
Gitignore guard, exactly once (same idiom the orchestrator uses for quality_reports/reviews/:
check first, append if absent, never rewrite the file):
gi="$root/.gitignore"
if ! grep -qxF '/.freeze-boundary' "$gi" 2>/dev/null; then
nl=""
# Command substitution strips trailing newlines, so a non-empty result means the
# file does not end in one and the append would glue onto the last line, silently
# corrupting an existing ignore rule in someone else's repo.
[ -s "$gi" ] && [ -n "$(tail -c 1 "$gi")" ] && nl=$'\n'
printf '%s/.freeze-boundary\n' "$nl" >> "$gi"
fi
The entry is anchored (/.freeze-boundary) because the file only ever lives at the root.
Report the boundary and the escape hatch in one line: frozen: <dir> (lift with /freeze off).
/freeze status
root=$(git rev-parse --show-toplevel 2>/dev/null) && cat "$root/.freeze-boundary" 2>/dev/null
Print the boundary path, or no boundary set when the file is absent or empty. Do not create it.
/freeze off
root=$(git rev-parse --show-toplevel) && rm -f "$root/.freeze-boundary"
Report boundary lifted. Leave the .gitignore line alone: it is inert when the file is gone,
and removing it would just churn the diff on the next freeze.
How the guard behaves
Read hooks/freeze-guard.sh before changing anything here; the two must stay in step, and
hooks/tests/test_hook_constants_sync.py asserts the basename quoted above matches
FREEZE_BOUNDARY_BASENAME in hooks/_freeze_boundary.sh.
| Situation |
Guard |
| No boundary file at the edited file's git root |
allows, zero output |
| Edit inside the boundary |
allows |
| Edit outside the boundary, same repo |
denies, message names the boundary and /freeze off |
| Edit in a different repo |
allows (the boundary is repo-local) |
Edited path (file_path, or notebook_path for NotebookEdit) missing, empty or newline-bearing |
allows, one-line stderr warning |
| Boundary file empty or whitespace-only |
allows, one-line stderr warning |
jq not installed |
allows, one-line stderr warning |
Every failure path allows. A false deny would block every edit in the repo until someone notices
and lifts the boundary; a false allow shows up in the diff.
When to use
- Long debugging sessions where the fix is known to live under one directory.
- Handing a scoped subtask to yourself after a
LOCALIZE step: the file list is already decided.
When NOT to use
- Anything that must be enforced rather than nudged. Use
permissions.deny in settings for that.
- Multi-directory work. One boundary, one directory: freezing the repo root is a no-op.
1---2name: freeze3description: "Restrict edits to one directory of the current repo for the rest of the session, so focused debugging cannot spill into unrelated files. Use when user says /freeze, freeze the boundary, freeze this dir, keep edits inside <dir>, unfreeze, or /freeze off. Writes a repo-local boundary file that the freeze-guard PreToolUse hook reads. Not a security boundary and not a scope mechanism for parallel subagents."4allowed-tools: [Bash, Read]5---67# ABOUTME: /freeze sets, shows and lifts the repo-local edit boundary read by hooks/freeze-guard.sh8# ABOUTME: Focus aid for long debugging sessions, deliberately not a security boundary910# /freeze1112Keep Edit, Write and NotebookEdit inside one directory of the current repo. The boundary is a13single file, `.freeze-boundary`, at the git root, holding one absolute physical directory path.14`hooks/freeze-guard.sh` reads it on every mutating tool call and denies paths outside it.1516## What this is not1718- **Not a security boundary.** `Bash` is not gated: `sed -i`, `cat >`, `git checkout` and any19 other shell write go straight through. gstack, where this idea comes from, says the same thing20 about its own version. Treat it as a focus aid that makes an out-of-scope edit loud, not as21 containment.22- **Not per-agent.** The boundary is session-wide and repo-local. Parallel subagents share the23 process and therefore the boundary; it does **not** give each of them its own scope.24- **Not persistent policy.** Nothing re-applies it. A new session starts unfrozen unless the25 file survived on disk, which it will: `/freeze off` is a deliberate step, not an automatic one.2627## Commands2829### `/freeze <dir>`30311. Resolve the repo root and the target, both physical (`pwd -P`, so a symlinked checkout and the32 hook agree on one spelling):3334 ```bash35 root=$(git rev-parse --show-toplevel) && root=$(cd "$root" && pwd -P)36 dir=$(cd "<dir>" && pwd -P)37 ```3839 If either `cd` fails, stop and say why: an unresolvable path would write a boundary that denies40 everything.41422. Refuse a target outside the repo. A boundary the repo cannot contain freezes the whole repo43 while enforcing nothing elsewhere (the guard is repo-local by design):4445 ```bash46 case "$dir/" in "$root"/*) ;; *) echo "refusing: $dir is outside $root"; exit 1 ;; esac47 ```48493. Write the boundary, with the trailing slash the guard's prefix match expects:5051 ```bash52 printf '%s/\n' "${dir%/}" > "$root/.freeze-boundary"53 ```54554. Gitignore guard, exactly once (same idiom the orchestrator uses for `quality_reports/reviews/`:56 check first, append if absent, never rewrite the file):5758 ```bash59 gi="$root/.gitignore"60 if ! grep -qxF '/.freeze-boundary' "$gi" 2>/dev/null; then61 nl=""62 # Command substitution strips trailing newlines, so a non-empty result means the63 # file does not end in one and the append would glue onto the last line, silently64 # corrupting an existing ignore rule in someone else's repo.65 [ -s "$gi" ] && [ -n "$(tail -c 1 "$gi")" ] && nl=$'\n'66 printf '%s/.freeze-boundary\n' "$nl" >> "$gi"67 fi68 ```6970 The entry is anchored (`/.freeze-boundary`) because the file only ever lives at the root.71725. Report the boundary and the escape hatch in one line: `frozen: <dir> (lift with /freeze off)`.7374### `/freeze status`7576```bash77root=$(git rev-parse --show-toplevel 2>/dev/null) && cat "$root/.freeze-boundary" 2>/dev/null78```7980Print the boundary path, or `no boundary set` when the file is absent or empty. Do not create it.8182### `/freeze off`8384```bash85root=$(git rev-parse --show-toplevel) && rm -f "$root/.freeze-boundary"86```8788Report `boundary lifted`. Leave the `.gitignore` line alone: it is inert when the file is gone,89and removing it would just churn the diff on the next freeze.9091## How the guard behaves9293Read `hooks/freeze-guard.sh` before changing anything here; the two must stay in step, and94`hooks/tests/test_hook_constants_sync.py` asserts the basename quoted above matches95`FREEZE_BOUNDARY_BASENAME` in `hooks/_freeze_boundary.sh`.9697| Situation | Guard |98|-----------|-------|99| No boundary file at the edited file's git root | allows, zero output |100| Edit inside the boundary | allows |101| Edit outside the boundary, same repo | denies, message names the boundary and `/freeze off` |102| Edit in a different repo | allows (the boundary is repo-local) |103| Edited path (`file_path`, or `notebook_path` for NotebookEdit) missing, empty or newline-bearing | allows, one-line stderr warning |104| Boundary file empty or whitespace-only | allows, one-line stderr warning |105| `jq` not installed | allows, one-line stderr warning |106107Every failure path allows. A false deny would block every edit in the repo until someone notices108and lifts the boundary; a false allow shows up in the diff.109110## When to use111112- Long debugging sessions where the fix is known to live under one directory.113- Handing a scoped subtask to yourself after a `LOCALIZE` step: the file list is already decided.114115## When NOT to use116117- Anything that must be enforced rather than nudged. Use `permissions.deny` in settings for that.118- Multi-directory work. One boundary, one directory: freezing the repo root is a no-op.