fix(hooks): fail-closed freeze + shared extractor + careful HIGH tier

Freeze boundary hook had four verified bugs: the grep-first JSON extractor
truncated at escaped quotes and failed OPEN on unparseable payloads; the deny
JSON was printf-interpolated so a quote- or newline-bearing path silently
no-oped the block; the freeze path read stripped INTERNAL spaces (a boundary
like ~/My Project could never match); and the path resolver skipped the final
component, letting an in-boundary symlink write through to an out-of-boundary
target.

Fixes, structurally: one shared sourced helper (careful/bin/hook-extract.sh)
now owns JSON extraction and JSON-encoded decision envelopes for BOTH hooks --
the two-copy drift is how freeze kept a broken extractor after careful's was
fixed. Freeze is now deny-tier fail-closed (unparseable payload denies,
parsed-but-no-file_path still allows), trims only leading/trailing whitespace,
and resolves symlinks through the final path component.

Careful gains a HIGH tier (hard deny, simple commands only): recursive delete
of /, ~, or $HOME, and force-push to the repo's default branch. Compound
commands always fall through to the MEDIUM ask; --force-with-lease is never
HIGH. Documented as a best-effort advisory hard-stop, not a policy boundary.
Plus additive-only project patterns (~/.gstack/careful-patterns.txt +
per-project file): config can only ADD warn rules, never suppress a baseline
family.

test/hook-scripts.test.ts: 89 tests incl. malformed-payload deny, parseable
deny JSON for hostile paths, space-bearing boundaries, symlink escape, HIGH
tier splits, additive invariant, invalid-regex resilience.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-15 23:12:19 -07:00
co-authored by Claude Fable 5
parent 2be6c06ba8
commit 455c805125
8 changed files with 565 additions and 66 deletions
+29 -1
View File
@@ -64,6 +64,34 @@ The hook reads the command from the tool input JSON, checks it against the
patterns above, and returns a `hookSpecificOutput` payload with
`permissionDecision: "ask"` and a warning reason if a match is found (the
decision must be nested under `hookSpecificOutput` — Claude Code ignores a
top-level `permissionDecision`). You can always override the warning and proceed.
top-level `permissionDecision`). You can always override a MEDIUM warning and
proceed.
## HIGH tier (hard deny)
A tiny set of catastrophic commands is **denied outright** while /careful is
active, not just warned:
- `rm -r`/`-R` targeting exactly `/`, `~`, or `$HOME`
- `git push --force` / `-f` to the repo's **default branch**
HIGH only fires on SIMPLE commands (no `;`, `&&`, `||`, `|`, newline) — string
matching cannot resolve what a compound command does, so compound shapes fall
through to the ordinary MEDIUM ask. `--force-with-lease` is deliberately not
matched (it's the safe force variant). This is a best-effort advisory
hard-stop, not a policy boundary: /careful is opt-in and session-scoped, so
the escape hatch is ending the /careful session.
## Project patterns (additive only)
Add your own warn rules — one POSIX ERE per line, `#` comments allowed — in:
- `~/.gstack/careful-patterns.txt` (all projects)
- `~/.gstack/projects/<slug>/careful-patterns.txt` (this project)
Matching lines warn with `[careful] Project rule matched: <pattern>`. Config
can only ADD rules: the files are consulted after the built-in families, so no
file content can suppress or weaken a baseline warning. Invalid regex lines
are skipped.
To deactivate, end the conversation or start a new one. Hooks are session-scoped.