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.4---56# ABOUTME: /freeze sets, shows and lifts the repo-local edit boundary read by hooks/freeze-guard.sh7# ABOUTME: Focus aid for long debugging sessions, deliberately not a security boundary89# /freeze1011Keep Edit, Write and NotebookEdit inside one directory of the current repo. The boundary is a12single file, `.freeze-boundary`, at the git root, holding one absolute physical directory path.13`hooks/freeze-guard.sh` reads it on every mutating tool call and denies paths outside it.1415## What this is not1617- **Not a security boundary.** `Bash` is not gated: `sed -i`, `cat >`, `git checkout` and any18 other shell write go straight through. gstack, where this idea comes from, says the same thing19 about its own version. Treat it as a focus aid that makes an out-of-scope edit loud, not as20 containment.21- **Not per-agent.** The boundary is session-wide and repo-local. Parallel subagents share the22 process and therefore the boundary; it does **not** give each of them its own scope.23- **Not persistent policy.** Nothing re-applies it. A new session starts unfrozen unless the24 file survived on disk, which it will: `/freeze off` is a deliberate step, not an automatic one.2526## Commands2728### `/freeze <dir>`29301. Resolve the repo root and the target, both physical (`pwd -P`, so a symlinked checkout and the31 hook agree on one spelling):3233 ```bash34 root=$(git rev-parse --show-toplevel) && root=$(cd "$root" && pwd -P)35 dir=$(cd "<dir>" && pwd -P)36 ```3738 If either `cd` fails, stop and say why: an unresolvable path would write a boundary that denies39 everything.40412. Refuse a target outside the repo. A boundary the repo cannot contain freezes the whole repo42 while enforcing nothing elsewhere (the guard is repo-local by design):4344 ```bash45 case "$dir/" in "$root"/*) ;; *) echo "refusing: $dir is outside $root"; exit 1 ;; esac46 ```47483. Write the boundary, with the trailing slash the guard's prefix match expects:4950 ```bash51 printf '%s/\n' "${dir%/}" > "$root/.freeze-boundary"52 ```53544. Gitignore guard, exactly once (same idiom the orchestrator uses for `quality_reports/reviews/`:55 check first, append if absent, never rewrite the file):5657 ```bash58 gi="$root/.gitignore"59 if ! grep -qxF '/.freeze-boundary' "$gi" 2>/dev/null; then60 nl=""61 # Command substitution strips trailing newlines, so a non-empty result means the62 # file does not end in one and the append would glue onto the last line, silently63 # corrupting an existing ignore rule in someone else's repo.64 [ -s "$gi" ] && [ -n "$(tail -c 1 "$gi")" ] && nl=$'\n'65 printf '%s/.freeze-boundary\n' "$nl" >> "$gi"66 fi67 ```6869 The entry is anchored (`/.freeze-boundary`) because the file only ever lives at the root.70715. Report the boundary and the escape hatch in one line: `frozen: <dir> (lift with /freeze off)`.7273### `/freeze status`7475```bash76root=$(git rev-parse --show-toplevel 2>/dev/null) && cat "$root/.freeze-boundary" 2>/dev/null77```7879Print the boundary path, or `no boundary set` when the file is absent or empty. Do not create it.8081### `/freeze off`8283```bash84root=$(git rev-parse --show-toplevel) && rm -f "$root/.freeze-boundary"85```8687Report `boundary lifted`. Leave the `.gitignore` line alone: it is inert when the file is gone,88and removing it would just churn the diff on the next freeze.8990## How the guard behaves9192Read `hooks/freeze-guard.sh` before changing anything here; the two must stay in step, and93`hooks/tests/test_hook_constants_sync.py` asserts the basename quoted above matches94`FREEZE_BOUNDARY_BASENAME` in `hooks/_freeze_boundary.sh`.9596| Situation | Guard |97|-----------|-------|98| No boundary file at the edited file's git root | allows, zero output |99| Edit inside the boundary | allows |100| Edit outside the boundary, same repo | denies, message names the boundary and `/freeze off` |101| Edit in a different repo | allows (the boundary is repo-local) |102| Edited path (`file_path`, or `notebook_path` for NotebookEdit) missing, empty or newline-bearing | allows, one-line stderr warning |103| Boundary file empty or whitespace-only | allows, one-line stderr warning |104| `jq` not installed | allows, one-line stderr warning |105106Every failure path allows. A false deny would block every edit in the repo until someone notices107and lifts the boundary; a false allow shows up in the diff.108109## When to use110111- Long debugging sessions where the fix is known to live under one directory.112- Handing a scoped subtask to yourself after a `LOCALIZE` step: the file list is already decided.113114## When NOT to use115116- Anything that must be enforced rather than nudged. Use `permissions.deny` in settings for that.117- Multi-directory work. One boundary, one directory: freezing the repo root is a no-op.