mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
* fix(careful): check PowerShell tool commands and nested pwsh/cmd (#3067) /careful and /guard matched only the Bash tool, so on Windows, where Claude Code's PowerShell tool is the primary shell, the guard never ran. check-careful.sh also had no PowerShell or cmd patterns. - careful/guard register a PowerShell matcher beside Bash. - hook-extract.sh gains gstack_hook_extract_tool: one parser call reads tool_name with the field, so dispatch adds no fork. - check-careful.sh runs one PowerShell/cmd pattern table on PowerShell tool commands and on the tail of any Bash command that launches pwsh/powershell/cmd: Remove-Item and aliases with -Recurse/-Force prefixes, rd/rmdir/del/erase /s, Format-Volume, Clear-Disk, Clear-Content, [IO.Directory]/[IO.File]::Delete, and asks with an explanation for -EncodedCommand, iex, Start-Process of a shell and & $cmd. Command-position anchors keep Bash negative controls allowed. - /careful documents the coverage as best-effort and points to Claude Code permission deny rules. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(freeze): NotebookEdit respects the freeze boundary (#3067) NotebookEdit was in no freeze/guard/investigate matcher, and check-freeze.sh read only file_path, so notebook edits ignored /freeze on every platform. NotebookEdit carries tool_input.notebook_path. - freeze, guard and investigate register a NotebookEdit matcher beside Edit and Write. - check-freeze.sh reads file_path, then notebook_path, through the shared extractor; denials name the tool, the field, the boundary and /unfreeze. A helper without the combined reader denies as out of date. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: run hook-scripts on the Windows lane (#3067) The careful/freeze hooks are Windows coverage, but test/hook-scripts.test.ts was excluded from windows-free-tests by the raw /tmp/ pattern; its /tmp/ literals are payload strings that are never opened. Admit it by name and gate the three cases Windows runners cannot host: a symlink (no Developer Mode), a newline in a directory name, and a POSIX ':' PATH prefix. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(evals): check-careful.sh is free-only for PR selection No paid fixture copies or runs careful/bin/check-careful.sh (the investigate boundary fixture copies hook-extract.sh and check-freeze.sh only); test/hook-scripts.test.ts covers it. As an unknown dependency it restored the full paid gate on every /careful hook change. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(careful): pin the #3067 Invoke-Sqlcmd DROP TABLE example on the PowerShell tool Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(evals): record the check-careful.sh selection change as non-behavioral Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: /careful covers PowerShell (best-effort), /freeze covers NotebookEdit (#3067) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: release v1.91.37.0 (CHANGELOG, TODOS for deferred hook follow-ups) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: re-slot release to v1.91.39.0 (v1.91.37.0 and v1.91.38.0 claimed by #3075, #3076) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: re-slot release to v1.91.41.0 (merge-queue reservation) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(careful): hook payload reader survives Windows text-mode Python and backslash source paths windows-free-tests (run 37739845723) showed every PowerShell-tool case falling through: Python's text-mode stdout on Windows wrote tool_name as "PowerShell\r", so dispatch never matched, and freeze denials carried stray CRs. gstack_hook_extract_tool now reads and writes bytes. hook-extract.sh also located gstack-state-root.sh with ${BASH_SOURCE%/*}, which strips nothing when the file is sourced through a C:\... path, so gstack_hook_state_root vanished (state-root parity case on the same run). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(evals): PR lane selects office-hours-auto-mode by its real test name The PR paid lane filters audited files with -t built from CASE_TEST_NAMES[id] ?? id. office-hours-auto-mode's Bun test is named 'AskUserQuestion surfaces when --disallowedTools AskUserQuestion is set', so the filter matched 0 tests and the shard failed (#3077 eval-slices (6), run 37741434771). Map the label, and check every audited PR-profile case is addressable by name. Recorded as non-behavioral for HARNESS_VERSION. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: CHANGELOG notes for the Windows CRLF and PR-lane name fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: re-slot release to v1.91.47.0 (merge-queue assignment) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
97 lines
4.3 KiB
Cheetah
97 lines
4.3 KiB
Cheetah
---
|
|
name: freeze
|
|
version: 0.1.0
|
|
description: |
|
|
Restrict file edits to a specific directory for the session. Blocks Edit,
|
|
Write and NotebookEdit outside the allowed path. Use when debugging to prevent accidentally
|
|
"fixing" unrelated code, or when you want to scope changes to one module.
|
|
Use when asked to "freeze", "restrict edits", "only edit this folder",
|
|
or "lock down edits". (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..."
|
|
sensitive: true
|
|
---
|
|
|
|
# /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 "{{RUNTIME_ROOT}}/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
|